LoginSignup
1
1

More than 3 years have passed since last update.

react-reduxでHooksを使うには

Last updated at Posted at 2020-09-20

はじめに

前回 の続きです。

セットアップ

下記のようにDOMをrenderする部分(コンポーネントのツリーの最上位に位置するコンポーネント)をProviderタグで包み込んでstoreをコンポーネント全体で使えるようにします。


const store = createStore(rootReducer)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

react-reduxを使う前にuseCallbackを理解する

useCallbackってなに?

すごく簡単に言うとコールバック関数を返すメソッド。


// ドキュメントより

const memoizedCallback = useCallback(
    () => {
        doSomething(a, b);
    },
    [a,b],
);

初回のrender時にはdoSomething()が実行される、以後は第2引数で設定したa,bのいずれかが変化した場合のみ新たにdoSomething()を実行し、memoizedCallback に代入する。

ただし、再renderされた時にa,bのいずれにも変化がなかった場合はdoSomething()は新しく実行されずに前に実行された結果が代入されたmemoizedCallback が返される。

これはアロー式で関数を定義するとrenderごとに毎回定義した関数を実行してしまい(=厳密に言うと関数のオブジェクトを作る)、結果不必要なrenderを行うことを避けるための処置になります。

React HooksのuseCallbackを正しく理解する ## useCallbackとは何か より

useCallbackがやることは、「コールバック関数の不変値化」です。
「関数を決定する処理のメモ化」と言えるかもしれません。アロー式は原理的に常に新規関数オブジェクトを作ってしまいますが、useCallbackは「意味的に同じ関数」が返るかどうかを判別して、同じ値を返す関数が返る>>べきなら新規のアロー式を捨てて、前に渡した同じ結果を返す関数を使い回しで返します。

では、具体的にどういうところで使うのかというと例えば以下のような例があります。


import React, { useState } from 'react';
import Form from 'react-bootstrap/Form';

function Example() {
    const[text, setText] = useState("");
    const[email, setEmail] = useState("");

    return(
        <Form.Group controlID="exampleForm.ControlInput1">
            <Form.Label>Example Input</Form.Label>
            <Form.Control type="email" rows={3}  placeholder="email" onChange={(e) => setEmail(e.target.value)}/>
        </Form.Group>
        <Form.Group controlID="exampleForm.ControlTextarea1">
            <Form.Label>Example textarea</Form.Label>
            <Form.Control as="textarea" rows={3}  onChange={(e) => setText(e.target.value)}/>
        </Form.Group>
    )
}


Input、及びTextareaの入力にuseStateを使い、onChangeのイベントハンドラでアロー関数を使っていますが、前述の通りアロー関数はrenderごとに毎回定義した関数を生成して実行するので、
例えば上記の例だとInputまたはTextareaでpropsに変更がある度に、renderが始まってどちらも再描画されてしまいます。
これを防ぐために以下のようにします。


import React, { useState } from 'react';
import Form from 'react-bootstrap/Form';

function Example() {
    const[text, setText] = useState("");
    const[email, setEmail] = useState("");

    function ExampleuseCallback1() {
        const[text, setText] = useState("");
        const Textarea_onChange = useCallback((e) => setText(e.target.value), [setText]);
    }

    function ExampleuseCallback2() {
        const[email, setEmail] = useState("");
        const Email_onChange = useCallback((e) => setEmail(e.target.value), [setEmail]);
    }

    return(
        <Form.Group controlID="exampleForm.ControlInput1">
            <Form.Label>Example Input</Form.Label>
            <Form.Control type="email"  placeholder="email" onChange={Email_onChange}/>
        </Form.Group>
        <Form.Group controlID="exampleForm.ControlTextarea1">
            <Form.Label>Example textarea</Form.Label>
            <Form.Control as="textarea" rows={3}  onChange={Textarea_onChange}/>
        </Form.Group>
    )
}


