豆腐とコンソメ

豆腐とコンソメ

もろもろのプログラム勉強記録

react-reduxを学ぶ

ReduxをReactで使うときは、react-reduxを使うんだよ!と講座で教わり、以降何も考えずにreact-reduxを使ってきた。
connect()の書き方がよくわかんねえよ!とか、Reduxの非同期処理がわかんないよ!とか、いろいろありつつも、今回はそもそもreact-reduxってなんで必要なんだっけ?みたいなところを改めて整理しようと思う。


react-reduxがある世界

まずはいつも通り、react-reduxを使ってTodoリストをつくってみる。

最低限の機能でこんなかんじに。

codesandbox.io


Todoリストは、Todoを追加するAddTodoとTodoの一覧を表示するTodoListのコンポーネントの二つのコンポーネントで構成されている。

index.js

const store = createStore(todoReducer);

const App = () => (
  <div>
    <AddTodo />
    <TodoList />
  </div>
);

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

いずれも、react-reduxを用いて、コンポーネント内で、Reduxのdispatchや、Reduxのstateを参照してる。

TodoList.js

import React from "react";
import { connect } from "react-redux";

const TodoList = ({ todos }) => (
  <ul>
    {todos.map((todo, index) => (
      <li key={index}>{todo}</li>
    ))}
  </ul>
);

export default connect(state => ({
  todos: state
}))(TodoList);


react-reduxがない世界

次に、react-reduxを使わないで、Reduxをつかってみる。


storeを渡す

createStoreでstoreをつくったところで、早速手が止まります。
あれ、コンポーネントからstoreをどうやって参照すればいいのかな、と。

index.js

// storeをつくったけど、コンポーネントからどうやって参照すればいいのかしら
const store = createStore(todoReducer);

const App = () => (
  <div>
    <AddTodo />
    <TodoList />
  </div>
);

ReactDOM.render(
    <App />
  document.getElementById("root")
);

react-reduxのコードを眺めて見ると、ReactのContextの機能をつかっていることがわかりました。

なので、Contextを使ってコンポーネントにstoreを渡すことにします。
Contextはほとんどつかったことがないのであんまりよくわかってないので説明は割愛します。

まずは、コンテキスト作成します。

MyContext.js

import React from "react";
export default React.createContext();

作成したコンテキストを読み込み、Providerで配下のコンポーネントに渡すようにします。

index.js(抜粋)

// つくったContextをImport
import MyContext from "./context/MyContext";


const store = createStore(todoReducer);

// Contextでstoreを渡す
ReactDOM.render(
  <MyContext.Provider value={store}>
    <App />
  </MyContext.Provider>,
  rootElement
);

そしたら、コンテキストを参照するコンポーネントから、コンテキスト経由でstoreを受け取ります。

index.js(抜粋)

// つくったContextをImport
import MyContext from "./context/MyContext";

class App extends React.Component {
  // MyContext.Consumerでstoreを受け取る
  render() {
    return (
      <div>
        <MyContext.Consumer>
          {store => (
            <div>
              <AddTodo store={store} />
              <TodoList store={store} />
            </div>
          )}
        </MyContext.Consumer>
      </div>
    );
  }
}

AddTodoTodoListコンポーネントでは、プロパティ経由でstoreを渡すので、store.dispatchが使えます。

AddTodo.js

import React from "react";
import { addTodo } from "../actions";

const AddTodo = ({ store }) => {
  let input;
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        store.dispatch(addTodo(input.value));
      }}
    >
      <input type="text" ref={node => (input = node)} />
      <button type="submit">Add Todo</button>
    </form>
  );
};

export default AddTodo;


TodoListのほうも、store.getState()でReduxのステートを取得することができますね。

TodoList.js

import React from "react";

const TodoList = ({ store }) => {
  const todos = store.getState();
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>{todo}</li>
      ))}
      <button onClick={() => debugState(store)}>Debug</button>
    </ul>
  );
};

export default TodoList;

ここまでのコードを参考までに貼っておきます。

codesandbox.io


が、このままではテキストボックスからTodoを追加しても、画面に表示されません。

というもの、Reactはstateかpropsの更新があった場合にコンポーネントが再レンダリングされます。
store.dispatchをすることで、ステートの状態がかわってはいるのですが、storeオブジェクトの中身がかわったかどうかをReactは検知することができません。
immutable原則ってやつですね。

なので、dispatchをしたら、Reactに再レンダリングをしてねという仕組みを実装する必要があります。

まずdispatchをしたら、という部分ですが、Reduxに標準で用意されているstore.subscribeを利用することができます。

redux.js.org

続いて、Reactに再レンダリングさせるために、propsやstateを用意して、更新させてもよさそうなのですが、ここでは明示的に再レンダリングさせるforceUpdateを使うことにします。  

index.js(抜粋)

// つくったContextをImport
import MyContext from "./context/MyContext";

class App extends React.Component {
  // MyContext.Consumerでstoreを受け取る
  render() {
    return (
      <div>
        <MyContext.Consumer>
          {store => {
            // dispatchされたら
            store.subscribe(() => {
              // Appコンポーネントを再レンダリングする
             // ※ Appコンポーネントが再レンリングされると、配下のコンポーネントも再レンダリングされる
              this.forceUpdate()
            })
            return (
              <div>
                <AddTodo store={store} />
                <TodoList store={store} />
              </div>
            )
          }}
        </MyContext.Consumer>

      </div>
    );
  }
}

これで、やりたいことができました。   

コンポーネントを受け取ってMyContext.Consumerの部分をつけて返すような関数を使うとreact-reduxconnectのようなイメージになりそうですね。

実際のreact-reduxでは、パフォーマンス等いろいろな点が考慮されているとのことなので、今回のような実装は行うことはないかと思いますが、すこしだけreact-reduxがやっていることをイメージできるようになりました。