なぜ私はフックに失望しているのですか

記事の翻訳は、「React.jsDeveloper」コースの開始を見越して作成されました










フックはどのように役立ちますか?



何が、なぜがっかりしたのかをお話しする前に、私は実際にはフックのファンであることを公式に宣言したいと思います。



クラスのコンポーネントを置き換えるためにフックが作成されるとよく​​耳にします。残念ながら、Reactの公式サイトに公開された投稿「IntroducingHooks」は、率直に言って、この革新を宣伝しています。



フックは、クラスを作成せずに状態やその他のReact機能を使用できるReact16.8の革新です。



ここに表示されるメッセージは、「クラスはクールではありません!」のようになります。フックを使用するように動機付けるには十分ではありません。私の意見では、フックを使用すると、クロスカット機能の問題を以前のアプローチよりもエレガントに解決できます。ミックスイン高次コンポーネントレンダリング小道具などです。



ロギングおよび認証機能はすべてのコンポーネントに共通であり、フックを使用すると、そのような再利用可能な機能をコンポーネントにアタッチできます。



クラスコンポーネントの何が問題になっていますか?



小道具を入力として受け取り、React要素を返すステートレスコンポーネント(つまり、内部状態のないコンポーネント)には、理解できない美しさがあります。それは純粋な機能、つまり副作用のない機能です。



export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
  const Tag = `h${level}` as Taggable;

  return (
    <Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
      {children}
    </Tag>
  );
};


残念ながら、副作用がないため、ステートレスコンポーネントの使用が制限されます。結局のところ、状態の操作は不可欠です。Reactでは、これはステートフルなクラスBeanに副作用が追加されることを意味します。これらは、コンテナコンポーネントとも呼ばれます。それらは副作用を実行し、純粋なステートレス関数に小道具を渡します。



クラスベースのライフサイクルイベントには、いくつかのよく知られた問題があります。多くの人々は、メソッドcomponentDidMountとのロジックを繰り返さなければならないことに不満を持っていますcomponentDidUpdate



async componentDidMount() {
  const response = await get(`/users`);
  this.setState({ users: response.data });
};

async componentDidUpdate(prevProps) {
  if (prevProps.resource !== this.props.resource) {
    const response = await get(`/users`);
    this.setState({ users: response.data });
  }
};


遅かれ早かれ、すべての開発者はこの問題に直面します。



この副作用コードは、エフェクトフックを使用して単一のコンポーネントで実行できます。



const UsersContainer: React.FC = () => {
  const [ users, setUsers ] = useState([]);
  const [ showDetails, setShowDetails ] = useState(false);

 const fetchUsers = async () => {
   const response = await get('/users');
   setUsers(response.data);
 };

 useEffect( () => {
    fetchUsers(users)
  }, [ users ]
 );

 // etc.


フックuseEffectは生活をはるかに楽にしますが、以前使用していた純粋な機能(ステートレスコンポーネント)を奪います。これは私を失望させた最初のことです。



知っておくべきもう1つのJavaScriptパラダイム



私は49歳で、Reactのファンです。このオブザーバーと計算されたプロパティの狂気で残り火アプリ開発した後、私は常に一方向のデータフローに対して温かい気持ちになります。



フックuseEffectなどの問題は、JavaScriptランドスケープの他の場所では使用されないことです。彼は珍しく、一般的に奇妙です。私はそれを飼いならす唯一の方法を見る-実際にこのフックを使用して苦しむこと。そして、カウンターの例は、私が一晩中無私無欲にコーディングするように誘導することはありません。私はフリーランサーで、Reactだけでなく他のライブラリも使用していますが、すでに疲れていますこれらすべての革新に従ってください。正しい道に私を設定するeslintプラグインをインストールする必要があると思うとすぐに、この新しいパラダイムは私に負担をかけ始めます。



依存関係の配列は地獄です



useEffectフックが呼ばれるオプションの第二引数取ることができます依存列あなたがそれを必要とするとき、あなたが効果をコールバックすることを可能にします。変更が発生したかどうかを判断するために、ReactはObject.isメソッドを使用して値を相互に比較します最後のレンダリングサイクル以降に要素が変更された場合、その効果は新しい値に適用されます。



比較は、プリミティブデータタイプの処理に最適です。ただし、要素の1つがオブジェクトまたは配列である場合、問題が発生する可能性があります。Object.isは、オブジェクトと配列を参照によって比較しますが、それについては何もできません。カスタム比較アルゴリズムは適用できません。



参照によるオブジェクトの検証は、既知の障害です。私が最近遭遇した問題の簡略版を見てみましょう。



const useFetch = (config: ApiOptions) => {
  const  [data, setData] = useState(null);

  useEffect(() => {
    const { url, skip, take } = config;
    const resource = `${url}?$skip=${skip}&take=${take}`;
    axios({ url: resource }).then(response => setData(response.data));
  }, [config]); // <-- will fetch on each render

  return data;
};

const App: React.FC = () => {
  const data = useFetch({ url: "/users", take: 10, skip: 0 });
  return <div>{data.map(d => <div>{d})}</div>;
};


上のライン14useFetch新しいオブジェクトが渡される関数に、我々はそれはとても同じオブジェクトは毎回使用されていることを確認しない限り、各レンダリングのために。このシナリオでは、オブジェクトへの参照ではなく、オブジェクトのフィールドをチェックします。



Reactがこのソリューションのような深いオブジェクト比較を行わない理由を理解しています。したがって、フックを慎重に使用する必要があります。そうしないと、アプリケーションのパフォーマンスに重大な問題が発生する可能性があります。私はそれについて何ができるのか常に疑問に思っており、すでにいくつかのオプションを見つけています。より動的なオブジェクトの場合は、より多くの回避策を探す必要があります。エラーを自動的修正するeslintプラグイン



がありますコード検証中に見つかりました。あらゆるテキストエディタに適しています。正直なところ、テストするために外部プラグインをインストールする必要があるこれらすべての新機能に悩まされています。use-deep-object-compareuse-memo-one



などのプラグインの存在自体が、問題(または少なくとも混乱)が実際に存在することを示唆しています。



Reactは、フックが呼び出される順序に依存します



初期のカスタムフックはuseFetch、リモートAPIに要求を行うための関数の複数の実装でした。フックは機能コンポーネントの最初でしか使用できないため、それらのほとんどは、イベントハンドラーからリモートAPI要求を行う問題を解決しません。



しかし、データにページ付けされたサイトへのリンクがあり、ユーザーがリンクをクリックしたときにエフェクトを再実行したい場合はどうなりますか?簡単な使用例はuseFetch次のとおりです。



const useFetch = (config: ApiOptions): [User[], boolean] => {
  const [data, setData] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { skip, take } = config;

    api({ skip, take }).then(response => {
      setData(response);
      setLoading(false);
    });
  }, [config]);

  return [data, loading];
};

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<ApiOptions>({
    take: 10,
    skip: 0
  });

  const [users, loading] = useFetch(currentPage);

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {users.map((u: User) => (
        <div>{u.name}</div>
      ))}
      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li>
            <button onClick={() => console.log(' ?')}>{n + 1}</button>
          </li>
        ))}
      </ul>
    </>
  );
};


