更多分享内容可访问我的个人博客
本文通过几个例子来帮助初学者快速入门 redux-toolkit 在 react 的应用,不涉及异步 action 以及与服务器的交互,不解释 redux 的原理和用法,读者应当对 react 和 redux 有一定了解。
先来看一个简单的案例,这是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
,是一个获取计数器的数据的函数,dispatch
是useDispatch
的结果。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,需要对该部分进行类型限制,会产生大量代码)和同样麻烦的配置容器组件的过程。
注意useSelector
和useDispatch
只能用在函数式组件中,如果非要用到 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,发现已经有一个参数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
是新增的内容,这里给useDispatch
和useWelcomeSelector
添加了类型限定。
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>
);
}