Redux Toolkitのディレクトリ構成/フォルダ構成(Ducksとre-ducks比較)

Next.jsの覚書です。

Reduxのファイル/フォルダ/ディレクトリ構成

Nuxt.jsでVueXの時ときも少し悩みましたが、今回も。

トラディショナルな方法

redux/
  ├─ actions/
  │   ├─ uploadActions.ts
  │   └─ ...
  ├─ reducers/
  │   ├─ uploadReducer.ts
  │   └─ ...
  └─ store.ts

この方法はNuxt.jsのVueXのときもそうしましたが、1番最初にやめにしました。やっぱり一緒のファイルの方が作業がスムーズですね。

re-ducks

src/
  ├─ redux/
  │   ├─ module1/
  │   │   ├─ actions.ts
  │   │   ├─ index.ts
  │   │   ├─ operations.ts
  │   │   ├─ reducers.ts
  │   │   ├─ selectors.ts
  │   │   └─ types.ts
  │   ├─ module2/
  │   │   ├─ ...
  │   └─ ...
  └─ store.ts

わかりませんけど、ducksの応用編(?)としてファイル構造が複雑になってさらなる学習コストを生み、小規模なプロジェクトでの採用は過度な気もしますね。悪評もあったため今回は辞めに。

Ducks

redux/
  ├─ ducks/
  │   ├─ upload.ts
  │   ├─ user.ts
  │   └─ ...
  └─ store.ts

“Ducks” パターンは、Reduxの初期の頃からあるアイディアで、それぞれの機能やドメインごとに関連するアクション、アクションクリエーター、リデューサーを1つのファイルにまとめるという考え方です。

// uploadDuck.ts

// Actions
const UPLOAD_START = 'UPLOAD_START';
const UPLOAD_SUCCESS = 'UPLOAD_SUCCESS';
const UPLOAD_FAILED = 'UPLOAD_FAILED';

// Action Creators
export const uploadStart = () => ({ type: UPLOAD_START });
export const uploadSuccess = () => ({ type: UPLOAD_SUCCESS });
export const uploadFailed = error => ({ type: UPLOAD_FAILED, payload: error });

// Reducer
const initialState = { status: 'idle', error: null };

export default function uploadReducer(state = initialState, action) {
  switch (action.type) {
    case UPLOAD_START:
      return { ...state, status: 'loading' };
    case UPLOAD_SUCCESS:
      return { ...state, status: 'succeeded', error: null };
    case UPLOAD_FAILED:
      return { ...state, status: 'failed', error: action.payload };
    default:
      return state;
  }
}

Redux Toolkitでスライスごと

redux/
  ├─ slices(ducks)/
  │   ├─ uploadSlice.ts
  │   └─ ...
  └─ store.ts
// uploadSlice.ts

import { createSlice } from '@reduxjs/toolkit';

const uploadSlice = createSlice({
  name: 'upload',
  initialState: { status: 'idle', error: null },
  reducers: {
    uploadStart: state => {
      state.status = 'loading';
    },
    uploadSuccess: state => {
      state.status = 'succeeded';
      state.error = null;
    },
    uploadFailed: (state, action) => {
      state.status = 'failed';
      state.error = action.payload;
    }
  }
});

export const { uploadStart, uploadSuccess, uploadFailed } = uploadSlice.actions;
export default uploadSlice.reducer;

Ducksと構造上、違いはありませんが、コードの中身が違うパターンですかね…。

createSliceを使うと、勝手にDucksになりますからRedux Toolkitを使うとDucksなのかなという気がしました。こちらのツイートが参考になりました。

スポンサーリンク

Next.jsでRedux Toolkit の使い方

こちらの動画わかりやすいかもしれません。

Udemy講師をされている方です。個人的にも1本買ってみました。Nuxt.jsのおすすめ動画や本はこちらの記事です。

少し迷ったことを補足していきます。

createAsyncThunkの第1引数

createAsyncThunk の第1引数は、生成される非同期アクションの基本となる文字列の名前を表しています。この文字列は、Redux Toolkit が自動的に3つのアクションタイプ(start, success, failure)を生成する際のベースとして使用されます。

例えば、'upload/uploadImage' という名前を指定すると、以下の3つのアクションタイプが生成されます。

  1. 'upload/uploadImage/pending': 非同期処理が開始されたとき
  2. 'upload/uploadImage/fulfilled': 非同期処理が成功したとき
  3. 'upload/uploadImage/rejected': 非同期処理が失敗したとき

これらのアクションタイプは、後で createSliceextraReducers などで使用できます。