useCallbackでイベントハンドラをラップすることでExample Inputに変更があった場合はそちらはrender時に再描画されますが、Example textareaは直前のrenderの値がパスされるだけで再描画はされません。(逆もしかり)

こういうことをcallback関数をメモ化するというそうです。

useSelector

useSelector()は引数にグローバルストアを指定し、必要なステートをプロパティとして取り出します。アクションがディスパッチされるとステートが更新されていた場合のみ、コンポーネントを再レン​​ダリングします。

ReduxではstateがStoreと呼ばれるものに集約されるのでそこから必要なstateを取り出すためのメソッドということになります。
例を見てみると


import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

export const CounterComponent = () => {
    // storeにあるstateのうち、sate.counterを呼び出して渡す
    const counter = useSelector(state => state.counter)
    return
        <div>{counter}</div>
}

export const TodoListItem = props => {
        // storeにあるstateのうち、state.todosのうちpropsで渡されたidのものを呼び出して渡す
    const todo = useSelector(state => state.todos[props.id])
    return
        // todoプロパティの中からtextの値を抽出
        <div>{todo.text}</div>
}

またメモ化した例だと以下のようになる。


import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

// createSelectorでstoreから必要なstateを呼び出す処理をメモ化

const selectNumOfDoneTodos = createSelector(
    state => state.todos,
    todos => todo.filter(todo => todo.isDone).length
)

export const DoneTodosCounter = () => {
    // メモ化しSelectorをuseSelectorに代入。これでstate、todosプロパティに変更がない場合は初回以降の再renderはない。
    const NumOfDoneTodo = useSelector(selectNumOfDoneTodos)
    return
        <div>{NumOfDoneTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>
  )
}


また少し複雑だが、単一のコンポーネント、インスタンスでしか使用されないSelectorがコンポーネントのpropsに依存する場合の書き方は以下の通りである。


import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';


// selectorの設定
const selectNumOfTodosWithIsDoneValue = createSelector(
    state => state.todos,
    (_, isDone) => isDone,
    (todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)

export const TodoCounterForIsDoneValue = ({ isDone }) => {
    // ここでグローバルステートから引っ張ってくるstateを現在のstate,isDoneを引数にしたselectNumOfTodosWithIsDoneValueメソッドで返す
    const NumOfTodosWithIsDoneValue = useSelector(state =>
        selectNumOfTodosWithIsDoneValue(state, isDone)
    )

    return
        <div>{NumOfTodosWithIsDoneValue}</div>
}

export const App = () => {
    return (
        <>
            <span>Number of done Todos:</span>
            <TodoCounterForIsDoneValues isDone={true}/>
        <>
    )
}


しかし、今度はSelectorがコンポーネントのpropsに依存するが複数のコンポーネント、インスタンスで使用される場合はどうなるかというと以下の通りである。


import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

const makeNumOfTodosWithIsDoneSelector = () => createSelector(
    state => state.todos,
    (_, isDone) => isDone,
    (todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)

export const TodoCounterForIsDoneValue = ({ isDone }) => {
    // makeNumOfTodosWithIsDoneSelectorの実行結果をuseMemoでメモ化する
    const selectNumOfTodoWithIsDone = useMemo(
        makeNumOfTodoWithIsDoneSelector, []
    )

    const numOfTodosWithIsDoneValue = useSelector(state =>
        selectNumOfTodosWithIsDone(state, isDone)
    )

    return
    <div>{numOfTodosWithIsDoneValue}</div>
}

export const App = () => {
        return (
            <>
                <span>Number of done todos:</span>
                <TodoCounterForIsDoneValue isDone={true} />
            </>
        )
}

useDispatch

useDispatchはRedux ストアからディスパッチ関数への参照を返し、必要に応じてアクションをディスパッチするために使うことができます。

子コンポーネントにこれを利用してコールバック関数を渡す場合はやはり前述の例に習ってuseCallbackでメモ化するのがよいそうです。
では、実例を見てみます。


import React from 'react';
import { useDispatch } from 'react-redux';

export const CounterComponent = ({ value }) => {
    // storeに紐付く、dispatchを取得
    const dispatch = useDispatch()

    return (
        <div>
            <span>{value}</span>
            <button onClick={() => dispatch({ typeL 'increment-counter' })}>
                Increment counter
            </button>
        </div>
    )
}


これをuseCallbackを使ってリファクタリングすると以下の通りになる。


import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';

export const CounterComponent = ({ value }) => {
    const dispatch = useDispatch()
    // dispatchをuseCallbackでラップする
    const incrementCounter = useCallback(
        () => dispatch({ type: 'increment-counter '}),[dispatch]
    )
}

// React.memoでReactでコンポーネントをメモ化する
// これにより、メモ化されたコンポーネントが返した要素を再render時に比較して、必要な場合のみrenderを行う
// 実際はincrementCounterと逆の処理を行うdecrementCounterコンポーネントを用意し、どちらかの更新があった際に変更のないボタンを再renderしない……といったようなことをするための処置

export const MyIncrementButton = React.memo(({ onIncrement }) => (
  <button onClick={onIncrement}>Increment counter</button>
))

return (
    <div>
      <span>{value}</span>
      <!-- MyIncrementButtonincrementCounterを渡してボタンをrenderする -->
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  )
}



ポイントは

  • useCallbackでdispatchをラップしていること(特に、incrementCounterはイベントハンドラ用途なので)

  • React.memoでIncrement counterボタンを作るコンポーネントをラップすることによってボタンの不必要な再renderを防いでいること

の2点です。

ここでドキュメントの例ではイマイチ腑に落ちない……かもしれないので、参考先のページであるベストな手法は? Reactのステート管理方法まとめ さんから以下のコードを引用させていただいてuseSelectorとuseDispatchについて実例を踏まえて理解を深めていきます。



import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';


const add_todo = 'add_todo';
const completed_task = 'completed_task';

// action
export const addTodo = payload => ({
    type: add_todo,
    payload
});

export const completedTask = payload => ({
    type: completed_task,
    payload
});


// reducer
const initialState = { todos: [] };

export const todoReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'add_Todo':
            const newTodos = [...state.todos];
            newTodos.push({ id: state.todos.length + 1, task: action.payload });
            return {
                ...state,
                todos: newTodos
            };
        case 'complete_task':
            const filteredTodos = [...state.todos];
            filteredTodos.splice(action.payload, 1);
            return {
                ...state,
                todos: filteredTodos
            };
        default:
            return state;
    }
};

// Redux

export const Todo = () => {
    // テキストインプット用のローカルステート
    // これはフォームの入力部分の管理に使う(=このコンポーネントでのみの利用でよい)のでuseStateでローカルに管理するほうが楽
    const [input, updateInput] = useState("");

    // useSelector,useDispatch
    const dispatch = useDispatch();
    const { todos } = useSelector(state => state.todo);

    // テキストインプットを監視するHooks
    const onChangeInput = useCallback(
        event => {
            updateInput(event.target.value);
        },
        [updateInput]
    );

    // チェックボックスのイベント更新を監視するHooks
    const onCheckListItem = useCallback(
        event => {
            dispatch({ type: 'complete_task', payload: event.target.name });
        },
        [dispatch]
    );

    // ローカルステートとDispatch関数を監視するHooks
    const addTodo = useCallback(() => {
        dispatch({ type: 'add_todo', payload: input });
        updateInput("");
    }, [input, dispatch]);

    return (
        <>
            <input type="text" onChange={onChangeInput} value={input} />
            <button onClick={addTodo}>追加</button>
            <ul>
                {todos.map((todo, index) => (
                    <li key={todo.id}>
                        <input type="checkbox" onChange={onCheckListItem} name={index} />
                        {todo.task}
                    </li>
                ))}
            </ul>
        </>
    );
};


