始めに
『3 amazing REACT HOOKS to keep your code organized neatly』この記事を読んでいてうち一つは結構有益だなぁぁって思ったので紹介です!本編中のコードはここからの引用をちょっと修正したものになります。
react-useをご存知の方も改めてこうやって作れるなってコードベースが参考になったらなと思っています。
- あるある非同期処理
- useAsyncを使った場合
- useAsyncの作りかた
- react-use
といった形で紹介していきます。
As is:例えばあるあるの非同期処理
function MyComponent() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(() => {
(async () => {
setLoading(true)
try {
const result = await doSomeAction();
setResult(result);
} catch (e) {
setError(e.toString());
} finally {
setLoading(false);
}
})();
}, [])
if (loading) {
return <>loading...</>
}
if (error) {
return <>{error}</>
}
return <>{result}</>
}
といった形の処理です。
doSomeAction()という何らかの処理によって、非同期で動作します。そして、それは何らかの結果やエラーとして帰ってきます。
そこでこのようにuseStateを絡めながらコードを書いていくことがしばしばあるかなと思います。
とはいえ、これを何度か書いていくのは煩雑感があります・・・。そこで、react-apollo
の useQuery
みたいな形でかけたら幸せじゃないですか?
To be:useAsyncのある世界
function MyTidyComponent() {
const {loading, result, error} = useAsync(doSomeAction)
if (loading) {
return <>loading...</>
}
if (error) {
return <>something broke</>
}
return <>{result}</>
}
随分とシンプルになりました。
ではこれを作っていきましょう!というのが今日の本題。
useAsyncのつくりかた
function useAsync(asyncFunction, immediate = true) {
const [loading, setLoading] = useState(false)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
const mounted = useRef(true)
useEffect(() => {
return () => {
mounted.current = false
}
}, []);
const execute = useCallback(async (...args) => {
setLoading(true)
setResult(null)
setError(null)
try {
const r = await asyncFunction(...args)
if (mounted.current) {
setResult(r);
}
return r
} catch (e) {
if (mounted.current) {
setError(e);
}
} finally {
if (mounted.current) {
setLoading(false);
}
}
}, [asyncFunction])
useEffect(() => {
if (immediate) {
execute()
}
}, [execute, immediate]);
return { execute, loading, result, error };
}
ほとんどはAs is のコードをそのまま持ってきたのかのような形です。
immediateはおしゃれポイントみたいですね。コンポーネントがマウントされてもimmediateをfalseにしておけば実行されず、trueに変更した時に実行されます。
useEffect(() => {
if (immediate) {
execute()
}
}, [execute, immediate]);
useRefの記述や条件分岐はメモリリーク対策でこれが一度実行されてアンマウントされた場合に以下のコードが動かないようにしています。
useEffect(() => {
return () => {
mounted.current = false
}
}, []);
クリーンアップ関数でcurrentをfalseにしていますね。
といった形なんですがreac-use
ではこれよりももうちょっとだけ内部的に良いuseAsyncが提供されています。
じゃあなんで書き方を紹介したの?ってなるんですが、これは内部実装についてしるのが面白かったのと、簡単にかけるのでimmediatelyのようなカスタマイズを施せるので知っておいて損はないなと思ったからです。
react-use
import {useAsync} from 'react-use';
const Demo = ({url}) => {
const state = useAsync(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
</div>
);
};
おしゃれポイントはなくなっていますが、ほとんどおんなじです。どこら辺が少し高性能なのかでいくと、第二引数がdepsになっておりdepsが変更された時、非同期関数が作り直されます。また、mounted.currentのような判定に加えて呼び出し回数による制御もかかっています。
他にはuseIntervalやuseTimeoutなどの使い勝手もよいものがたくさん提供されているのでつかっていくのが良さそうですね。
まとめ
- 基本的にはreact-useを使った方が楽
- だけどカスタマイズを重ねていきたい時はこんな感じでuseAsyncみたいなものをつくることができる