23行目では、フックuseFetchは最初のレンダリングで1回呼び出されます。35〜38行目では、ページネーションボタンをレンダリングしています。しかしuseFetch、これらのボタンのイベントハンドラーからフックをどのように呼び出すのでしょうか。



フックルールには、次のように明確に記載



されています。ループ、条件、またはネストされた関数内でフックを使用しないでください。代わりに、常にReact関数のトップレベルでのみフックを使用してください。



フックは、コンポーネントがレンダリングされるたびに同じ順序で呼び出されます。これにはいくつかの理由があり、この優れた投稿から学ぶことができます。



あなたはこれを行うことはできません:



<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
  {n + 1}
</button>


useFetchイベントハンドラーから フックを呼び出すと、レンダリングごとに呼び出しの順序が変わるため、フックのルールに違反します。



フックから実行可能な関数を返す



私はこの問題の2つの解決策に精通しています。彼らは同じアプローチを取り、私は両方が好きです。react-async-hookプラグインはフックから関数を返しますexecute



import { useAsyncCallback } from 'react-async-hook';

const AppButton = ({ onClick, children }) => {
  const asyncOnClick = useAsyncCallback(onClick);
  return (
    <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
      {asyncOnClick.loading ? '...' : children}
    </button>
  );
};

const CreateTodoButton = () => (
  <AppButton
    onClick={async () => {
      await createTodoAPI('new todo text');
    }}
  >
    Create Todo
  </AppButton>
);


フックuseAsyncCallback呼び出すと、予想される負荷、エラー、結果のプロパティを持つオブジェクトとexecute、イベントハンドラーから呼び出すことができる関数が返されます



React-hooks-asyncは、同様のアプローチのプラグインです。関数を使用しますuseAsyncTask



簡略化したバージョンの完全な例を次に示しuseAsyncTaskます。



const createTask = (func, forceUpdateRef) => {
  const task = {
    start: async (...args) => {
      task.loading = true;
      task.result = null;
      forceUpdateRef.current(func);
      try {
        task.result = await func(...args);
      } catch (e) {
        task.error = e;
      }
      task.loading = false;
      forceUpdateRef.current(func);
    },
    loading: false,
    result: null,
    error: undefined
  };
  return task;
};

export const useAsyncTask = (func) => {
  const forceUpdate = useForceUpdate();
  const forceUpdateRef = useRef(forceUpdate);
  const task = useMemo(() => createTask(func, forceUpdateRef), [func]);

  useEffect(() => {
    forceUpdateRef.current = f => {
      if (f === func) {
        forceUpdate({});
      }
    };
    const cleanup = () => {
      forceUpdateRef.current = () => null;
    };
    return cleanup;
  }, [func, forceUpdate]);

  return useMemo(
    () => ({
      start: task.start,
      loading: task.loading,
      error: task.error,
      result: task.result
    }),
    [task.start, task.loading, task.error, task.result]
  );
};


