sagantaf

メモレベルの技術記事を書くブログ。

React HooksのuseReducerとuseContextとカスタムフック

useReducer

useReducerの基本

Stateとdispatch(actionを送信する関数)を返すフック。

これを使うことでコンポーネント内でstate管理ができる。

書き方は以下。

const [state, dispatch] = useReducer(reducer, stateの初期値)

reducer にはstateの更新方法を記述する。

useReducerを生成して返ってくるdispatchはstateを更新ために利用する。dispatchにactionという更新内容を渡すことでstateを更新できる。

使い方は以下。

const action = {
    type: 'ADD_TODO',
    text: 'Learning React'
};

// reducerではactionをswitch文で分岐して書くことで可読性が上がる
function reducer(state, action) {
    switch(action.type) {
        case 'ADD_TODO':
            return [...state, [{ text: action.text, completed: false }]];
        default:
            return state;
    }
}

const [state, dispatch] = useReducter(reducer, {})
dispacth(action);

別の例。

// import省略
// 現在のstateとactionを受け取り、actionに応じて更新したstateを返す関数
function reducer(state, action){
    switch (action.type) {
        case 'INCRE':
            return {count: state.count + 1}
        case 'DECRE':
            return {count: state.count - 1}
        case 'RESET':
            return {count: 0}
        default:
            return state;
    }
}
export default App() {
    const [state, dispatch] = useReducter(reducer, { count:0 })
}

return(
    <>
        <p>count: {state.count}</p>
        <button onClick={dispatch(state, {type: 'INCRE'})}>+</button>
        <button onClick={dispatch(state, {type: 'DECRE'})}>-</button>
        <button onClick={dispatch(state, {type: 'RESET'})}>reset</button>
    </>
)

useReducerの使い所

複雑なstateを扱う時にはuseStateよりもuseReducerの方が良い。たとえば以下のような場合。

  • stateを更新するために他のstateを参照する
  • stateを更新したら別のstateも更新する必要がある
  • stateを更新するロジックが複雑である

useReducerを利用すると、

  • stateの更新ロジックをreducerに集約できる
  • stateの更新がdispatchの記述に統一される

というメリットがある。

また、dispatchは同一性が保たれるため、React.memoにdispatchを渡しても、親コンポーネントの再レンダーによる、子コンポーネントレンダリングを回避できる。useCallbackを使う必要はない。

useContext

useContextの基本

useContextはReactのContextと併用することで力をはっきする。

ReactのContextとは、Propsを利用せずに様々な階層のコンポーネントに値を共有できる機能。

共有したい値をContextオブジェクトとして生成し、Providerで使えるコンポーネント範囲を指定し、使いたいコンポーネントでuseContextを使って利用する、という流れ。Contextオブジェクトを利用するコンポーネントのことをConsumerと呼ぶ。

使い方は以下。

// import省略

// Contextオブジェクトの生成
const HogeContext = React.createContext();

function Child() {
    // コンポーネントでの利用
    const name = useContext(HogeContext);
    return <h1>Hello, {name}</h1>;
}

// Providerタグで各層のコンポーネントを囲むことで、囲まれたコンポーネントはContextを共有できるようになる
// 渡したいContextをvalueで渡す
export default function App() {
    const name = 'React'
    return (
        <HogeContext.Provider value={name}>
            <Child />
        </HogeContext.Provider>
    )
}

Contextのメリット

Contextを利用することで得られるメリットは

  • どのコンポーネントからもアクセスできるstateを作成できる
  • Prop drilling問題を解消できる(階層が深いとPropsを渡すのが煩わしくなる問題)

の2点。

たとえば、共通利用するデータをContextで一元化し、Contextの更新を全てのコンポーネントにもれなく反映させることができる(下記例)。

import React, { useState, useContext, createContext } from 'react';

const LangContext = createContext();

export default function App(){
    const [lang, setLang] = useState('JP')
    return (
        <>
            <button onClick={() => setLang('JP')}>日本語</button>
            <button onClick={() => setLang('EN')}>English</button>
            <LangContext.Provider value={lang}>
                <Header />
                <Body />
            </LangContext.Provider>
        <>
    );
}

function Header(){
    const lang = useContext(LangContext);
    const text = {
        JP: "Reactハンズオンラーニング",
        EN: "React Hands-on Learning"
    };
    return (
        <header>
            <h1>{text[lang]}</h1>
        </header>
    )
}

function Body() {
    const lang = useContext(LangContext);
    const text = {
        JP: "Reactの練習",
        EN: "React Training"
    };
    return <p>{text[lang]}</p>
}

