Basic Usage of redux-toolkit in React


redux-toolkit 在 react 中的基础用法

本文通过几个例子来帮助初学者快速入门 redux-toolkit 在 react 的应用,不涉及异步 action 以及与服务器的交互,不解释 redux 的原理和用法,读者应当对 react 和 redux 有一定了解。

quickstart

先来看一个简单的案例,这是redux toolkit document中的例程。

首先来看最终在 react 中使用 redux 的部分。

// features/counter/Counter.tsx

import React from 'react'
import { RootState } from '../../app/store'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'

export function Counter() {
  const count = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

这一段代码实现了一个计数器组件。该组件有两个按钮,一个做加法一个做减法。另外还有一组用来显示数据的<span>

光看 return 部分,最重要的无非就是count以及dispatch。这两个都在前面有定义。其中count=…value,是一个获取计数器的数据的函数,dispatchuseDispatch的结果。dispatch的参数是一个函数的返回值,那么这个函数显然是 action creator。

注意用 useSelector 获取数据只有在触发 action -> reducer 这一通操作时才会更新。如果只用这个函数获取数据的话,所有修改数据的操作都必须经过这一流程。

现在的问题是 action creator 从哪里来。看引入语句,来到features/counter/counterSlice.ts

// features/counter/counterSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;

看到最后的导出语句,这些 action creators 来自counterSlice.actions。那么这个值是从哪里来的呢。其实是自动生成的。看到createSlice这部分,传入的参数包括 Slice 的名字,state 的初始值,reducers。这些 action creators 就是根据传入的 reducers 自动生成的。同时,看这些 reducers,可以发现它们不再是纯函数,而可以在内部修改传入参数的值。

那么 action creators 的来历搞清楚了,再看到最后导出的counterSlice.reducer,看看它去哪了。

// app/store.ts

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

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

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

这是生成 store 的代码,刚才导出的 reducers 在这里被configureStore接收,字段名是刚才给counterSlice起的名字counter

案例没有给出的最后一步就是Provider。这里有个例子可以看一下。welcome 是一个页面,login 是该页面中的一个组件。这里的 welcomeStore 就是上文的 store(一般来说整个程序只需要一个 store,不同的部分可以用 不同的 slice 划分)。

import { Provider } from "react-redux";
import { Login } from "../components/welcome/Login";
import { welcomeStore } from "../stores/welcome";

export default function Welcome() {
  return (
    <Provider store={welcomeStore}>
      <Login />
    </Provider>
  );
}

至此,所有的流程走完了,比起不用工具,省略了最恶心的定义 actionsTypes、actions、action creators 的过程(由于使用 typescript,需要对该部分进行类型限制,会产生大量代码)和同样麻烦的配置容器组件的过程。

注意useSelectoruseDispatch只能用在函数式组件中,如果非要用到 class 组件,那么只能像下面这样写。但显然有点麻烦,非必要就算了。

class App extends Component {
    constructor(props){
        super(props)
        this.state = {
            reduxState : {}
        }
    }

    DummyView = () => {
        const reducer = useSelector(state => state.reducer)
        useEffect(() => {
            this.setState({
                reduxState : reducer
            })
        }, [])
        return null
    }

    render(){
        return(
            <this.DummyView/>
        )
    }
}

reducer 传参

查看刚才写的 reducer,发现已经有一个参数state。但这显然是不够的,比如有一个输入框,现在希望将输入的内容显示在框内。首先显示的部分很容易,只要把 state 中的东西写到 InputText 组件的 value 中即可。但是如何将输入的内容存到 store 中呢。这需要 reducer 有第二个参数。来看以下案例。

export const loginSlice = createSlice({
  name: "login",
  initialState,
  reducers: {
    login: (state) => {
      if (state.username == "user" && state.password == "password") {
        state.authentication = true;
      } else {
        state.authentication = false;
      }
    },
    setUsername: (state, action) => {
      state.username = action.payload;
    },
    setPassword: (state, action) => {
      state.password = action.payload;
    },
  },
});


<TextInput
  value={loginState.username}
  onChangeText={(value) => dispatch(setUsername(value))}
/>

首先,reducer 的第二个参数就叫 action,action 的字段就取 payload,不要改就行了。用法也很简单,传进去什么就是什么。具体可见最后的组件部分代码。

类型安全

quickstart 中的案例没有进行严格的类型限制,下面再通过一个案例来看使用 typescript 时的正常写法。

该案例是一个 react native 项目,功能是实现一个简单的登陆功能,项目结构如下。

stores/
  page1.ts
pages/
  page1.tsx
  page1.scss
hooks/
  page1.ts
components/
  page1/
    component1.tsx
    component1Redux.ts
    component1.scss

这不是完整的目录结构,没用到的暂时没加

stores/Welcome.ts没有改变。

import { configureStore } from "@reduxjs/toolkit";
import loginReducer from "../components/welcome/LoginRedux";

export const welcomeStore = configureStore({
  reducer: {
    login: loginReducer,
  },
});

export type WelcomeState = ReturnType<typeof welcomeStore.getState>;
export type WelcomeDispatch = typeof welcomeStore.dispatch;

hooks/Welcome.ts是新增的内容,这里给useDispatchuseWelcomeSelector添加了类型限定。

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";

import type { WelcomeState, WelcomeDispatch } from "../stores/welcome";

export const useWelcomeDispatch = () => useDispatch<WelcomeDispatch>();
export const useWelcomeSelector: TypedUseSelectorHook<WelcomeState> =
  useSelector;

components/welcome/LoginRedux.ts中对initialState的类型定义方式做了改变,且增加了对 reducer 第二个参数的类型限定。

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

interface LoginState {
  username: string;
  password: string;
  authentication: boolean;
}

const initialState = {
  username: "",
  password: "",
  authentication: false,
} as LoginState; // 根据文档,改成这样是为了避免typescript无必要地收紧initialState的类型

export const loginSlice = createSlice({
  name: "login",
  initialState,
  reducers: {
    login: (state) => {
      if (state.username == "user" && state.password == "password") {
        state.authentication = true;
      } else {
        state.authentication = false;
      }
    },
    // 这里对payload的类型进行了限定
    setUsername: (state, action: PayloadAction<string>) => {
      state.username = action.payload;
    },
    setPassword: (state, action: PayloadAction<string>) => {
      state.password = action.payload;
    },
  },
});

export const { login, setUsername, setPassword } = loginSlice.actions;

export default loginSlice.reducer;

components/login.tsx如下所示。注意此时用的是 hooks 导出的两个函数。

import { TextInput, Button, View, Text } from "react-native";
import { useWelcomeDispatch, useWelcomeSelector } from "../../hooks/Welcome";
import { login, setUsername, setPassword } from "./LoginRedux";

export function Login() {
  const loginState = useWelcomeSelector((state) => state.login);

  const dispatch = useWelcomeDispatch();

  return (
    <View>
      <Text>Username: </Text>
      <TextInput
        value={loginState.username}
        onChangeText={(value) => dispatch(setUsername(value))}
      />
      <Text>Password: </Text>
      <TextInput
        value={loginState.password}
        onChangeText={(value) => dispatch(setPassword(value))}
      />
      <Button title="login" onPress={() => dispatch(login())} />
      <Text>{loginState.authentication ? "success" : "fail"}</Text>
    </View>
  );
}

pages/welcome.tsx如下,把Provider加上即可。

import { Provider } from "react-redux";
import { Login } from "../components/welcome/Login";
import { welcomeStore } from "../stores/Welcome";

export default function Welcome() {
  return (
    <Provider store={welcomeStore}>
      <Login />
    </Provider>
  );
}

文章作者: niuiic
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 niuiic !
评论
  目录