Todoアプリにおけるタスクの追加と既存のタスクの一覧にチェックボックスをつけて完了・未完了で区別する……という処理になります。
ポイントは以下の通りです。

  • ローカルステートとuseSelector・useDispatchでのステート管理を使い分けて併用している

  • ローカルステートとDispatch関数を同時にuseCallbackでラップしている

2番目の点については、タスクの追加には当然テキストインプットの部分が関わってくるのでそのローカルステートが変わったときにのみdispatchとupdateInputが実行されるのが適切だということですね。

useDispatchをuseCallbackする理由については先述の通りです。今回もonChange、onClickとイベントハンドラで使っているのがわかると思います。

useStore

こちらは冒頭で示した


const store = createStore(rootReducer)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

コンポーネントにReduxストアへの参照を渡すという処理がありますが、useStoreはこの処理で渡されたReduxストアへの参照を返します。

しかし、通常は上記のコードを実装した上で、useSelectorを使うので、ドキュメントによるとReducerの置き換えなどのようにどうしても特別にストアへのアクセスを必要とするような処理に使うのが推奨されているようです。

単純にstoreを取得するには以下のようなコードで実装できます。


import React from 'react'
import { useStore } from 'react-redux'

export const CounterComponent = ({ value }) => {
    const store = useStore()

    return
        <div>{store.getState()}</div>
}

ドキュメントではあくまでのuseStoreの処理の例として紹介されていて実装することは非推奨となっています。

なお、この状態ではstoreの状態が更新されてもコンポーネントは自動的に更新されることはありません。

また、余談としては


const store = createStore(rootReducer)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

こちらのコードではContextを使うことができるようです。
Contextは普段、コンポーネントがpropsでリレーしながらデータをやり取りしているのに対してContextに収容されたデータはpropsを介さず直接アクセスできるという特徴があります。

ドキュメントの例を見てみましょう。


import React from 'react'
import {
    Provider,
    createStoreHook,
    createDispatchHook,
    createSelectorHook
} from 'react-redux'


// 現在のContextを返す
const MyContext = React.createContext(null)

// MyContextを引数に各カスタムフックを定義する
export const useStore = createStoreHook(MyContext)
export const useDispatch = createDispatch(MyContext)
export const useSelector = createSelector(MyContext)

const myStore = createStore(rootReducer)

export function MyProvider({ children }) {
    return (
        <Provider context={MyContext} store={myStore}>
            <!-- 子コンポーネント  -->
            {children}
        </Provider>
    )
}

こうすることでContextがグローバルに定義されてpropsを介さずともuseStore、useDispatch、useSelectorにアクセスできるということになります。

補足

従来のconnectを使っていた部分をReduxでもHooksで代用できるようになったというのが今回のお話ですがそれ故の不具合もあるようで、react-reduxでHooksを使う場合はuseSelectorを使って以下のことに、留意するべきだとドキュメントには書いてあります。

Don't rely on props in your selector function for extracting data

セレクタ関数のpropsに頼らずにデータを抽出する

In cases where you do rely on props in your selector function and those props may change over time, or the data you're extracting may be based on items that can be deleted, try writing the selector functions defensively. Don't just reach straight into state.todos[props.id].name - read state.todos[props.id] first, and verify that it exists before trying to read todo.name.

セレクタ関数でpropsに依存している場合で、それらのpropsが時間の経過とともに変化する可能性がある場合や、抽出するデータが削除可能な項目に基づいている可能性がある場合は、セレクタ関数を防御的に記述してみてください。state.todos[props.id].nameに直接手を伸ばしてはいけません - state.todos[props.id]を最初に読み、todo.nameを読み込もうとする前に存在するかどうかを確認してください。

Because connect adds the necessary Subscription to the context provider and delays evaluating child subscriptions until the connected component has re-rendered, putting a connected component in the component tree just above the component using useSelector will prevent these issues as long as the connected component gets re-rendered due to the same store update as the hooks component.

