hysryt / wiki

https://hysryt.github.io/wiki/
0 stars 0 forks source link

Redux #181

Open hysryt opened 3 years ago

hysryt commented 3 years ago
hysryt commented 3 years ago

アクション:ストアのデータを書き換える何かしらのイベント リデューサー:現在のストアのデータとアクションを受け取り、アクションの内容に応じてストアのデータを更新して返す関数 ストア:ステートツリーを保持する

アクションのログを取るだけで全ての変化を記録できる。


npm install redux react-redux

TypeScriptの場合は型も一緒にインストール

npm install redux react-redux @types/react-redux
hysryt commented 3 years ago

Redux Toolkit

https://redux-toolkit.js.org/ Reduxを使うときはRedux Toolkitも一緒に使うことが推奨されている。(もちろん ReduxToolkit無しでも書ける) Reduxを使ったアプリを簡単に、ベストプラクティスに則って記述できる。

npm install @reduxjs/toolkit
hysryt commented 3 years ago

アクション(Action)

const action = {
  type: 'increment',
};

アクションはプレーンなオブジェクト。 type フィールドは必須、domain/eventNameの形式で書くことが多い。 負荷情報を持たせることも可能で、一般的に payload フィールドとして追加する。

リデューサー(Reducer)

const reducer = (state, action) => {
  ...
  return newState;
}

リデューサーはステートとアクションを受け取り、新しいステートを返す関数。 アクションの内容に応じてステートを更新する。 引数で受け取ったステートは書き換えられないため、ステートをコピーし、そちらを書き換える。

リデューサーの中で非同期通信をしたりランダム値を計算してはならない。 引数が同じ場合は同じ結果を返さなければならない。 非同期通信の結果やランダム値が必要な場合はアクションにその値を持たせ、リデューサーでは更新だけを行う。

ストア(Store)

const store = createStore(reducer);

ストアはリデューサーを1つ持つ。 規模が大きいアプリの場合は、リデューサーは子リデューサーを持つ。 ストアが直接保持するリデューサーのことをルートリデューサーと呼ぶ。

store.dispatch(action);

ストアは dispatch 関数でアクションを受け取る。 アクションを受け取ると、ストアが保持している現在のステートと受け取ったアクションをルートリデューサーに渡し、ステートを更新する。

ミドルウェア

ストアを拡張する。 例えば以下のような機能を実現する。

hysryt commented 3 years ago

Redux Toolkit

ストア(Store)

configureStore

const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

Redux Toolkit では createStore の代わりに configureStore を使ってストアを生成する。 configureStore の第一引数には reducer を指定(必須)したオブジェクトを渡す。 このリデューサーは createSlice によって生成したオブジェクト内の関数。 リデューサーは機能ごとに分割して設定できる。

reducer に指定したオブジェクトのキー名はそのままステートのキー名となる。

スライス(Slice)

機能ごとに分割したリデューサーとアクションのセット。 リデューサーは機能ごとに分割したステートを対象とする。 分割したリデューサーはスライスリデューサーと呼ばれることもある。

createSlice

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

createSlice は複数の更新関数を一つのリデューサーにまとめてくれる。 また、アクションを生成する関数も生成してくれる。

counterSlice.reducer; // リデューサー
counterSlice.actions.increment(); // typeが "counter/increment" のアクションを生成する

アクションのtypeは createSliceに渡した name の値と、 reducers に渡したオブジェクトのキー名を合わせたものとなる。

createSlice 内のリデューサーは引数で受け取ったステートをそのまま変更できる

// 通常のリデューサー
const newState = {
  ...state,
  value: state.value + 1,
};

// createSlice内のリデューサー
state.value += 1;

内部でImmerというライブラリを使っているとのこと。

prepare関数

リデューサーと一緒にアクションのpayloadを設定する関数を設定できる。

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    incrementAmount: {
      reducer(state, action) {
        state.value += action.payload.amount;
      },
      prepare(amount) {
        return { payload: { amount } };
      }
    }
  },
});

