Redux基础
Redux+typescript是官方也推荐的方式,此系列也将结合起来介绍。
安装
使用create-react-app
安装并选装redux
+typescript
:
yarn create react-app redux-ts-demo --template redux-typescript
npm init react-app redux-ts-demo --template redux-typescript
npx create-react-app redux-ts-demo --template redux-typescript
初学时可以先忽略工程中原官方案例代码。在src/index.ts
内完成基本内容的探索。
redux的核心
state
、action
、reducer
、dispatch
、Provider
、subscribe
。其中Provider
是react-redux
包中提供的组件,用于连接React组件
和Redux Store
的通信。
reducer
可以理解为事件管理者,dispathch
则是事件通知者,action
是触发的动作,state
是状态的容器。这样看起来,reducer
是一个核心成员,关于如何处理state的逻辑都在这里完成。
import React from 'react';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
type Action = { type: "increment" }
// 1. 定义处理和响应"事件"的管理中心函数
function reducers(state = 0 /** 初始化 state 的值 */, action: Action) {
const {type} = action;
switch (type) {
case "increment":
return state + 1;
case "decrement":
return state -1;
default:
return state;
}
}
// 2. 创建 store
const store = createStore(reducers);
// 3. 获取state状态
console.log(store.getState());
// 4. 订阅 state 的改变
store.subscribe(() => {
render();
})
const App: React.FC = () => {
return (
<>
<p>{store.getState()}</p>
<button onClick={() => store.dispatch({
type: "increment",
})}>increment</button>
<button onClick={() => store.dispatch({
type: "decrement"
})}>decrement</button>
</>
)
}
// 5. 渲染UI
render();
function render() {
ReactDOM.render(
// 根组件连接,并关联 store
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
)
}
此时的组件还不具有自动更新UI的能力,因此需要我们通过render
函数并在subscribe
监听到state
发生变化时手动更新UI。
以上即为Redux
运行的主要过程。
createStore
在创建store时可以传入第二个参数,用于初始化reducers
函数参数1state
的值。当在createStore
中传入初始值后,reducers
内state的初始值就失效了!
拆分Reducers
上面的例子state
只有一个number
的状态值,通常的应用会有多个数据状态需要保存,而在拥有多个全局状态时,把处理逻辑都放到一个reducer
中,那么在工程不断拓展后会使整个文件变得异常庞大使得难以阅读和维护!此时就需要将各个独立的状态操作放到一个单独的reducer
中进行维护,通常会在项目根目录创建一个reducers
目录来进行管理。
创建userReducer:
// src/reducers/user.ts
import { AnyAction } from "redux";
export type UserState = {
username: string,
gender: number,
age: number
}
export type UserAction = {type: "login" ,user: UserState}
const initUserState: UserState = {
username: '',
gender: 0,
age: 0
}
const userReducer = (state: UserState = initUserState, action: AnyAction) => {
const {type, user} = action as UserAction;
switch(type) {
case "login":
return {...state, ...user};
default:
return state;
}
}
export default userReducer;
创建postReducer:
// src/reducers/post.ts
import { AnyAction } from "redux";
export type PostState = {
id: string,
title: string,
content: string
}
export type PostAction = {type: "add_post" , post: PostState}
const initPostState: PostState = {
id: '',
title: '',
content: ''
}
const postReducer = (state: PostState = initPostState, action: AnyAction) => {
const {type, post} = action as PostAction;
switch(type) {
case "add_post":
return {...state, ...post};
default:
return state;
}
}
export default postReducer;
把子reducer合并到一个文件中:
// src/reducers/index.ts
import { post, PostState, PostAction } from "reducers/post";
import { user, UserAction, UserState } from "reducers/user";
import { combineReducers } from "redux";
export type RootAction = UserState | PostAction;
export type RootState = {
user: UserState,
post: PostState
};
const initState: RootState = {
user: {
username: '',
gender: 0,
age: 0
},
post: {
id: '',
title: '',
content: ''
}
};
export default function rootReducers(
state: RootState = initState,
action: RootAction
) {
return {
user: user(state.user, action),
post: post(state.post, action)
};
}
export const combineReducer = combineReducers({
user,
post,
});
在子reducer
中,需导出当前reducer
中state
和action
的类型,以供合并文件reduders
内使用。并且子reducer
函数参数2的action
类型需设置为anyAction然后在使用时将其强转为本reducer
的action的类型即可。
你可能会好奇为何不直接引入RootAction
来作为子reducer action
的类型,原因如下:
为了使
dispatch
时可以明确载荷类型,不想所有的action除了type
外都使用payload
来作为载荷的键名!(如何你想这么做也不是不可以)。如果用RootAction
那么将必须统一action
的类型,比如是:{type: string, payload: any}
将子
reducer
函数参数action
设置为anyAction
后,通过类型强转(as
)可以将action
的类型重新圈定回当前reducer
中,在文件内编写代码时可以得到只包含当前reducer
的类型提示。要是使用RootAction
,那么类型就不再纯粹,将包含其他文件的action
内容。
reducer
初始化细致的你可能已经注意到每个子reducer
函数参数1state
都被赋予了默认值,你是否疑问这是必须的吗?是的,这是必须的!
请为reducer
赋一个初始值! 这非常重要,这在你使用combineReducers
时是必须的!未进行初始化赋值,那么你的state
将有可能是undefined
,redux将会提示你这个错误,告诉你须提供一个初始值:
Error: The slice reducer for key "post" returned undefined during initialization.
If the state passed to the reducer is undefined, you must explicitly return the
initial state. The initial state may not be undefined. If you don't want to set a
value for this reducer, you can use null instead of undefined.
combineReducers
是一个辅助函数,帮助我们简化手动书写整合reducers
的代码,当我们不使用combineReducers
时,可以忽略在子reducer
中初始化state,但也必须给rootReducers
提供一个初始state
。与其在index.ts
中写,不如在子reducer
中直接赋初始值更好,不仅自己管理自己的状态,还可以简化index
中的代码!
每个应用程序只有一个 Redux Store
通常的应用整个组件树都只需一个store
,也仅是在<App />
组件外用<Provider>
进行包裹。建议是不要将createStore
创建出的store
进行导出,多处引入store
将可能导致行为不可预测。为避免错误的发生,可以只在项目src/index.tsx
内使用createStore
。
使用ReduxDevTools监控状态改变
像VueDevTools,在浏览器安装完成后即可监听Vuex的改变。但react+redux则不仅需要react devtools
,还需要redux devtools
协作配合状态的监听:
安装浏览器ReduxDevTools插件;
npm安装redux-devtools-extension
yarn add redux-devtools-extension -D
;
提示
如果不需要一些高级配置的话,直接在创建store时,这样做即可。
const store = createStore(
reducer, /* preloadedState, */
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
写这么长的代码有些繁琐,如果涉及一些配置的话就更麻烦了,此时可以利用安装的redux-devtools-extension
插件提供的函数进行配置。
如果不需要配置中间件(middleware)或增强器(enhancers)的话,可以仅做这样的配置达成浏览器插件的状态监听。
import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension';
const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer({});
如果想要进行代码追踪的话,可以配置成这样:devToolsEnhancer({trace: true})
图片引用自:https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Features/Trace.md
提示
如果使用RTK(Redux ToolsKit)
,已默认进行了一些配置,无需我们再手动配置!详阅RTK DevTools
更多配置请参考: redux-devtools-extension 、Redux DevTools