Redux Toolkit非公式翻訳(2) Basic Tutorial

元ページ: redux-toolkit.js.org


基本的なチュートリアル

Redux Toolkitへようこそ!これはRedux Toolkitに含まれる基本的な関数を紹介するチュートリアルです。

このチュートリアルでは、読者は既にReduxそのものや、Reactと組み合わせて使う方法についてはよく知っているものと仮定しています。もしあなたがそうでないなら、まずはReduxやReactのドキュメントに目を通してみて下さい。ここではRedux Toolkitの使用法がどのように"典型的"なReduxのコードと異なるのかを中心に説明していきます。

イントロダクション:カウンターアプリケーションを作る

まずは、とても小さなReduxのサンプルを見てみることから始めましょう。シンプルなカウンターのアプリケーションです。

Reduxによるカウンターアプリの例

Reduxのドキュメントには"バニラ"なカウンターアプリのサンプルがあります。このサンプルは、一つの数値を持ち、"INCREMENT"や"DECREMENT"のようなaction typeに応じて動作するRedux storeとreducerの作成方法が示されています。完全なコードはCodesandboxで確認することができますが、簡略化したコードは下にあります。

function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0
  }
  
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

var store = Redux.createStore(counter)

document.getElementById('increment').addEventListener('click', function () {
  store.dispatch({ type: 'INCREMENT' })
})

このコードではcounterというreducer関数を作成しています。この関数はデフォルトのstate値として0を取り、"INCREMENT"と"DECREMENT"というaction typeを受け取り、ボタンがクリックされたときaction typeの"INCREMENT"をdispatchします。

例を修正する

上記の例はシンプルですが、あまり現実的ではありません。ほとんどのReduxアプリはES6記法で書かれているし、関数はundefinedなパラメータが渡ってきた場合のためにデフォルト引数を含んでいるものです。また、コードに直接actionオブジェクトを記述するのではなく"action creator"関数を定義し、action typeを毎回記述するのではなく定数として定義しておくほうが一般的です。

上記のアプローチを使って最初の例を修正してみましょう。

const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'

function increment() {
  return { type: INCREMENT }
}

function decrement() {
  return { type DECREMENT }
}

function Counter(state = 0, action) {
  switch(action.type) {
    case INCREMENT:
      return state + 1
    case DECREMENT:
      return state - 1
    default:
      retunr state
  }
}

const store = Redux.createStore(counter)

document.getElementById('increment').addEventListener('click', () => {
  store.dispatch(increment())
})

この例は小さなものなので、あまり見た目に大きな変化はないでしょう。サイズに関しては、デフォルト引数を使うことで数行削減できましたが、action creator関数を追加したことでさらに大きくなっています。そして重複している箇所があります。
const INCREMENT = 'INCREMENT'はばかげているように見えるでしょう:) 特に、これに関しては二カ所でしか使われていません。action creatorとreducerです。

加えて、switch式は多くの人にとってはわずらわしいものです。代わりにこれをlookup tableのようなものに置き換えることができれば良さそうです。

configureStore

Redux ToolkitにはReduxコードをシンプルにするいくつかの関数が含まれています。最初の関数はconfigureStoreです。

通常は、createStore()を呼び出してルートreducer関数を渡すことでRedux storeを作成します。Redux Toolkitでは、createStorre()をラップしたconfigureStore()を使うことで同じことができます。

既存のcreateStore呼び出しは簡単にconfigueStoreに置き換えられます。configureStoreは複数の引数でなく名前付きフィールドが定義された単一のオブジェクトを受け取るため、reducer関数をreducerフィールドとして渡す必要があります。

// 変更前
const store = createStore(counter)

// 変更後
const store = configureStore({
  reducer: counter
})

変更前と比べてあまり違いが無いように見えます。しかし実際には、dispatchされたactionやsttateの変更が確認できるようstoreはRedux DevToolsExtensionが使えるような形で定義され、いくつかのミドルウェアが自動で含まれます。詳細は次のチュートリアルで確認しましょう。

createAction

次はcreateActionを見てみましょう。

createActionはaction typeの文字列を引数として受け取り、type文字列を使用するaction creator関数を返します。(これはつまり、この関数の名前が少し不正確だということです。関数は"actionオブジェクト"でなく"action creator関数"を作成します。しかしcreateActionCreatorよりは短くて覚えやすいのでこの名前になっています。)次の2つの例は同じ意味です。

// オリジナル:手動でaction typeとaction creatorを記述する
const INCREMENT = 'INCREMENT'

function incrementOriginal() {
  return { type: INCREMENT }
}

console.log(incrementOriginal())
// { type: "INCREMENT" }

// 'createAction'を使ってaction creatorを生成する
const incrementNew = createAction('INCREMENT')

console.log(incrementNew())
// { type: "INCREMENT" }

reducerの中でaction type文字列を参照する必要がある場合はどうでしょうか。createActionを使えば、2つのやり方で実現できます。1つ目はオーバーライドされたaction creatorのtoString()を使う方法です。このメソッドはaction type文字列を返します。2つ目は関数の.typeフィールドで取得する方法です。

const increment = createAction('INCREMENT')

console.log(increment.toString())
// "INCREMENT"

console.log(increment.type)
// "INCREMENT"

createActionによって前のカウンターアプリのサンプルをシンプルにできます。

const increment = createAction('INCREMENT')
const decrement = createAction('DECREMENT')

function counter(state = 0, action) {
  switch(action.type) {
    case increment.type:
      return state + 1
    case decrement.type;
      return state - 1
    default:
      return state
  }
}

const store = Redux.createStore(counter)

document.getElementById('increment').addEventListener('click', () => {
  store.dispatch(increment())
})

configureStoreの時のように数行削減できました。また、INCREMENTのような文字列が重複しているようなこともありません。

createReducer

reducer関数を見てみましょう。reducerではif式やループなどの条件付きロジックを扱うことができますが、最も一般的なアプローチはaction.typeフィールドをチェックしてaction typeに応じた処理を行うやり方です。reducerは初期値も提供し、actionがそのreducerが関心を持つものでなければ既存のstateを返します。

Redux ToolkitはcreateReducerという関数を含んでいます。これを使うことで、"lookup table"のようなオブジェクトを使ってreducerを記述することができます。このオブジェクトはそれぞれのキーがReduxのaction type文字列になっており、値はreducer関数です。これを使って直接既存のcountr関数を置き換えることができます。action type文字列をキーとして使う必要があるため、type文字列からキーを作成するためにES6オブジェクトの"computed property"シンタックスを使うことができます。

const increment = createAction('INCREMENT')
const decrement = createAction('DECREMENT')

const counter = createReducer(0, {
  [increment.type]: state => state + 1,
  [decrement.type]: state => state - 1
})

完全なコードを確認する場合はcreateActionとcreateReducerの使い方を示したCodeSandboxコードを見てください。

createSlice

この時点でcounterのサンプルコードがどんな風になっているか見てみましょう。

const increment = createAction('INCREMENT')
const decrement = createAction('DECREMENT')

const counter = createReducer(0, {
  [increment]: state => state + 1,
  [decrement]: state => state - 1
})

const store = configureStore({
  reducer: counter
})

document.getElementById('increment').addEventListener('click', () => {
  store.dispatch(increment())
})

悪くはないですが、もう一つ大きな変更を加えることができます。なぜaction creatorをばらばらに生成したり、action type文字列を書き出したりしないといけないのでしょうか?ここで本当に重要なのはreducer関数です。

createSlice関数はまさにこのためにあります。この関数によってreducer関数を含むオブジェクトを取得することができ、またユーザーが指定したreducerの名称に基づいて自動的にaction type文字列とaction creator関数を生成します。

createSliceは”slice”というオブジェクトを返します。これは生成されたreducer関数をreducerというフィールドに含み。actionsフィールドにaction creatorsがセットされています。

createSliceを使って書き換えたカウンターアプリの例はこちらです。

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
    decrement: state => state - 1
  }
})

const store = configureStore({
  reducer: counterSlice.reducer
})

