フックはどのように役立ちますか?
何が、なぜがっかりしたのかをお話しする前に、私は実際にはフックのファンであることを公式に宣言したいと思います。
クラスのコンポーネントを置き換えるためにフックが作成されるとよく耳にします。残念ながら、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>;
};
上のライン14、
useFetch
新しいオブジェクトが渡される関数に、我々はそれはとても同じオブジェクトは毎回使用されていることを確認しない限り、各レンダリングのために。このシナリオでは、オブジェクトへの参照ではなく、オブジェクトのフィールドをチェックします。
Reactがこのソリューションのような深いオブジェクト比較を行わない理由を理解しています。したがって、フックを慎重に使用する必要があります。そうしないと、アプリケーションのパフォーマンスに重大な問題が発生する可能性があります。私はそれについて何ができるのか常に疑問に思っており、すでにいくつかのオプションを見つけています。より動的なオブジェクトの場合は、より多くの回避策を探す必要があります。エラーを自動的に修正するeslintプラグイン
がありますコード検証中に見つかりました。あらゆるテキストエディタに適しています。正直なところ、テストするために外部プラグインをインストールする必要があるこれらすべての新機能に悩まされています。use-deep-object-compareやuse-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
かなり複雑です。また、フックを使用する場合、クロージャーなしでは実行できません。閉鎖は私の魂に多くの傷を残した継続的な驚きです。
閉鎖(に渡されたもの
useEffect
やuseCallback
)古いバージョンの小道具と状態値を取得できます。これは、たとえば、キャプチャされた変数の1つが何らかの理由で入力配列にない場合に発生し、問題が発生する可能性があります。
クロージャーでコードを実行した後に発生する廃止された状態は、フックリンターが解決するように設計されている問題の1つです。 Stack Overflowには、廃止されたフックなどについて多くの質問がありますこのように
useEffect
関数をラップしuseCallback
て依存関係配列をねじり、古い状態やレンダリングの問題の無限の繰り返しを取り除きました。それ以外のことはできませんが、少し面倒です。これはあなたが自分の価値を証明するために解決しなければならない本当の問題です。
この投稿の冒頭で、私は一般的にフックが好きだと言いました。しかし、それらは非常に複雑に見えます。現在のJavaScriptランドスケープには、これに似たものはありません。機能コンポーネントがレンダリングされるたびにフックを呼び出すと、ミックスインでは発生しない問題が発生します。このパターンを使用するためのリンターの必要性はあまり信頼できず、閉鎖が問題になります。
このアプローチを誤解しただけだといいのですが。もしそうなら、コメントにそれについて書いてください。
続きを読む: