LoginSignup
81

More than 3 years have passed since last update.

React, Redux 初心者が、Hooks 時代の React, Redux, React-Redux に触れてみて感じたこと

Last updated at Posted at 2020-02-21

筆者は、 Reactも Reduxも React-Reduxも初心者で、プライベートでちょっと触っている程度のペーペーです。

React に Hooks が導入されてしばらく経ちますが、React-Reduxにも Hooks が導入されていることを最近知りました。ちょっと試して見たところ、Hooksを使うことで少しだけモヤモヤが晴れてきたので、これまで理解したことを整理したいと思います。

useState, useReducer は(規模の大きな開発には)使わない

useState, useReducer はとっつきやすくてわかりやすいです。なので、ちょっとしたプログラムでは使うと思います。便利です。YoutubeのReact Today and Tomorrow and 90% Cleaner React With Hooks で、初めてこれを知った時は、感動すら、覚えました。

ですが、今回、自分でちょっと触ってみて、複数のコンポーネントにまたがるような情報を管理する必要が出てくると面倒そうだと感じました。
また、 テストも面倒なんじゃないかなと思ったりしてます。

以下が、毎度お馴染みの、 useState を使った実装例です。

src/ComponentUseState.js
import React, { useState } from "react";

const ComponentUseState = () => {
  const [num, setNum] = useState(0);

  return (
    <div>
      <h2>Using useState</h2>
      Number: {num}
      <button onClick={() => setNum(num + 1)}>+</button>
      <button onClick={() => setNum(num - 1)}>-</button>
    </div>
  );
};

export default ComponentUseState;

シンプルに書けるのですが、足し算、引き算の処理(ロジック)が、 コンポーネントの中に含まれている状態で、ちょっとテストが面倒そうです。
また、別のコンポーネントから、 num を参照したり、変更したりするのが面倒そうです。

useReducer を使うとこんな感じになります。

src/ComponentUseReducer.js
import React, { useReducer } from 'react'

const initialState = {num: 0};

const reducer = (state, action) => {
  switch(action.type) {
    case 'decrement':
      return {...state, num: state.num - 1}
    case 'increment':
      return {...state, num: state.num + 1}
    default:
      return state;
  }
}

const ComponentUseReducer = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  const { num } = state
  return (
    <div>
      <h2>Using useReducer</h2>
      Number: {num}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </div>
  );
};

export default ComponentUseReducer;

useState を使うより、コードが長くなってしまいますが、 useReducer を使うことによって、 足し算、引き算のロジックの部分は、reducer 関数の中に閉じ込めて、コンポーネントの ComponentUseReducer とは、分離することができます。
reducer は、純粋な JavaScript のコードなので、単体で実行することができ、テストもしやすそうです。(本来、reducer は、コンポーネントとは別ファイルに分割すべきです。ここでは、敢えて分割しておりません。)

const reducer = (state, action) => {
  switch(action.type) {
    case 'decrement':
      return {...state, num: state.num - 1};
    case 'increment':
      return {...state, num: state.num + 1};
    default:
      return state;
  }
}

例えば、以下のようにChrome のデベロッパーツールの console でも単独で実行できます。

image.png

コードの中で、初期状態を示しているのが initialState です。この状態をボタンが押されるなどのイベントによってどう変化させていくのかというロジックの部分が reducer になります。ですから、この reducer の部分と initialState をどうするかは実際の開発では自分で考えて実装しないといけないところですね。(私はこのことに気づく(理解する)のに、随分と、時間がかかりました。)

ただ、 reducer 関数の書き方には、決まり事があります。

  1. 引数は、state と action の2つ。
  2. action は、 type で、state をどう変更させるかを指定する。
  3. 戻り値は、変更後の新しい state にする。

ということで、コンポーネントとロジックを分けることができたのですが、やっぱり、 num を複数コンポーネントで共有するのはちょっと面倒そうです。

React-Redux の useSelector と useDispatch を使う。 connect は使わない。

そこで、 Redux と Hooks 時代の React-Redux です。
Redux と React-Redux を使うとこんな感じになります。

src/ComponentUseReactRedux.js
import React from "react";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";

const initialState = { num: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case "decrement":
      return { ...state, num: state.num - 1 };
    case "increment":
      return { ...state, num: state.num + 1 };
    default:
      return state;
  }
};

const store = createStore(reducer, initialState);

const ComponentUseReactRedux = () => {
  return (
    <div>
      <h2>ComponentUseReactRedux</h2>
      <Provider store={store}>
        <ChildComponentUseReactRedux />
      </Provider>
    </div>
  )
}

const ChildComponentUseReactRedux = () => {
  const num = useSelector(state => state.num);
  const dispatch = useDispatch();
  return (
    <div>
      <h3>Using useSelector, useDispatch</h3>
      Number: {num}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
};

export default ComponentUseReactRedux;

随分と長くなってしまいました :sweat_smile:

initialStatereducer は、 useReducer を使った例 src/ComponentUseReducer.js と同じです。初期値とロジックなので、ここは変わりません。

const store = createStore(reducer, initialState);

で、情報の格納先を1つ作ります。イメージ的には、これを複数のコンポーネントから参照する感じです。これはお約束の1つです。

ComponentUseReactRedux では、 React-Redux で提供される Provider を使って、 以下のように ComponentUseReactRedux の子コンポーネントからも、 store の情報にアクセスできるようにします。これもお約束ごとです。

      <Provider store={store}>
        <ChildComponentUseReactRedux />
      </Provider>

子コンポーネントの ChildComponentUseReactRedux の中では、 useSelector を使って num の値を参照できるようにします。

  const num = useSelector(state => state.num);

ここで、登場する、 state は、 reducer 関数の引数の state と同じものです。以下のように書き換えた方がわかりやすいかも知れません。

const selector = state => {
  return state.num;
}

const num = useSelector(selector);

また、 useDispatch() を使って、 dispatch を取得します。

  const dispatch = useDispatch()

ChildComponentUseReactRedux の残りの部分は、 useReducer を使った場合と同じですね。

で、この書き方だと、複数のコンポーネントから、同じ num を参照できるのです。

試しに、 ComponentUseReactRedux の中の ChildComponentUseReactRedux を2つにしてみます。

const ComponentUseReactRedux = () => {
  return (
    <div>
      <h2>ComponentUseReactRedux</h2>
      <Provider store={store}>
        <ChildComponentUseReactRedux />
        <ChildComponentUseReactRedux />
      </Provider>
    </div>
  )
}

するとその2つの子コンポーネントの num の値は連動して変化します。

gif1.gif

まとめ

  1. useStateuseReducer は 規模の大きな開発には使わない。
  2. 初期ステータスをどう定義するか、また、 reducer によってその初期ステータスをどう変化させるか(ロジック)は自分で考えて実装しないといけない。
  3. createStore を使ってステータスの格納先を作成する。
  4. Provider を使ってコンポーネント間で、ステータスを共有。
  5. useSelector を使って、ステータスのどの値を参照するのか指定する。
  6. useDispatch を使って、 dispatch を取得する。
  7. connect は使わない。 (mapStateToProps とか mapDispatchToProps とか私には難しい...。)

まだ整理できていないこと

  • useContext を使う方法もありそうなのですが、良く調べてません。
  • redux-thunk や redux-saga などの middleware の部分をどうするのが良いのか、まだ自分の中で整理ができていません。 (useCallback が使えそうですが...)

追記

今回は、 dispatch の引数(Action)を素のまま書きましたが、この部分を関数化するのが、より正しいお作法なのかも知れません。

const increment = () => {
  return {
    type: 'increment'
  };
}
const decrement = () => {
  return {
    type: 'decrement'
  };
}

を定義して、 dispatchを使うときに、 dispatch(increment())dispatch(decrement()) のようにします。

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
81