document.getElementById('increment').addEventListener('click', () => {
  store.dispatch(counterSlice.actions.increment())
})

多くの場合、ES6の分割記法を使ってaction creatorやreducerを変数として読み込みたいと思います。

const { actions, reducer } = counterSlice
const { increment, decrement } = actions

まとめ

それぞれの関数の機能についてまとめてみましょう。

  • configureStore: Redux独自のcreateStoreのようにRedux storeインスタンスを作成しますが、こちらは名前付きオプションがセットされたオブジェクトを受け取り、Redux DevTools拡張を自動的にセットアップします。
  • createAction: action type文字列を受け取り、その文字列を使用するaction creator関数を返します。
  • createReducer: stateの初期値とreducer関数に対応するaction typeのリストを受け取り、全てのaction typeを制御するreducerを生成します。
  • createSlice: state初期値、reducer名と対応する関数のリストを受け取り、action creator関数、action type文字列、reducer関数を自動で生成します。

これらの関数はReduxの挙動自体を変えるものではないことに注意してください。私たちは未だにRedux storeを作成し、どの処理を実行するかが記載されたactionオブジェクトをdispatchし、reducer関数を使って更新されたstateを返しています(注:オリジナルのReduxを使う場合とRedux-Toolkitの上記関数群を使う場合では、どちらも行っている処理の流れ自体は同じ)。

また、Redux Toolkitの関数群は、UIを構築する際にどんなアプローチを取ったとしても使うことができます。これは、各関数が単に"プレーンなRedux store"を扱うだけのものだからです。今回のサンプルではバニラなJSによってstoreを扱いましたが、同じことはReactやAngular、VueなどUI層に何を採用したとしても同じように行うことができます。

最後に、サンプルをよく見てみると、非同期な処理を行っている箇所が一つあるのがわかると思います。"increment async"ボタンです。

document.getElementById('incrementAsync').addEventListener('click', function() {
  setTimeout(function() {
    store.dispatch(incremenet())
  }, 1000)
})

非同期処理をreducerのロジックから切り離し、storeを更新する必要があるときにactionをdispatchしているのがわかると思います。Redux Toolkitはこの点については何も変更しません。

サンプルの完全なコードは以下です。

<!DOCTYPE html>
<html>
  <head>
    <title>Redux Starter Kit: createSlice Example</title>
    <script src="https://unpkg.com/@reduxjs/toolkit@latest/dist/redux-toolkit.umd.js"></script>
  </head>
  <body>
    <div>
      <p>
        Clicked: <span id="value">0</span> times
        <button id="increment">+</button>
        <button id="decrement">-</button>
        <button id="incrementIfOdd">Increment if odd</button>
        <button id="incrementAsync">Increment async</button>
      </p>
    </div>
    <script>
      const RTK = window.RTK;

      const counterSlice = RTK.createSlice({
        name: "counter",
        initialState: 0,
        reducers: {
          increment: state => state + 1,
          decrement: state => state - 1
        }
      });

      const { increment, decrement } = counterSlice.actions;

      const store = RTK.configureStore({ reducer: counterSlice.reducer });
      const valueEl = document.getElementById("value");

      function render() {
        valueEl.innerHTML = store.getState().toString();
      }

      render();
      store.subscribe(render);

      document
        .getElementById("increment")
        .addEventListener("click", function() {
          store.dispatch(increment());
        });

      document
        .getElementById("decrement")
        .addEventListener("click", function() {
          store.dispatch(decrement());
        });

      document
        .getElementById("incrementIfOdd")
        .addEventListener("click", function() {
          if (store.getState() % 2 !== 0) {
            store.dispatch(increment());
          }
        });

      document
        .getElementById("incrementAsync")
        .addEventListener("click", function() {
          setTimeout(function() {
            store.dispatch(increment());
          }, 1000);
        });
    </script>
  </body>
</html>

それぞれの関数の基本が理解できたと思います。次のステップでは、関数の機能をさらに詳しく見るためにもう少し大規模なサンプルに組み込んでいきます。これは中級チュートリアルで行います。