Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux #181

Open
hysryt opened this issue Aug 1, 2021 · 4 comments
Open

Redux #181

hysryt opened this issue Aug 1, 2021 · 4 comments

Comments

@hysryt
Copy link
Owner

hysryt commented Aug 1, 2021

  • ステートを一元的に、コンポーネントの外で管理する
  • アクションを介してのみステートを更新できる
    • 予測可能になる
@hysryt
Copy link
Owner Author

hysryt commented Aug 1, 2021

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

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


npm install redux react-redux

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

npm install redux react-redux @types/react-redux

@hysryt
Copy link
Owner Author

hysryt commented Aug 9, 2021

Redux Toolkit

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

npm install @reduxjs/toolkit

@hysryt
Copy link
Owner Author

hysryt commented Aug 11, 2021

アクション(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
Copy link
Owner Author

hysryt commented Aug 12, 2021

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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant