Redux App Structure~最もシンプルなReduxアプリケーションで構造を理解しよう

Redux App Structure

どうも、ぽっぽです。

Reduxを試す一番の近道は、公式のテンプレートを使うことです。これで面倒な環境構築をコマンド1つで済ますことが出来ます。

npx create-react-app redux-essentials-example --template redux

カウンターアプリを使ってみよう

テンプレートを使ってセットアップされるのはシンプルなカウターアプリです。操作するごとにカウンターをアップしたりダウンしたりして表示するだけです。

Reduxが管理しているのはカウンターです。最もシンプルなアプリケーションを使ってReduxでどうやってカウンターを管理しているかが分かるようになっています。

Reduxの開発をするなら入れておきたいのがブラウザーの拡張機能としてインストールするDevToolsです。

これはstateの状態を可視化出来ます。アプリケーションを操作した履歴を管理し、一つ前の操作に戻った状態に戻すことなどが出来ます。デバッグで活躍するツールです。

アプリケーション構造

アプリケーションのソースは以下のような構造です。

  • /src
    • index.js:アプリのスタートポイント
    • App.js:トップレベルのReact component
    • /app
      • store.js:Reduxストアインスタンスの作成
    • /features
      • /counter
        • Counter.js:カウンター用のUIを表示するReact component
        • counterSlice.js:カウンター用のRedux logic

Redux Storeの作成

app/store.jsを開いてみましょう。

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})

Redux storeはRedux ToolkitのconfigureStoreを使います。configureStoreは引数にreducerを渡します。

アプリケーションはたくさんの異なる仕様から出来ています。それぞれの仕様に対してreducer関数があります。

{counter: counterReducr}のようなオブジェクトを引数に渡すことは、state.counterがRedux stateオブジェクトの一部で、counterReducer関数がstate.counterをどうやって更新するか管理することを表します。

Slice ReducersとActions

counterReduce関数は、features/counter/counterSlice.jsにあります。

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

reducersの中で定義された、increment,decrement,incrementByAmountによってstateが更新されます。createSliceを使うことでこのようなシンプルな表現が可能になります。これでaction creater functionが意識せずに定義が終わっています。

console.log(counterSlice.actions.increment())
// {type: "counter/increment"}

Reducersのルール

  • 新しいstateは元のstateaction引数に基づいてのみ計算される
  • stateを直接変更してはならない。代わりにstateのコピーを作って変更することで更新するimmutable updatesでなくてなはらない
  • 非同期処理やその他の副作用を行ってはならない

Reduxのゴールの一つにコードが予測可能であることがある。関数の出力は入力によってのみ計算される。それはコードの処理内容やテストが理解しやすいことです。

immutable updatesは予測可能のためには重要なルールです。

ReducersとImmutable Updates

存在するオブジェクトや配列の値を変更することがmutationで変えることが出来ない値がimmutabilityです。

Reduxではreducersは決してオリジナルや現在のstateの値を変更することを許しません。

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

// ✅ This is safe, because we made a copy
return {
  ...state,
  value: 123
}

JavaScriptの配列やオブジェクトをspread oparetorsなどを使って、手動でimmutable updatesを書くことは出来ます。ただし覚えたり正しく記載するのが困難なのです。

Redux ToolkitのcreateSlice関数は簡単な方法でimmutable updatesを書くことが出来ます。immerと呼ばれるライブラリを内部で使っていてmutatesな書き方をして安全にimmutably updateを実行します。

function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
}

function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}

この2つの関数はどちらも同じ値を更新しています。後者は簡単に書けるし読みやすく理解しやすいのは一目瞭然でしょう。

Thunksを使った非同期処理の書き方

thunkは非同期処理を含むことができる特別なRedux関数です。Thunksは2つの関数を使って書かれています。

  • 内側のthunk関数はdispatchgetStateを引数としてもちます。
  • 外側のクリエーター関数は生成とthunk関数を返します。

counterSliceからエクスポートされた次の関数はthunk action creatorの例です。

features/counter/counterSlice.js
// The function below is called a thunk and allows us to perform async logic.
// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
// This will call the thunk with the `dispatch` function as the first argument.
// Async code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount))
  }, 1000)
}

以前はthunkを使うにはRedux用プラグインとしてredux-thunkミドルウエアをRedux Storeに追加される必要がありました。Redux ToolkitのconfigureStore関数は既に組み込み済みなのですぐにthunkを使えます。

サーバからデータを取得するのにAJAX呼び出しが必要なとき、thunkの中に記述します。

features/counter/counterSlice.js
// the outside "thunk creator" function
const fetchUserById = userId => {
  // the inside "thunk function"
  return async (dispatch, getState) => {
    try {
      // make an async call in the thunk
      const user = await userAPI.fetchById(userId)
      // dispatch an action when we get the response back
      dispatch(userLoaded(user))
    } catch (err) {
      // If something went wrong, handle it here
    }
  }
}

React Hook

ReactからRedux StoreにアクセスするにはReactのカスタムフックを利用したReact-Reduxライブラリを用います。

Redux Storeから必要なデータを読み出すにはuseSelectorフックを使います。

features/counter/counterSlice.js
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value

現在のStoreからcounter valueを取得するには次のようにします。

const count = useSelector(selectCount)

ActionsをDispatchingするにはuseDispatchフックを使います。Redux Storeからdispatchメソッドを取得します。

const dispatch = useDispatch()

Component State and Forms

すべてのstateはRedux storeにいつも配置する必要はありません。複数のコンポーネントから利用されるグローバルなstateだけです。コンポーネント内で管理するstateは今まで通りReactで管理します。


Formsのstateなどはコンポーネント内で管理するstateです。useStateフックを使って管理しましょう。


Redux storeで管理するかどうか迷ったときは以下のことを考えましょう。

  • アプリケーションの他の場所でそのデータを使うかどうか
  • オリジナルデータに基づく継承されたデータのことに気を使う必要があるかどうか
  • 複数のコンポーネントで同じデータを使うかどうか
  • タイムトラベルデバッグのような任意の時点にstateを戻す値があるかどうか
  • UIコンポーネントをホットリロードする間データを保ち続けたいかどうか

Providing the Store

useSelectoruseDispatchフックによってRedux storeとやり取りを説明しましたが、どうやってRedux storeをアプリケーションに摘要するかを説明します。

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'

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

ルートの<App>コンポーネントをレンダリングし始めるためにReactではReactDom.render(<App />)を呼び出します。Redux storeを引数に渡した<Provider>を呼び出します。

これでどのReactコンポーネントからでもuseSelectoruseDispatchを呼び出してRedux storeにアクセスすることが出来ます。

目次に戻る

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

先頭に戻る