LoginSignup
13
10

More than 3 years have passed since last update.

【React Hooks】非同期処理をいい感じに扱うuseAsyncを自分でつくる。

Posted at

始めに

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-apollouseQuery みたいな形でかけたら幸せじゃないですか?

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

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みたいなものをつくることができる
13
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10