🛠️

Next.jsにReduxを実装してみて

6 min read

はじめに

新しい Next.js のプロジェクトに、ログイン情報など page 層をまたぐ値を格納する Store を導入したく、Redux を触ってみた
間違いやもっといい方法がある場合はどしどしご指摘くださいませ

記事のターゲット

  • Redux を導入しようとしてる人
  • 基本は page でステート管理するけどグローバルな情報だけ store を使いたい(ライトに redux 使いたい)人

概要

ざっくりイメージ

  • @reduxjs/toolkit で state の管理
  • react-redux で react と bind してる

環境

Redux(+localStorage とバインドさせるために)を入れるためにインストールしたもの

  1. yarn add @reduxjs/toolkit redux-persist react-redux
  2. yarn add -D @types/react-redux
package.json
{
  "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

があるらしい

https://superhahnah.com/redux-directory-petterns/

Redux Toolkit 使うのでそもそもコードの記述量が減るのとあくまでログイン情報のみを store で管理する予定なのでstore.tsの 1 ファイルでもいいかもと思った(store はこれまでもこれからも拡大しないよの意を 1 ファイルであることで表す)

いざ実装

slice を作ります

src/store/user/index.ts
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 のドキュメントを大いに参考にさせていただく

https://redux-toolkit.js.org/usage/usage-guide#use-with-redux-persist
src/store/index.ts
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 を呼びます

src/pages/_app.tsx
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 に値を格納したり、取得したり

src/pages/index.tsx
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 を呼び出す感じ

GitHubで編集を提案

Discussion

ログインするとコメントできます