如何在nextjs(typescript)中集成redux(with Redux Toolkit)

1,010 阅读5分钟

Redux Toolkit 是官方强烈推荐,开箱即用的一个高效的Redux 开发工具集, 本文将介绍如何在nextjs项目中集成redux来共享全局状态

同步

1.创建一个slice(可以理解为redux的一个数据模块)

  • 根目录下创建store文件夹
  • store文件夹中创建一个counter文件夹, 这个couter作为我们一个store的模块
  • 在counter中创建counterSlice.ts文件, 因为在操作过程中会创建其他的文件或文件夹, 所以我把文件结构放在本文最后, 有需要可以拉到最下面查看; counterSlice.ts文件的位置应该是在/store/counter/counterSlice.ts
  • 接下来我们在counterSlice.ts文件中创建一个slice
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export interface CounterState {
  value: number;
  status: "idle" | "loading" | "failed";
}

const initialState: CounterState = {
  value: 0,
  status: "idle",
};

// 重点看这一部分
export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

可以看到, 我们的slice主要由3部分组成

  • name: 模块名
  • initialState: 初始状态
  • reducers: reducers是一个对象, 对象的key是字符串(方法名), value是一个方法(可以理解为vuex中的一个action), 这个方法接受一个state参数, 改变这个参数的值会更新我们所有用到这个state的地方的view

2.将slice上的reducer暴露给出来, 用于在store总模块中集成

export default counterSlice.reducer;

3.创建store

创建store其实就是就是把所有的reducer像是拼积木一样组合到一起的过程

import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
  reducer: { 
      counter: counterReducer // 当然, 这个key值你是可以自定义的, 这影响到你调用时的写法会不一样, 但是为了避免歧义, 一般我们会写成模块名
      ... // 如果你有多个模块的话, 都是写在这里, 比如这样
      auth: authReducer
  },
});

4.为store添加类型支持

export type AppState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

所以我们/store/index.ts文件应该是这样的:


import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counter/counterSlice";

const store = configureStore({
  reducer: { counter: counterReducer },
});

export type AppState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

export default store;

到这里, 我们一个最小化的store就算继承完了,

5.在项目中去使用state

const count = useSelector<AppState, number>((state) => {
    state.counter.value;
});

然后我们就可以在项目中{count}去是使用这个state啦

6.在项目中改变state

改变state我们需要用到useDispatch这个hook

const dispatch = useDispatch();
const handleCountPlus = () => dispatch(incrementByAmount(10));

7.为useSelector增强类型

不知道大家有没有发现, 我们在使用useSelector的时候, 每次都要传入2个泛型, 即一个AppState, 一个number, 这样编辑器才知道我们返回的count是什么类型, 非常的麻烦, 为此, 我看到react-redux为我们提供了一个泛型工具可以完美的解决这个问题, 我们在store文件夹下新建一个hooks.ts文件

import { TypedUseSelectorHook, useSelector } from "react-redux";
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;

然后我们就可以这样使用

const count = useAppSelector((state) => state.counter.value);

当然, 如果涉及到一些复杂的运算, 我们也可以吧(state) => state.counter.value写成一个函数放在slice中(类比于vue的getter)

export const selectCount = (state: AppState) => state.counter.value;

然后我们就可以这样使用它

const count = useAppSelector(selectCount);

到这里, 我们redux的的集成的基础部分就算做完啦, 但是, 目前我们还不具备异步更新state的能力, 我们需要用到redux的一个中间件, reudx-thunk, 当然还有一个基于generetor函数写的redux-saga, reduc-thunk相对来说更加容易上手, 下面我们来用rendux-thunk来处理异步更新state

异步

1.创建一个asyncThunk(就是一个异步函数)

const fetchNewCount = (amount: number) =>
  request.post("/count", {
    data: { amount },
  });


export const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount: number) => {
  const response = await fetchNewCount(amount);Ï
  return response.data;
});

2.将创建的syncThunk添加到我们的slice(store 模块)中

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
  // 重点看这里
  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, err) => {
        state.status = "failed";
      });
    },
});

这就是redux处理异步的办法啦

补充知识

1.手写一个thunk

现在我们手写一个thunk, 这个thunk可以包含同步或者异步的逻辑

// 这个类型是从官方copy的
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, AppState, unknown, Action<string>>;

export const incrementIfOdd =
  (amount: number): AppThunk =>
  (dispatch, getState) => {
    const currentValue = selectCount(getState());
    if (currentValue % 2 === 1) {
      dispatch(incrementByAmount(amount));
    }
  };

上面的手写thunk还是比较容易理解的

  • 其实就是一个函数接受一个任意的参数
  • 然后返回一个AppThunk类型
  • AppThunk也是一个函数, 这个函数接收, dispatch用于调用同步方法更新state, getState是一个函数, 可以用于获取最新的state
  • 通过selectCount可以获取state上最新的value值
  • 也和普通的reducer一样调用即可dispatch(incrementIfOdd(incrementValue))

2.为redux天假数据自动持久化

redux persiter为我们提供了自动管理全局数据与持久化数据同步的能力, 除此之外, 我们可以对持久化数据的位置进行配置, 还可以配置需要持久化的模块的白名单, 为我们开发带来非常大的便捷性, 当然, 如果你的应用程序足够小, 这不是一个必选项, 当你感觉需要频繁的去操作localStorage的时候, 也许你应该考虑redux-persist了, 现在, 让我们开始来学习它吧

  1. 组合reduces
import { combineReducers } from "@reduxjs/toolkit";
const rootReducer = combineReducers({
  counter: counterReducer,
});
  1. 对reducer进行persist
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, PersistConfig } from "redux-persist";

const persistConfig: PersistConfig<any> = {
  key: "root",
  version: 1,
  storage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);

当然, 如果有需要, 我们也可以对我们的任意一个reducer进行persist配置, 比如counterReducer, 但是目前来说我还没有发现这个功能的使用场景在哪里, 比如

const persistCounterConfig: PersistConfig<any> = {
  key: "counter",
  version: 1,
  storage,
  whiteList: [...]
};
const persistedCounterReducer = persistReducer(persistCounterConfig, counterReducer);

3.创建store和persistStore, 并导出

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }),
});

const persister = persistStore(store);

export default {
  store,
  persister,
};

4.最后, 让我们在root component中去使用它

 <Provider store={store.store}>
  <PersistGate loading={null} persistor={store.persister}>
    <Component {...pageProps} />
  </PersistGate>
</Provider>