Next.jsの覚書です。
Contents
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の応用編(?)としてファイル構造が複雑になってさらなる学習コストを生み、小規模なプロジェクトでの採用は過度な気もしますね。悪評もあったため今回は辞めに。
Re-ducks パターンを採用してたプロジェクト、そろそろ負債化の芽が見えだしてるんじゃなかろか。自分が関わってたプロジェクトでは全力で止めといたけど。
— 大岡由佳『りあクト! 第4版』BOOTHで販売中!紙本も (@oukayuka) December 25, 2019
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なのかなという気がしました。こちらのツイートが参考になりました。
Redux Toolkit で不要だと思うのは Thunk ですね。副作用処理は Hooks で行えばいいので、Middleware 自体の存在意義に疑問符がついてると思ってます。
— 大岡由佳『りあクト! 第4版』BOOTHで販売中!紙本も (@oukayuka) February 28, 2020
MW に副作用をもたせると、通信の状態やエラー情報まで Store に突っ込むことになって管理が煩雑ですし。
Next.jsでRedux Toolkit の使い方
こちらの動画わかりやすいかもしれません。
Udemy講師をされている方です。個人的にも1本買ってみました。Nuxt.jsのおすすめ動画や本はこちらの記事です。
少し迷ったことを補足していきます。
createAsyncThunkの第1引数
createAsyncThunk
の第1引数は、生成される非同期アクションの基本となる文字列の名前を表しています。この文字列は、Redux Toolkit が自動的に3つのアクションタイプ(start, success, failure)を生成する際のベースとして使用されます。
例えば、'upload/uploadImage'
という名前を指定すると、以下の3つのアクションタイプが生成されます。
'upload/uploadImage/pending'
: 非同期処理が開始されたとき'upload/uploadImage/fulfilled'
: 非同期処理が成功したとき'upload/uploadImage/rejected'
: 非同期処理が失敗したとき
これらのアクションタイプは、後で createSlice
の extraReducers
などで使用できます。
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階層あるところは短縮することも考える
参考になれば幸いです。
コメント