5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React + Redux アプリのデータの流れを説明する

Posted at

この記事は

React + Redux を使ったアプリケーションのデータの流れについて解説しています。

誰に向けた記事か

React + Redux をまだ使用したことがない or 経験がまだ少ない人。

React + Redux アプリのデータの流れ

React のコンポーネント

コンポーネントのコードは以下のようになっています。

const ProductList = () => (
  <div>
    <h1>商品リスト</li>
    <ul>
      <li>Nintendo Switch</li>
      <li>PlayStation</li>
      <li>Xbox</li>
    </ul>
  </div>
);

イメージにするとこのような木構造になっているとお考えください。
image.png

state と props

コンポーネントで扱うデータには state と props があります。
state はコンポーネント内部に持っているデータで、props は親から子に渡されるデータです。
image.png

以下の例では title が state で、listTitle が props になります。

// これが親コンポーネント
const Product = () => {
  // これがstate
  const [title, setTitle] = useState("商品リスト");
  // HTMLの属性のようにして渡しているのがprops
  return <ProductList listTitle={title} />;
};

// これが子コンポーネント
const ProductList = (props) => (
  <div>
    <h1>{props.listTitle}</li>
    <ul>
      <li>Nintendo Switch</li>
      <li>PlayStation</li>
      <li>Xbox</li>
    </ul>
  </div>
);

props にはデータだけでなくコールバック関数を渡すことができます。
この関数をイベントハンドラとして onClick などに設定することで、親のコンポーネントに value を渡すことができます。
image.png

以下の例では子コンポーネントにコールバック関数を渡し、親の state を更新しています。

// これが親コンポーネント
const Product = () => {
  // これがstate、setTitleはtitleを変更するための関数
  const [title, setTitle] = useState("商品リスト");
  // HTMLの属性のようにして渡しているのがprops
  // handleClickのようにコールバック関数を渡すこともできる
  return <ProductList listTitle={title} handleClick={setTitle} />;
};

// これが子コンポーネント
const ProductList = (props) => (
  <div>
    <h1>{props.listTitle}</li>
    <ul>
      <li>Nintendo Switch</li>
      <li>PlayStation</li>
      <li>Xbox</li>
    </ul>
    <button onClick={() => props.handleClick("ゲーム機")}>タイトル変更</button>
  </div>
);

render とライフサイクル関数

props が更新されると、それを受け取ったコンポーネントのツリーが自動的に更新されます。
image.png

具体的にはコンポーネントの render 関数やライフサイクル関数が再実行されます。
ライフサイクルに関して、公式サイトからリンクされていた図があるので、ご参照ください。

横断的関心事

別々のコンポーネントで同じデータを使いたい場合があったとします。
image.png

その場合は、共通の親コンポーネントから props をリレーのように渡す必要があります。
image.png

しかしこれはあまりスマートではないですよね?

Redux を使うとこの問題に対処できます。
Redux では Store と呼ばれる共有データストアから、直接コンポーネントに props を渡すことができます。
image.png

Redux の仕組み

Store からデータを props に渡すために、container と呼ばれるコンポーネントでラップします。
container コンポーネントでは props のデータに Store の一部のデータ、コールバック関数に dispatch と呼ばれる関数をマッピングします。
image.png

connect という関数を使って Store のデータと dispatch をマッピングします。

// Storeのどのデータをpropsにマッピングするか
const mapStateToProps = (state) => ({
  isLogin: state.Auth.isLogin,
});

// LoginPageコンポーネントのpropsには、isLoginとdispatch関数が渡される
export default connect(mapStateToProps)(LoginPage);

Action

Store のデータを更新したい場合は、コンポーネントから dispatch 関数に対して Action と呼ばれる JavaScript オブジェクトを渡します。
image.png

以下のように、コンポーネントの onClick などから実行します。
ですが、この実装方法だと、type を間違えるなどのミスの原因になりそうですね。

const Button = (props) => (
  <button onClick={() => props.dispatch({ type: "login" })}>ログイン</button>
);

そのため、通常は ActionCreator と呼ばれる関数を定義して共用します。

// Action Type
export const LOGIN = "Auth/LOGIN";

// Action Creator
export function login() {
  return { type: LOGIN };
}
const Button = (props) => (
  <button onClick={() => props.dispatch(login())}>ログイン</button>
);

実際には container コンポーネントでマッピングして利用することが多いです。
connect 関数の第 2 引数に、マッピングする関数を指定します。

// Storeのどのデータをpropsにマッピングするか
const mapStateToProps = (state) => ({
  isLogin: state.Auth.isLogin,
});

// dispatchにどのActionCreatorをマッピングするか
const mapDispatchToProps = (dispatch) => ({
  login: bindActionCreators(login, dispatch),
  logout: bindActionCreators(logout, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);

こうすると、コンポーネントからはマッピングされた関数を実行するのみです。

const Button = (props) => (
  <button onClick={() => props.login()}>ログイン</button>
);

Reducer

Action オブジェクトが dispatch 関数に渡されると、Reducer という関数が実行されます。
この Reducer が実行されると、Store のデータが更新されます。
image.png

Reducer は現在の Store と Action を受け取り、次の Store を返す関数です。
Action の type によって、return する state が異なるのがわかるかと思います。

※スプレッド演算子(...state)を使っているのは、引数で渡された state をコピーするためです。
Redux は state の更新を検知するために、浅い比較(shallow equal)を行います。
state を直接変更すると更新を検知できず、コンポーネントが再レンダリングされなくなります。

// 初期State
const initState = {
  isLogin: false;
};

// Reducer関数
function reducer(state = initState, action) {
  switch (action.type) {
    case LOGIN:
      return { ...state, isLogin: true };
    case LOGOUT:
      return { ...state, isLogin: false };
    default:
      return state;
  }
}

Reducer が実行され Store が変更されると、Store の情報はマッピングされたコンポーネントに渡されます。

props が更新されると、コンポーネントのツリーが自動的に更新されるので、Store が更新されると View が自動的に更新されるような挙動になります。

Redux thunk

Store データの更新には dispatch 関数に Action オブジェクトを渡す、というのはわかったのですが、Action は type というキーが必須のただのオブジェクトです。
とてもシンプルですが、API 通信などの外部通信は、どこに記述するのが良いでしょうか?

// Action Type
export const LOGIN = "Auth/LOGIN";

// Action Creator
export function login() {
  return { type: LOGIN };
}

通常 Action Creator は純粋な Action オブジェクトしか返しません。
Redux thunk を使うと、Action Creator が関数を返せるようになります。

// Action Type
export const LOGIN = "Auth/LOGIN";

// Action Creator
export function login() {
  // dispatchを引数にとる関数を返す
  return async (dispatch) => {
    // API通信をawaitで待つ
    await fetch("/auth/login");
    // Actionをdispatch
    dispatch({ type: LOGIN });
  };
}

例えば API 通信の最中にアプリをローディング表示したい場合などは、以下のように通信前後で別々の Action を dispatch 関数に渡し、状態を変更します。

// Action Type
export const FETCHING = 'Auth/FETCHING';
export const LOGIN = 'Auth/LOGIN';

// 初期State
const initState = {
  isFetching: false;
  isLogin: false;
};

// Reducer関数
function reducer(state = initState, action) {
  switch (action.type) {
    case FETCHING:
      return { ...state, isFetching: true };
    case LOGIN:
      return { ...state, isFetching: false, isLogin: true };
    case LOGOUT:
      return { ...state, isLogin: false };
    default:
      return state;
  }
}
// Action Creator
export function login() {
  // dispatchを引数にとる関数を返す
  return async (dispatch) => {
    // API通信前に別のActionをdispatch
    dispatch({ type: FETCHING });
    // API通信をawaitで待つ
    await fetch('/auth/login');
    // Actionをdispatch
    dispatch({ type: LOGIN });
  };
}

これでコンポーネント側で API 通信中の表示を制御できます。

// ログインページのコンポーネント
const LoginPage = (props) => {
  // API通信中はローディングを表示
  if (props.isFetching) {
    return <Loading />;
  }
  return (
    <>
      <p>{props.isLogin ? "ログイン中" : "未ログイン"}</p>
      <Button />
    </>
  );
};

// ログインボタン
const Button = (props) => (
  <button onClick={() => props.login()}>ログイン</button>
);

最後に

以上、React + Redux アプリのデータの流れでした。
各ライブラリの挙動について、理解の助けになればと思います。

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?