connect は必要な Subscription をコンテキストプロバイダに追加し、接続されたコンポーネントが再レンダリングされるまで子サブスクリプションの評価を遅らせるので、useSelector を使用して接続されたコンポーネントをコンポーネントのすぐ上のコンポーネントツリーに置くことで、接続されたコンポーネントが hooks コンポーネントと同じストア更新によって再レンダリングされる限り、これらの問題を防ぐことができます。

As mentioned earlier, by default useSelector() will do a reference equality comparison of the selected value when running the selector function after an action is dispatched, and will only cause the component to re-render if the selected value changed. However, unlike connect(), useSelector() does not prevent the component from re-rendering due to its parent re-rendering, even if the component's props did not change.

前述したように、デフォルトでは useSelector() は、アクションがディスパッチされた後にセレクタ関数を実行する際に選択された値の参照し、値の変更がないかの比較を行い、選択された値が変更された場合にのみコンポーネントの再レンダリングを行います。ただし、connect() とは異なり、useSelector() は、コンポーネントのプロップが変更されていなくても、親の再レンダリングによってコンポーネントが再レンダリングされるのを防ぐことはできません。

If further performance optimizations are necessary, you may consider wrapping your function component in React.memo():

よって上記の問題の最適化が必要な場合は関数コンポーネントを React.memo() でラップすることを検討してください。

2番目に関しては今回のuseSelectorの項でやった以下のコードを見ればわかりますね。


import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';

export const CounterComponent = () => {
    // storeにあるstateのうち、sate.counterを呼び出して渡す
    const counter = useSelector(state => state.counter)
    return
        <div>{counter}</div>
}

export const TodoListItem = props => {
        // storeにあるstateのうち、state.todosのうちpropsで渡されたidのものを呼び出して渡す。
    const todo = useSelector(state => state.todos[props.id])
    return
        // todoプロパティの中からtextプロパティの値を抽出。必ずstate.todos[props.id]を読み込んたあとの実行にする。
        <div>{todo.text}</div>
}

最後の部分に関しても今回の例に以下のように出てきました。


import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';

export const CounterComponent = ({ value }) => {
    const dispatch = useDispatch()
    // dispatchをuseCallbackでラップする
    const incrementCounter = useCallback(
        () => dispatch({ type: 'increment-counter '}),[dispatch]
    )
}

// React.memoでReactでコンポーネントをメモ化する
// これにより、メモ化されたコンポーネントが返した要素を再render時に比較して、必要な場合のみrenderを行う
// 実際はincrementCounterと逆の処理を行うdecrementCounterコンポーネントを用意し、どちらかの更新があった際に変更のないボタンを再renderしない……といったようなことをするための処置

export const MyIncrementButton = React.memo(({ onIncrement }) => (
  <button onClick={onIncrement}>Increment counter</button>
))

return (
    <div>
      <span>{value}</span>
      <!-- MyIncrementButtonincrementCounterを渡してボタンをrenderする -->
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  )
}


この例にはuseSelectorは使われていませんが、実際には合わせて使うことが殆どなので気をつけておきましょうと言うことですね。

ドキュメントでは以下のように例が示されています。


const CounterComponent = ({ name }) => {
    const counter = useSelector(state => state.counter)
    return (
        <div>
            {name} : {counter}
        </div>
    )
}

export const MemoizedCounterComponent = React.memo(CounterComponent)

つまり、useSelectorを含むコンポーネントに関して内部に不必要に再renderされるのが望ましくない箇所がある場合はメモ化しましょうというわけですね。
上記の例だとreturn以下が更新がない限り再renderしたくない部分になります。

参考

React Redux Hooks
React Context
ベストな手法は? Reactのステート管理方法まとめ
React hooksを基礎から理解する (useContext編)
React hooksを基礎から理解する (useCallback編) と React.memo)
React HooksのuseCallbackを正しく理解する

1
1
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
1
1