createTask関数は、次の形式でタスクオブジェクトを返します。



interface Task {
  start: (...args: any[]) => Promise<void>;
  loading: boolean;
  result: null;
  error: undefined;
}


ジョブは状態がありそして我々が期待します、。ただし、この関数は、start後で呼び出すことができる関数も返します関数で作成されたジョブcreateTaskは更新に影響しません。更新は、関数forceUpdateおよびforceUpdateRefトリガーされますuseAsyncTask



これでstart、必ずしも機能コンポーネントの最初からではなく、イベントハンドラーまたは別のコードから呼び出すことができる関数ができました。



しかし、機能コンポーネントの最初の実行時にフックを呼び出す機能が失われました。react-hooks-asyncプラグインに関数が含まれているのは良いことですuseAsyncRun-これは物事を簡単にします:



export const useAsyncRun = (
  asyncTask: ReturnType<typeof useAsyncTask>,
  ...args: any[]
) => {
  const { start } = asyncTask;
  useEffect(() => {
    start(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asyncTask.start, ...args]);
  useEffect(() => {
    const cleanup = () => {
      //   
    };
    return cleanup;
  });
};


この関数startは、引数のいずれかが変更されるたびに実行されますargsこれで、フックのあるコードは次のようになります。



const App: React.FC = () => {
  const asyncTask = useFetch(initialPage);
  useAsyncRun(asyncTask);

  const { start, loading, result: users } = asyncTask;

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {(users || []).map((u: User) => (
        <div>{u.name}</div>
      ))}

      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li key={n}>
            <button onClick={() => start({ skip: 10 * n, take: 10 })}>
              {n + 1}
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};


フックの規則に従ってuseFetch、機能コンポーネントの先頭にフックを使用します。この関数useAsyncRunは最初にAPIを呼び出し、ページネーションボタンのstartハンドラーonClick関数を使用します。



これで、フックuseFetchを本来の目的に使用できますが、残念ながら、間違った方向に移動する必要があります。また、クロージャーも使用していますが、これは認めざるを得ませんが、少し怖いです。



アプリケーションプログラムのフックの制御



アプリケーションプログラムでは、すべてが意図したとおりに機能する必要があります。コンポーネント関連の問題と特定のコンポーネントとのユーザーの操作を追跡する場合は、LogRocketを使用できます







LogRocketは、サイトで発生するほとんどすべてを記録する一種のWebアプリケーションビデオレコーダーです。 React用のLogRocketプラグインを使用すると、ユーザーがアプリケーションの特定のコンポーネントをクリックしたユーザーセッションを見つけることができます。ユーザーがコンポーネントを操作する方法と、一部のコンポーネントが何もレンダリングしない理由を理解できます。



LogRocketは、Reduxストアからのすべてのアクションと状態を記録します。これは、ヘッダーと本文を使用して要求/応答を記録できるようにする、アプリケーション用のツールのセットです。彼らはページにHTMLとCSSを記述し、最も複雑な単一ページのアプリケーションでもピクセルごとのレンダリングを提供します。



LogRocketは、Reactアプリケーションをデバッグするための最新のアプローチを提供します無料でお試しください



結論



cの例useFetchは、私がフックに不満を感じている理由を最もよく説明していると思います



期待した結果を得るのは思ったほど簡単ではありませんでしたが、フックを特定の順序で使用することがなぜそれほど重要なのかは理解できます。残念ながら、フックは機能コンポーネントの最初にしか呼び出せないため、機能が大幅に制限されており、回避策をさらに探す必要があります。解決策はuseFetchかなり複雑です。また、フックを使用する場合、クロージャーなしでは実行できません。閉鎖は私の魂に多くの傷を残した継続的な驚きです。



閉鎖(に渡されたものuseEffectuseCallback)古いバージョンの小道具と状態値を取得できます。これは、たとえば、キャプチャされた変数の1つが何らかの理由で入力配列にない場合に発生し、問題が発生する可能性があります。



クロージャーでコードを実行した後に発生する廃止された状態は、フックリンターが解決するように設計されている問題の1つです。 Stack Overflowには、廃止されたフックなどについて多くの質問がありますこのようにuseEffect関数をラップしuseCallbackて依存関係配列をねじり、古い状態やレンダリングの問題の無限の繰り返しを取り除きました。それ以外のことはできませんが、少し面倒です。これはあなたが自分の価値を証明するために解決しなければならない本当の問題です。



この投稿の冒頭で、私は一般的にフックが好きだと言いました。しかし、それらは非常に複雑に見えます。現在のJavaScriptランドスケープには、これに似たものはありません。機能コンポーネントがレンダリングされるたびにフックを呼び出すと、ミックスインでは発生しない問題が発生します。このパターンを使用するためのリンターの必要性はあまり信頼できず、閉鎖が問題になります。



このアプローチを誤解しただけだといいのですが。もしそうなら、コメントにそれについて書いてください。





続きを読む:






All Articles