Context利用時の注意

Contextオブジェクトを使うConsumerは、Providerのvalueが更新されるたびに再レンダーされてしまう。 これを防ぐ方法は、

  • Contextを分割する
  • React.memoを利用する
  • useMemoを利用する

の3パターンある。

  • Contextを分割する

Contextを細かく分割することで、関係あるContextの更新のみでConsumerが再レンダーされるようにする。

  • React.memoを利用する

useContextを直接利用したコンポーネントの再レンダーは避けられない。ただ、そのコンポーネントでさらにReact.memo化したコンポーネントを呼ぶ形で、再レンダーを少し減らすことはできる。

//import省略
const CountContext = createContext();

function countReducer(state, action) {
    switch (action.type) {
        case 'INCRE':
            return {count: state.count + 1}
        case 'DECRE':
            return {count: state.count - 1}
        case 'RESET':
            return {count: 0}
        default:
            return state;
    }
}

function CountProvider({ chileren }) {
    const [state, dispatch] = useReducer(countReducer, { count: 0 })
    const value = { state, dispacth }

    return (
        <CountContext.Provider value={value}>{children}</CountProvider>
    )
}

function Count() {
    const { state } = useContext(CountContext)
    return <h1>{state.count}</h1>
}

function Counter() {
    const {dispatch} = useContext(CountContext)
    return <DispatchButton dispatch={dispatch} />
}
// 上記コンポーネントでは DispatchButtonにdispatchを渡している
// valueの変更によるCounterの再レンダーは避けられないため、
// メモ化したDispatchButtonにdispatchを渡すことでDispatchButton自体の再レンダーを回避する対応を取っている

const DispatchButton = React.memo( ({ dispatch }) => {
    return (
        <>
            <button onClick={() => dispatch({ type: "DECRE" })>-</button>
            <button onClick={() => dispatch({ type: "INCRE" })>+</button>
        </>
    )
})

export default function App() {
    return (
        <CountProvider>
            <Count />
            <Counter />
        </CountProvider>
    )
}
  • useMemoを利用する

useContextを使ったコンポーネントのreturnでuseMemoを返す形にすることで、再レンダーを防ぐことができる。上記のReact.memoの例のCounterを下記のように変更することで実現される。

function Counter() {
    const { dispatch } = useContext(CountContext)
    return useMemo(() => {
        return (
            <>
                <button onClick={() => dispatch({ type: "DECRE" })>-</button>
                <button onClick={() => dispatch({ type: "INCRE" })>+</button>   
            </>
        )
    }, [dispatch])
}

つまり、Counterのレンダリング結果(return結果)をメモ化することで、dispatchの変更以外の要因でCounterが再レンダーされたとしてもreturn内容は再レンダーされないようになる。

Contextのデメリットと利用事例

デメリットは以下。

  • コンポーネントがContextに依存するため、再利用性が低下する
  • グローバルなstateなので、値がどこで更新されたり利用されたりしているのかが分かりづらくなる

上記デメリットを許容してでもContextを使いたい事例

  • 認証情報:ログインしているかどうかで要素の出し分けをするために利用
  • テーマ:選択しているテーマに応じて、コンポーネントに適用するスタイルを制御
  • 言語:選択している言語に応じて、表示する言語を切り替えるために利用する

カスタムフック

カスタムフックの基本

自作のフックのことをカスタムフックと呼ぶ。

カスタムフックのメリットは、

  • 独自のロジックをカスタムフックとして切り出すことで再利用ができる

→複数のフックを組み合わせた内容を再利用できるため便利。

  • 複雑なロジックをカスタムフックとして切り出すことで可読性が上がる

事例1

import React, { useState } from 'react';

// stateとstate更新ロジックを持つカスタムフックを定義
function useCounter(initialCount) {
    const [count, setCount] = useState(initialCount)
    const increment = () => {
        setCount(count + 1)
    }
    const decrement = () => {
        setCount(count -1)
    }
    return { count, increment, decrement }
}

function App() {
    const {count, increment, decrement} = useCounter(0);
    return (
        <div>
            <p>count: {count}</p>
            <button onClick={decrement}>-</button>
            <button onClick={increment}>+</button>
        </div>
    )
}

export default App;

事例2

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

カスタムフックの名前は必ずuseから始める必要がある。useを使うことで、linterプラグインが動作してバグ検知してくれる。

公開されているカスタムフックもあるので、便利なものがあるかもしれない。 GitHub - streamich/react-use: React Hooks — 👍 Collection of React Hooks useHooks - Easy to understand React Hook recipes

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon