Skip to content

Redux完全ガイド

Reduxを使用した状態管理を、実務で使える実装例とベストプラクティスとともに詳しく解説します。

Reduxは、予測可能な状態管理のためのライブラリです。単一のストアでアプリケーション全体の状態を管理します。

Reduxの特徴
├─ 単一の真実の源(Single Source of Truth)
├─ 状態は読み取り専用(Read-only State)
├─ 純粋関数による変更(Pure Functions)
└─ タイムトラベルデバッグ

問題のある構成(Reduxなし):

// 問題: 状態が複数の場所に散在
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// 状態が増えると管理が困難
return <YourApp />;
}

解決: Reduxによる一元管理

// 解決: Reduxによる一元管理
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
user: userReducer,
theme: themeReducer,
},
});
Terminal window
npm install @reduxjs/toolkit react-redux
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slices/counterSlice';
import userReducer from './slices/userSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST'],
},
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
App.jsx
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<YourApp />
</Provider>
);
}
store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
},
},
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
store/slices/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 非同期アクション
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: {
user: null,
loading: false,
error: null,
},
reducers: {
clearUser: (state) => {
state.user = null;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
export const { clearUser } = userSlice.actions;
export default userSlice.reducer;
components/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from '../store/slices/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
hooks/redux.ts
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '../store';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
// components/Counter.tsx
import { useAppSelector, useAppDispatch } from '../hooks/redux';
import { increment, decrement } from '../store/slices/counterSlice';
function Counter() {
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}

5. 実務でのベストプラクティス

Section titled “5. 実務でのベストプラクティス”
store/selectors/counterSelectors.js
export const selectCount = (state) => state.counter.value;
export const selectCountDouble = (state) => state.counter.value * 2;
// コンポーネントでの使用
import { useSelector } from 'react-redux';
import { selectCount, selectCountDouble } from '../store/selectors/counterSelectors';
function Counter() {
const count = useSelector(selectCount);
const countDouble = useSelector(selectCountDouble);
return (
<div>
<p>Count: {count}</p>
<p>Double: {countDouble}</p>
</div>
);
}

パターン2: ミドルウェアの使用

Section titled “パターン2: ミドルウェアの使用”
store/middleware/logger.js
const logger = (store) => (next) => (action) => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', store.getState());
return result;
};
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import logger from './middleware/logger';
export const store = configureStore({
reducer: {
// reducers
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger),
});

パターン3: 永続化(Redux Persist)

Section titled “パターン3: 永続化(Redux Persist)”
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import userReducer from './slices/userSlice';
const persistConfig = {
key: 'root',
storage,
whitelist: ['user'], // 永続化するスライス
};
const persistedReducer = persistReducer(persistConfig, userReducer);
export const store = configureStore({
reducer: {
user: persistedReducer,
},
});
export const persistor = persistStore(store);

原因:

  • セレクターが不適切
  • メモ化が不十分

解決策:

// メモ化されたセレクターを使用
import { createSelector } from '@reduxjs/toolkit';
const selectUsers = (state) => state.users.items;
const selectFilter = (state) => state.users.filter;
export const selectFilteredUsers = createSelector(
[selectUsers, selectFilter],
(users, filter) => users.filter(user => user.name.includes(filter))
);

問題2: 非同期処理のエラーハンドリング

Section titled “問題2: 非同期処理のエラーハンドリング”

原因:

  • エラーハンドリングが不十分
  • ローディング状態の管理が不適切

解決策:

// 適切なエラーハンドリング
const userSlice = createSlice({
name: 'user',
initialState: {
user: null,
loading: false,
error: null,
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});

これで、Reduxの基礎知識と実務での使い方を理解できるようになりました。