Next.jsにReduxを実装してみて
はじめに
新しい Next.js のプロジェクトに、ログイン情報など page 層をまたぐ値を格納する Store を導入したく、Redux を触ってみた
間違いやもっといい方法がある場合はどしどしご指摘くださいませ
記事のターゲット
- Redux を導入しようとしてる人
- 基本は page でステート管理するけどグローバルな情報だけ store を使いたい(ライトに redux 使いたい)人
概要
ざっくりイメージ
- @reduxjs/toolkit で state の管理
- react-redux で react と bind してる
環境
Redux(+localStorage とバインドさせるために)を入れるためにインストールしたもの
yarn add @reduxjs/toolkit redux-persist react-redux
yarn add -D @types/react-redux
{
"dependencies": {
"@reduxjs/toolkit": "1.5.0",
"next": "10.0.3",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-hook-form": "6.14.0",
"react-redux": "7.2.2",
"redux-persist": "6.0.0"
},
"devDependencies": {
"@types/react-redux": "7.1.15",
}
}
redux-persistの最新コミットが結構古くて若干怖いけど star 数も多いし、Next.js の examplesもあったので導入。いざとなれば自作するくらいのお気持ち。
Redux のドキュメントを見ると Redux Toolkit イイヨ!ってお勧めされてたから Redux Toolkit を使うことにした
Whether you're a brand new Redux user setting up your first project, or an experienced user who wants to simplify an existing application, Redux Toolkit can help you make your Redux code better.
store(Redux) のディレクトリ構成(参考)
- redux-way
- ducks
- re-ducks
があるらしい
Redux Toolkit 使うのでそもそもコードの記述量が減るのとあくまでログイン情報のみを store で管理する予定なのでstore.ts
の 1 ファイルでもいいかもと思った(store はこれまでもこれからも拡大しないよの意を 1 ファイルであることで表す)
いざ実装
slice を作ります
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export type User = {
name: string | null
age: number | null
email: string | null
token: string | null
history: string[]
}
export type UserState = {
user: User
}
export type UpdateUserPayload = User
export type AddHistoryPayload = string
const initialState: UserState = {
user: {
name: null,
age: null,
email: null,
token: null,
history: [],
},
}
export const userSlice = createSlice({
name: 'user',
initialState,
// HACK: reducerは肥大化したらファイル分けたくなるかも
reducers: {
updateUser(state, action: PayloadAction<UpdateUserPayload>) {
state.user = action.payload
},
addHistory(state, action: PayloadAction<AddHistoryPayload>) {
state.user.history.push(action.payload)
},
reset(): UserState {
return initialState
},
},
})
store 作ります
ここで persist の設定もしちゃう
Redux Toolkit のドキュメントを大いに参考にさせていただく
import {
configureStore,
getDefaultMiddleware,
combineReducers,
EnhancedStore,
} from '@reduxjs/toolkit'
import { userSlice } from 'store/user'
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import createWebStorage from 'redux-persist/lib/storage/createWebStorage'
// HACK: `redux-persist failed to create sync storage. falling back to noop storage.`の対応
// https://github.com/vercel/next.js/discussions/15687#discussioncomment-45319
const createNoopStorage = () => {
return {
getItem(_key) {
return Promise.resolve(null)
},
setItem(_key, value) {
return Promise.resolve(value)
},
removeItem(_key) {
return Promise.resolve()
},
}
}
const storage =
typeof window !== 'undefined'
? createWebStorage('local')
: createNoopStorage()
const rootReducer = combineReducers({
user: userSlice.reducer,
})
export type RootState = ReturnType<typeof rootReducer>
const persistConfig = {
key: 'p-next-test',
version: 1,
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const useStore = (): EnhancedStore => {
return configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
}
persistConfig を作る時に storage を入れるんだけども redux-persist の v6 だと yarn dev した際にredux-persist failed to create sync storage. falling back to noop storage.
って言われる
一時的にはこの対応で文句は言われなくなる
_app.tsx で store を呼びます
import { AppProps } from 'next/app'
import { Provider } from 'react-redux'
import { persistStore } from 'redux-persist'
import { PersistGate } from 'redux-persist/integration/react'
import { useStore } from 'store'
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
const store = useStore()
const persistor = persistStore(store)
return (
<Provider store={store}>
<PersistGate persistor={persistor}>
<Component {...pageProps} />
</PersistGate>
</Provider>
)
}
export default MyApp
@reduxjs/toolkit が Provider なるもの用意してくれてるのかと思えばそうではなく react-redux が必要なのがこんがらがりポイント
store に値を格納したり、取得したり
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'store'
import { userSlice } from 'store/user'
const StorePage = (): JSX.Element => {
const dispatch = useDispatch()
const user = useSelector((state: RootState) => state.user)
const handleConfirm = () => {
// eslint-disable-next-line
console.log(user)
}
const handleUpdate = () => {
dispatch(
userSlice.actions.updateUser({
name: 'name',
age: 28,
email: 'email',
token: 'token',
history: [],
})
)
}
const handleReset = () => {
dispatch(userSlice.actions.reset())
}
const handleAddHistory = () => {
dispatch(userSlice.actions.addHistory('push'))
}
return (
<div>
<h1>storeの動作確認</h1>
<button type="button" onClick={handleConfirm}>
確認
</button>
<button type="button" onClick={handleUpdate}>
update
</button>
<button type="button" onClick={handleReset}>
reset
</button>
<button type="button" onClick={handleAddHistory}>
addHistory
</button>
</div>
)
}
export default StorePage
react-redux の useSelector で state を取得して、useDispatch で action を呼び出す感じ
Discussion
大変参考になりました!
こちらを見ながら作っていたのですが、上手くstoreが更新されない時があり、
Provider
にstoreを入れるやり方があっていないかもしれないです。こちらのドキュメントを見た感じ、
configureStore
は新しいstoreを作るようなので、MyApp内で呼び出すと毎回新しいstoreを作ってしまっているのかなと思いました。なので外側でstoreを作成して、それをProviderに渡すのが正しいかもです。僕はこれで更新されない問題は解消されました。
ありがとうございます!
内容確認して、記事の修正しまっす!
ちなみに
こちら、どういった時にstoreが更新されませんでしたでしょうか?
手元で再現できず・・
基本的な実装はNext.jsのexamplesを参考にしてます!
もしstoreがうまく更新されないようであれば、動作確認する上で、いくつかexampleから省いた実装もあるので、そこが原因かと思ってます
すみません、返信が遅くなりました。
もしかしたらライブラリのバージョンの関係もあるかもしれませんが、ログイン周りの実装をした際に、dispatchしてからすぐrouter.pushでログイン後の画面に遷移したらstoreにログイン情報が入っていなかった感じでした。
リロードしたらデータは入っていましたが、そもそもpersistしないケースだと画面遷移するたびにstoreが初期値になってしまい、Functional Component内でstoreを作るやり方が間違っているんじゃないかなと思った次第です。