Create React App
react-reduxをサンプルで試してみる。
# npxはinstallに必要なpackageをinstallして、実行後に不要なpackageを削除します。
# npxの実行は環境を汚染しません。
# my-appはapp名です。他のapp名を指定することもできます。
npx create-react-app my-app
cd my-app
npm start
my-appの中に初期プロジェクトを作成します。
# 作成されるディレクトリ
my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
└── setupTests.js
◾️Philosophy
One Dependency
ビルドの依存関係は一つだけです。webpack, 、Babel、ESLint、その他の素晴らしいプロジェクトを使っていますが、それらの上に凝縮された体験を提供します。
No Configuration Required
何も設定する必要がありません。開発用ビルドと本番用ビルドの両方が適切に設定されるので、コードを書くことに集中できます。
No Lock-In
いつでもカスタムステップにイジェクトすることができます。
コマンドを実行するだけで、すべての設定とビルドの依存関係がプロジェクトに直接移動されるので、中断したところからすぐに再開することができます。
// src/index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css';
const container = document.getElementById('root')!;
// ReactDOM.renderはReact 18ではサポートされなくなります。代わりにcreateRootを使用します。
// 詳細は: https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis
const root = createRoot(container);
root.render(
// StrictModeはアプリケーションの潜在的な問題点を洗い出すためのツールです。
// 詳細は: https://ja.reactjs.org/docs/strict-mode.html
// <App />の親要素に<Provider store={store}>を設定することでApp内でstoreを利用可能になる。
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// アプリのパフォーマンス測定を開始したい場合は、結果を記録する関数を渡すか、
//(例:reportWebVitals(console.log))
// 分析用のエンドポイントに送信します。詳細はこちら: https://bit.ly/CRA-vitals
reportWebVitals();
App.tsxでCounterContainerを使用しています。
// src/features/counter/Counter.tsx
// 必要な箇所を抜粋しています。
import {
useAppSelector, // stateの取得
useAppDispatch // storeにactionを渡す
} from '../../app/hooks';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
incrementIfOdd,
selectCount,
} from './counterSlice';
export function Counter() {
const count = useAppSelector(selectCount); // 必要なstateだけ取得する
...以下略
}
// src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// useDispatchとuseSelectorの代わりにアプリ全体で使用する。
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// src/app/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
/*
reducer:
SingleFunctionが渡された場合は直接storeのためにRootReducerとして使用されます。
slice reducersのオブジェクト(例: { users: usersReducer, posts: postsReducer })が渡された場合は、
configureStoreはこのオブジェクトをRedux combineReducers utilityに渡すことで自動でRootReducerを自動で作成します。
*/
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
/*
type AppDispatch = ThunkDispatch<{
counter: CounterState;
},
undefined, // ExtraThunkArg: 追加の引数をthunksの内部関数に渡します。Thunk middlewareをセットアップする時に明示します。
AnyAction>
& Dispatch<AnyAction>
*/
export type AppDispatch = typeof store.dispatch;
/*
type RootState = {
counter: CounterState;
}
*/
export type RootState = ReturnType<typeof store.getState>; // ReturnType: function Typeの戻り値からなる型を構築します。
/*
ThunkAction<
ReturnType, // dispatchされたactionの戻り値
RootState, // Stateの型
unknown, // dispatchとgetStateの他にもうひとつ取れる引数の型
Action<string> // Actionの型です。
>;
*/
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
src/features/counter/counterSlice.ts
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
import { fetchCount } from './counterAPI';
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
// 以下の関数はthunkと呼ばれる。 非同期ロジックを実行することができる
// thunkは通常の関数と同様にdispatchすることができます。(例) dispatch(incrementAsync(10))
// これはdispatch関数を第一引数としてthunkを呼び出します。
// そして、非同期コードを実行し、他のActionsをdispatchすることができます。
// Thunkは通常、非同期なリクエストを行うために使用されます。
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount);
// 返された値はfulfilled Actionのpayloadになります。(payload=actionに必要な値)
return response.data;
}
);
// createSliceは内部的にcreateActionとcreatReducerを使用しています。
export const counterSlice = createSlice({
name: 'counter',
initialState,
// reducers
// フィールドではreducersを定義し関連するactions, action type, reducerを生成する。
reducers: {
increment: (state) => {
// Redux Toolkitを使うと、変異ロジックをreducersの中に書くことができます。
// Immerライブラリは「draft state」への変更を検出し、その変更に基づいて全く新しいイミュータブルなstateを生成する。
// そのため、実際にはstateを変異させない。 old state -mutate-> new state
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// PayloadActionを使用して、action.paloadの内容を宣言する。
/*
PayloadAction<
P = void, action payloadの型
T extends string = string, 使用されるaction type
M = never, option: actionのmetaの型
E = never option: actionのerrorの型
>
*/
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
// extraReducersフィールドは、createAsyncThunkや他のsliceで生成されたactionなど、
// 他の場所で定義されたactionをsliceで処理できるようにします。
// 詳細: https://redux-toolkit.js.org/api/createReducer#usage-with-the-builder-callback-notation
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = 'failed';
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 以下の関数はselectorと呼ばれ、stateから値を選択できます。
// Selectorsはsliceファイルではなく、使用するインラインで定義することもできます。
// 例: useSelector((state: RootState) => state.counter.value)
export const selectCount = (state: RootState) => state.counter.value;
// 手書きでthunksを書くこともできます。thunksには同期と非同期の両方のロジックが含まれることがあります。
// これはcurreent stateに基づいて条件付きでactionをdispatchする例です。
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;