サンク(Thunk)

内部で非同期処理を行い、処理が終わったタイミングでdispatchを行う関数のことをサンクと呼ぶ。 サンクを作成する関数をサンククリエーター関数と呼ぶ サンククリエーター関数を実行すると dispatchgetState を引数としてとる関数(サンク関数)が返ってくる。 サンク関数を store.dispatch に渡すと非同期処理が行われ、終わったタイミングでdispatchが実行される。

// サンククリエーター関数
const incrementAsync = amount => {
  // サンク関数
  return (dispatch, getState) => {
    setTimeout(() => {
      dispatch(incrementByAmount(amount));
    }, 1000);
  }
};

store.dispatch(incrementAsync(5));

サンクを使用するには redux-thunk ミドルウェアが必要。 Redux Tookitのデフォルト設定で redux-thunk は追加されている。

内部でajax通信を行うサンク関数の例。

// サンククリエーター関数
const fetchUserById = userId => {
  // サンク関数
  return async (dispatch, getState) => {
    try {
      const user = await userAPI.fetchById(userId)
      dispatch(userLoaded(user))
    } catch (err) {
      // エラー処理
    }
  }
}

createAsyncThunk

start/success/failure アクションを自動的にディスパッチするサンクを作成する。

const fetchPosts = createAsyncThunk('posts/fetchPosts', async() => {
  const response = await client.get('/fakeApi/posts');
  return response.posts;
});

第一引数はアクションタイプのプレフィックス。 第二引数はPromiseを返す処理。 dispatch(fetchPosts()) を実行するとまずタイプが posts/fetchPosts/pending のアクションをディスパッチする。 そしてPromiseが解決できると post/fetchPosts/fulfilled または post/fetchPosts/rejected のアクションをディスパッチする。

セレクター

useSelector

Redux自体の機能。 ストアから特定のステートを取得するフック。

const count = useSelector(state => {
  return state.counter.value,
});

ストアが更新されると useSelector は再実行される。 ステートが変更されている場合はコンポーネントを再レンダリングする。

ディスパッチ

useDispatch

Redux自体の機能。 ディスパッチ関数を取得するフック。

const dispatch = useDispatch();
dispatch({
  type: "counter/increment",
});

createSelector

userSelector のメモ化版。 特定の値が変更された時のみセレクターを実行し、再レンダリングを行う。

createEntityAdapter

要素のコレクション(ユーザーリストや投稿リストなど)を扱いやすくするためのAPI。 コレクションを操作するための関数やセレクタが自動生成されたり、ソート順が維持されたりといったメリットがある。

createEntityAdapter 関数を使うとステートはデフォルトで ids 配列と entities オブジェクトを持つオブジェクトとなる。 entities オブジェクトのキーは ids 配列に格納されてる各IDとなる。 これによって find 関数ではなく entities[id] のように直接要素へアクセスできるようになるため高速。

createEntityAdapter の第一引数にはオプションを渡す。 setComparer オプションは比較関数を設定できる。この比較順に都度 ids 配列が並び替えられる。

const adapter = createEntityAdapter({
  sortComparer: (a, b) => b.date.localeCompare(a.date)
});

console.log(adapter.getInitialState());  // { ids: [], entities: {} }

ステートにプロパティを追加したい場合は getInitialState 関数の第一引数に指定する。

console.log(adapter.getInitialState({
  otherState: 'other',
})); // { ids: [], entities: {}, otherState: 'other' }

コレクションに要素を追加する場合は addOne 関数、または addMany 関数を使用する。 addOne 関数は一つの要素、addMany 関数は複数の要素を追加する。 引数指定する state は可変である必要がある。

同じように、更新と追加を行いたい場合は upsertOneupsertMany 関数が使える。

getSelectors 関数で汎用的なセレクタ関数を生成できる。

export const {
  selectAll,
  selectById,
  selectIds,
} = postAdapter.getSelectors(state => state.posts);