Redux ToolkitのcreateAsyncThunkは、単一の引数のみを受け入れるように設計されています。そのため、複数のデータピースを非同期アクションに渡したい場合は、それらを一つのオブジェクトにまとめて渡す必要があります。

reducersとは?

reducers という言葉は、Reduxの文脈では、アプリケーションの状態を変更する関数を指します。リデューサーは、現在の状態とアクションを受け取り、新しい状態を返す純粋な関数です。この名前は、JavaScriptの Array.prototype.reduce メソッドからインスパイアされています。ただし、彼らが果たす役割は異なります。

“reduce” の意味としての “減少” や “減らす” はこの文脈では直接関係ありません。しかし、JavaScriptの Array.prototype.reduce メソッドの動作を理解すると、なぜ Redux の主要な関数を “reducer” と呼ぶのかの背景が少し明確になるかもしれません。

JavaScriptの reduce メソッドは、配列の各要素に対して関数を適用して、単一の出力値を生成します。この “reducing” というプロセスは、多くの要素を一つの値に “集約” するという意味での “reduce” です。

例として、数字の配列の合計を取る場合を考えてみましょう。

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum);  // 15

上記の例では、reduce メソッドは配列の各要素に対して関数を適用し、合計値を計算しています。

Reduxの “reducer” も似たような考え方に基づいています。状態とアクションを入力として受け取り、新しい状態を返します。このプロセスは、多くのアクションと現在の状態を “集約” して、新しい状態を “生成” するという意味での “reduce” です。

言い換えれば、リデューサーは、アプリケーション Colon完面書きの手紙 to reduce various actions and current state into a new state. です。

React-Reduxのラップ

 <Provider store={store}>
      {children}
 </Provider>

store.tsに書かれたものをラップして各page.tsxで使っています。

エラーがでたとき、対応しました。

React-Reduxのget

useSelector((state: RootState) => state.reducerName.stateProperty)

React-Redux は React と Redux を統合するためのもので、useSelector, useDispatch などのフックを提供しています。

スポンサーリンク

Next.js13全体のディレクトリ構成

Next.js13はcomponentsとhooksは2か所もありかなっと。

- public/ 画像など素材
  - font
  - image
- src/
  - app/ ページ(個別のCSS、部分のカスタムフックを含む)
    - components
    - hooks
    - globals.css
  - components/ Reactコンポーネント(UI要素) 基本はこっちにいれるけどapp内も許容
  - hooks/ 全体のカスタムフック だいたいはこっちにいれるけどapp内も許容
  - lib(utils)/ firebaseなどライブラリ
  - redux(server)/
  - styles/ 全体のCSS, root:
  - const/ 定数
  - data(content)/
  - tests/ テスト用

app内おくかapp外におくか、stackoverflowなどいろいろとみたがどちらでもよいようです。

NextJS 13 folder structure best practice

https://stackoverflow.com/questions/76214501/nextjs-13-folder-structure-best-practice

ただ、app内はpagesのフォルダが多くなるため、どちらかというとアプリ外派ですかね。globals.cssだけデフォルトの位置のままにしておきます。

スポンサーリンク

ディレクトリ構成/フォルダ構成の考え方

nextjs 14 use client;のファイル構成

クライアントサイドはuse client;と明示的につければいいだけです。

ただ、ファイル構成はどうしましょう。1つのファイルはpages.tsxほか、複合的なファイル構成になる場合が多いです。もちろん、use client;をつけるものとつけないものの複合は許容されています。

pages.tsx
client-side-component.tsx(use client;)

この場合、SSRでレンダリングするのなら、page.tsxにuse client;を使えません

client-side-component.tsxみたいなファイルが必要になって、そこからカスタムフックを呼び出す必要があります。このあたりがちゃんとしていないと予期しないエラーにハマることがあります。

pagesの中身

UIUXやサーバーとの統一性などを考えると、意外と深いです。

- pages/
  - personality/
    - [id].tsx
  - p-templates/
    - PersonalityForm1.tsx
    - PersonalityForm2.tsx
  - p-image/
    - p1/
      - [id].tsx
    - p2/
      - [id].tsx

上記の構造は少なくとも5つのことを考えています。

  • 関連性を考えてディレクトリ構造をつくる
  • ただし、概念ごとに入れ子にすると階層が深くなってしまいがち(その構造がURLに反映される場合がある)
  • Firebaseのストレージなどと構造を同じにしたい
  • 階層を浅くするために並列に並べた場合、フォルダ名やファイル名の関連性で明示的にする
  • 2階層あるところは短縮することも考える

参考になれば幸いです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする