官方已推荐使用 @reduxjs/toolkit
(地址:https://cn.redux.js.org/introduction/why-rtk-is-redux-today)
使用传统 redux
安装 redux、react-redux、redux-thunk
yarn add redux react-redux redux-thunk
// src/store-redux/index.js
import {createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { thunk } from 'redux-thunk';
import { reducer as page1Reducer } from '@pages/page1/reducer';
const combinedReducerObj = {
page1: combineReducers(page1Reducer)
};
const store = createStore(
combineReducers(combinedReducerObj),
compose(applyMiddleware(thunk)
));
export default store;
// src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store-redux/index';
import Page1 from '@pages/page1/index.jsx';
const App = () => {
return <Provider store={store}>
<Page1 />
</Provider>;
};
export default App;
// src/pages/page1/reducer.js
// 定义 actionType
const ADD_TODO = 'ADD_TODO';
const TODO_TOGGLED = 'TODO_TOGGLED';
const SET_DATA = 'SET_DATA';
// 定义 reducer
const todos = (state = [{ id: 1, text: 'zhangsan', completed: false }], action) => {
switch (action.type) {
case ADD_TODO:
return state.concat({
id: action.payload.id,
text: action.payload.text,
completed: false
});
case TODO_TOGGLED:
return state.map(todo => {
if (todo.id !== action.payload.id) return todo
return {
...todo,
completed: !todo.completed
}
});
default:
return state
}
};
const dataList = (state = [], action) => {
if (action.type === SET_DATA) {
return action.payload;
}
return state;
};
export const reducer = {
todos,
dataList
};
export const actionTypes = {
ADD_TODO,
TODO_TOGGLED,
SET_DATA
};
// src/pages/page1/actions.js
import { actionTypes } from './reducer';
import { getDataService } from './service';
// 同步 action
const addTodo = ({ text }) => {
return {
type: actionTypes.ADD_TODO,
payload: {
text,
id: Math.random()
}
};
};
const todoToggled = ({ id }) => {
return {
type: actionTypes.TODO_TOGGLED,
payload: {
id
}
};
};
const setDataList = data => {
return {
type: actionTypes.SET_DATA,
payload: data
};
};
// 异步 action
const thunkGetDataList = (data) => {
return (dispatch, getState) => {
// dispatch(addTodo({ text: 'lisi'}));
const page1State = getState().page1;
console.log(page1State);
getDataService().then(res => {
dispatch(setDataList(res.data))
});
};
};
export default {
// 同步 action
addTodo,
todoToggled,
// 异步 action
setDataList,
thunkGetDataList
};
// src/pages/page1/service.js
// 模拟接口获取数据
export const getDataService = (params) => {
console.log(params);
return new Promise((resolve, reject) => {
const res= {
data: [
{
id: 1,
name: 'zhangsan',
age: 20
},
{
id: 2,
name: 'lisi',
age: 30
}
]
};
setTimeout(() => {
resolve(res);
}, 3000)
});
};
// src/pages/page1/index.jsx
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import actions from './actions';
const Page1 = ({
todos,
dataList,
addTodo,
getData
}) => {
useEffect(() => {
getData();
}, []);
return <div>
<p>This is App Component.</p>
{
dataList.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id};姓名: {item.name};年龄:{item.age}</p>
</div>
);
})
}
<p>-------------------------------------------</p>
{
todos.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id};text: {item.text};compoleted: {item.completed}</p>
</div>
);
})
}
<button onClick={addTodo}>添加</button>
</div>;
};
const mapStateToProps = (state, props) => {
// let stateCommon = state.common;
const statePage1 = state.page1;
return {
todos: statePage1.todos,
dataList: statePage1.dataList
};
},
mapDispatchToProps = (dispatch, props) => {
return {
addTodo: () => {
dispatch(actions.addTodo({ id: 4, text: 'zzz' }));
},
getData: () => {
dispatch(actions.thunkGetDataList());
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(React.memo(Page1));
使用 @reduxjs/toolkit
安装 @reduxjs/toolkit、react-redux
yarn add @reduxjs/toolkit react-redux
官方使用方案
创建 store
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import commonReducer from './common/reducer';
import HomeReducer from '@pages/home/reducer';
export const store = configureStore({
// 合并所有的 reducer
reducer: {
common: commonReducer,
home: HomeReducer
},
// 解决 redux 无法序列化 数据时的报错
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
});
export default store.dispatch;
创建 reducerSlice
-
configureStore
创建store
- 在根节点通过
Provider
传入store
-
createSlice
创建reducer
切片:传入name
、初始状态、定义reducer
,生成reducer
和actions
; - 页面上使用:通过
useSlector
获取状态;useDispatch
分发action
// src/page2/reducer.js
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'page2', // 命名空间
initialState: {
counter: 0,
todoList: []
}, // 初始值
reducers: {
counterIncrement: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
// 并不是真正的改变状态值,因为它使用了 Immer 库
// 可以检测到“草稿状态“ 的变化并且基于这些变化生产全新的
// 不可变的状态
state.counter += 1;
},
counterDecrement: state => {
state.counter -= 1;
},
counterIncrementByAmount: (state, action) => {
state.counter += action.payload;
},
todoListAdded(state, action) {
state.todoList.push({
id: action.payload.id,
text: action.payload.text,
completed: false
});
},
todoListToggled(state, action) {
const todoItem = state.todoList.find(todo => todo.id === action.payload);
todoItem.completed = !todoItem.completed;
}
}
});
// 普通 action
export const {
counterDecrement,
counterIncrement,
counterIncrementByAmount,
todoListAdded,
todoListToggled
} = todosSlice.actions;
export const actions = todosSlice.actions;
// reducer
export default todosSlice.reducer;
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import page2Reducer from '@pages/page2/reducer';
export const store = configureStore({
// 传入所有的 reducer, configureStore 会帮我们自动合并 reducer
reducer: {
page2: page2Reducer
}
});
// src/pages/page2/index.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { actions } from './reducer';
const Page2 = () => {
const { counter, todoList } = useSelector(state => state.page2), // 获取状态
dispatch = useDispatch();
return <div>
<p>This is App Component.</p>
<p>计数器:{ counter }</p>
{
todoList.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id}</p>
<p>text: {item.text}</p>
<p>completed: {String(item.completed)}</p>
</div>
);
})
}
<button onClick={() => dispatch(actions.counterIncrement())}>计数器添加</button>
<button onClick={() => dispatch(actions.counterDecrement())}>计数器减少</button>
<button onClick={() => dispatch(actions.counterIncrementByAmount(5))}>计数器每次+5</button>
<button onClick={() => dispatch(actions.todoListAdded({ id: 1, text: 'zhangsan' }))}>添加</button>
<button onClick={() => dispatch(actions.todoListToggled(1))}>切换状态</button>
</div>;
};
export default React.memo(Page2);
处理异步操作 createAsyncThunk
传统 redux
需要使用安装 redux-thunk
。redux toolkit
使用 createAsyncThunk
API
简化异步调用。
// src/pages/page2/asyncActions.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import { actions } from './reducer';
// 模拟接口获取数据
const getDataService = (params) => {
console.log(params);
return new Promise((resolve, reject) => {
const res= {
data: [
{
id: 1,
name: 'zhangsan',
age: 20
},
{
id: 2,
name: 'lisi',
age: 30
}
]
};
setTimeout(() => {
resolve(res);
}, 3000)
});
};
// 定义异步 action
export const asyncGetData = createAsyncThunk('page2/fetchPosts', async (payload, { dispatch, getState }) => {
// 通过 payload 可以接收数据
const { params } = payload;
// 通过 getState 可以获取 store
const { counter } = getState().page2;
console.log(counter);
// 通过 dispatch 可以分发 action
// 在 createAsyncThunk 可以 dispatch 普通的 action
dispatch(actions.counterIncrement());
// 在 createAsyncThunk 也可以 dispatch 异步 action
dispatch(asyncSetLocale({ locale: 'pl' }));
const response = await getDataService(params);
// 在这里 return 的数据,需要在 createSlice 的 extraReducers 里进行处理,更新 state
return {
dataList: response.data
};
});
export const asyncSetLocale = createAsyncThunk('page2/setLocael', async (payload, { dispatch, getState }) => {
return {
locale: payload.locale,
menuList: [{
id: 11,
menuName: '电站'
}]
};
});
// 导出异步 actions
const asyncActions = {
asyncGetData,
asyncSetLocale
};
export default asyncActions;
// src/pages/page2/reducer.js
import { createSlice } from '@reduxjs/toolkit';
import asyncActions from './asyncAtions';
const todosSlice = createSlice({
name: 'page2', // 命名空间
initialState: {
counter: 0,
todoList: [],
dataList: [], // 接口返回的数据
locale: 'en',
menuList: []
}, // 初始值
reducers: {
counterIncrement: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
// 并不是真正的改变状态值,因为它使用了 Immer 库
// 可以检测到“草稿状态“ 的变化并且基于这些变化生产全新的
// 不可变的状态
state.counter += 1;
},
counterDecrement: state => {
state.counter -= 1;
},
},
extraReducers: builder => {
builder.addCase(asyncActions.asyncGetData.fulfilled, (state, action) => {
// 接收 createAsyncThunk 中 return 的数据
const { dataList } = action.payload;
// 设置 state
state.dataList = dataList;
});
builder.addCase(asyncActions.asyncSetLocale.fulfilled, (state, action) => {
const {
locale,
menuList
} = action.payload;
state.locale = locale;
state.menuList = menuList;
});
}
});
// 普通 action
export const {
counterDecrement,
counterIncrement,
} = todosSlice.actions;
export const actions = todosSlice.actions;
// reducer
export default todosSlice.reducer;
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import asyncActions from './asyncAtions';
const Home = () => {
const { counter, todoList, locale, menuList, dataList } = useSelector(state => state.page2), // 获取状态
dispatch = useDispatch();
useEffect(() => {
// 调用异步 action
dispatch(asyncActions.asyncGetData({ query: '参数1' }));
}, []);
return <div>
<p>This is App Component.</p>
{/* <p>计数器:{ counter }</p> */}
<p>当前语言: { locale }</p>
{
dataList.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id};姓名: {item.name};年龄:{item.age}</p>
</div>
);
})
}
<p>-------------------------------------------</p>
{
menuList.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id};菜单: {item.menuName};</p>
</div>
);
})
}
</div>;
};
export default React.memo(Home);
简化 reducerSlice 和 createAsyncThunk
简化 createAsyncThunk
由于 createAsyncThunk
内部就可以直接 dispatch
一个普通的 action
,就可以直接在这里面通过 dispatch(actions.setXxx('zhangsan'))
的方式修改 state
,不需要 return
数据,这样就不用在 createSlice
中编写 extraReducers
。
// src/pages/page2/asyncActions.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import { actions } from './reducer';
// 模拟接口获取数据
const getDataService = (params) => {
console.log(params);
return new Promise((resolve, reject) => {
const res= {
data: [
{
id: 1,
name: 'zhangsan',
age: 20
},
{
id: 2,
name: 'lisi',
age: 30
}
]
};
setTimeout(() => {
resolve(res);
}, 3000)
});
};
// 定义异步 action
export const asyncGetData = createAsyncThunk('page2/fetchPosts', async (payload, { dispatch, getState }) => {
// 通过 payload 可以接收数据
const { params } = payload;
// 通过 getState 可以获取 store
const { counter } = getState().page2;
console.log(counter);
// 通过 dispatch 可以分发 action
// 在 createAsyncThunk 可以 dispatch 普通的 action
dispatch(actions.counterIncrement());
// 在 createAsyncThunk 也可以 dispatch 异步 action
dispatch(asyncSetLocale({ locale: 'pl' }));
const response = await getDataService(params);
// 在这里 return 的数据,需要在 createSlice 的 extraReducers 里进行处理,更新 state
// return {
// dataList: response.data
// };
// 直接在这里 setState
dispatch(actions.setDataList(response.data));
});
export const asyncSetLocale = createAsyncThunk('page2/setLocael', async (payload, { dispatch, getState }) => {
// return {
// locale: payload.locale,
// menuList: [{
// id: 11,
// menuName: '电站'
// }]
// };
// 直接在这里 setState
dispatch(actions.setLocale(payload.locale));
dispatch(actions.setMenuList([{ id: 11, menuName: '电站' }]));
});
// 导出异步 actions
const asyncActions = {
asyncGetData,
asyncSetLocale
};
export default asyncActions;
// src/pages/page2/reducer.js
import { createSlice } from '@reduxjs/toolkit';
import asyncActions from './asyncAtions';
const todosSlice = createSlice({
name: 'page2', // 命名空间
initialState: {
counter: 0,
todoList: [],
dataList: [], // 接口返回的数据
locale: 'en',
menuList: []
}, // 初始值
reducers: {
counterIncrement: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
// 并不是真正的改变状态值,因为它使用了 Immer 库
// 可以检测到“草稿状态“ 的变化并且基于这些变化生产全新的
// 不可变的状态
state.counter += 1;
},
counterDecrement: state => {
state.counter -= 1;
},
setDataList(state, action) {
state.dataList = action.payload;
},
setLocale(state, action) {
state.locale = action.payload;
},
setMenuList(state, action) {
state.menuList = action.payload;
}
},
// extraReducers: builder => {
// builder.addCase(asyncActions.asyncGetData.fulfilled, (state, action) => {
// // 接收 createAsyncThunk 中 return 的数据
// const { dataList } = action.payload;
//
// // 设置 state
// state.dataList = dataList;
// });
//
// builder.addCase(asyncActions.asyncSetLocale.fulfilled, (state, action) => {
// const {
// locale,
// menuList
// } = action.payload;
//
// state.locale = locale;
// state.menuList = menuList;
// });
// }
});
export const actions = todosSlice.actions;
// reducer
export default todosSlice.reducer;
// src/pages/page2/index.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import asyncActions from './asyncAtions';
const Home = () => {
const { counter, todoList, locale, menuList, dataList } = useSelector(state => state.page2), // 获取状态
dispatch = useDispatch();
useEffect(() => {
dispatch(asyncActions.asyncGetData({ query: '参数1' }));
}, []);
return <div>
<p>This is App Component.</p>
{/* <p>计数器:{ counter }</p> */}
<p>当前语言: { locale }</p>
{
dataList.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id};姓名: {item.name};年龄:{item.age}</p>
</div>
);
})
}
<p>-------------------------------------------</p>
{
menuList.map((item, index) => {
return (
<div key={item.id}>
<p>id: {item.id};菜单: {item.menuName};</p>
</div>
);
})
}
</div>;
};
export default React.memo(Home);
如果你还是想在 cerateAsyncThunk 中通过 return 得形式返回状态。那么可以优化 extraReducers 的写法,前提是 return 一个对象。
// src/store/createExtraReducers.js
export const createExtraReducers = (actions = {}) => {
const extraReducers = {};
for (const action in actions) {
if (actions.hasOwnProperty(action)) {
// action 的异步任务执行完成才修改 state
extraReducers[actions[action].fulfilled] = (state, { payload }) => {
for (const key in payload) {
if (Object.hasOwnProperty.call(payload, key)) {
if (key !== 'callback') {
state[key] = payload[key];
} else {
payload.callback && payload.callback();
}
}
}
};
}
}
return extraReducers;
};
// src/pages/page2/reducer.js
import { createSlice } from '@reduxjs/toolkit';
import asyncActions from './asyncAtions';
import { createExtraReducers } from '@store/createExtraReducers.js';
const extraReducers = createExtraReducers(asyncActions);
const todosSlice = createSlice({
name: 'page2', // 命名空间
initialState: {
counter: 0,
todoList: [],
dataList: [], // 接口返回的数据
locale: 'en',
menuList: []
}, // 初始值
reducers: {
counterIncrement: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
// 并不是真正的改变状态值,因为它使用了 Immer 库
// 可以检测到“草稿状态“ 的变化并且基于这些变化生产全新的
// 不可变的状态
state.counter += 1;
},
counterDecrement: state => {
state.counter -= 1;
},
setDataList(state, action) {
state.dataList = action.payload;
},
setLocale(state, action) {
state.locale = action.payload;
},
setMenuList(state, action) {
state.menuList = action.payload;
}
},
extraReducers: builder => {
for (const actionCase in extraReducers) {
if (extraReducers.hasOwnProperty(actionCase)) {
builder.addCase(actionCase, (state, action) =>
extraReducers[actionCase](state, action)
);
}
}
}
});
export const actions = todosSlice.actions;
// reducer
export default todosSlice.reducer;
import { createAsyncThunk } from '@reduxjs/toolkit';
import { actions } from './reducer';
// 模拟接口获取数据
const getDataService = (params) => {
console.log(params);
return new Promise((resolve, reject) => {
const res= {
data: [
{
id: 1,
name: 'zhangsan',
age: 20
},
{
id: 2,
name: 'lisi',
age: 30
}
]
};
setTimeout(() => {
resolve(res);
}, 3000)
});
};
// 定义异步 action
export const asyncGetData = createAsyncThunk('page2/fetchPosts', async (payload, { dispatch, getState }) => {
// 通过 payload 可以接收数据
const { params } = payload;
// 通过 getState 可以获取 store
const { counter } = getState().page2;
console.log(counter);
// 通过 dispatch 可以分发 action
// 在 createAsyncThunk 可以 dispatch 普通的 action
dispatch(actions.counterIncrement());
// 在 createAsyncThunk 也可以 dispatch 异步 action
dispatch(asyncSetLocale({ locale: 'pl' }));
const response = await getDataService(params);
// 在这里 return 的数据,需要在 createSlice 的 extraReducers 里进行处理,更新 state
return {
dataList: response.data
};
});
export const asyncSetLocale = createAsyncThunk('page2/setLocael', async (payload, { dispatch, getState }) => {
return {
locale: payload.locale,
menuList: [{
id: 11,
menuName: '电站'
}]
};
});
// 导出异步 actions
const asyncActions = {
asyncGetData,
asyncSetLocale
};
export default asyncActions;
简化 createSlice
由于需要给每个 state 定义至少一个修改 state 的 reducer,为了简化这一步,其实每个 state 只需要定义一个 setState 方法就可以了,处理数据可以放在外部去处理,我们只要传递 newState 即可。
比如定一个 name
,修改它直接通过 dispatch(actions.setName('zhangsan'))
。
这样就可以封装一个统一的方法,来帮我们自动生成 state 对应的 setState,就不需要每个模块去单独写一遍,提升开发效率。
// src/store/createReducerSlice.js
// 工具方法
import { createSlice, createAction } from '@reduxjs/toolkit';
// 为每个 state 创建一个对应的 setState 方法,
// 如果 state 是 name,那么修改 name 则通过 setName 进行修改
// dispatch(simpleActions.setName('zhangsan'))
const createSetState = initialState => {
const reducers = {};
for (const key in initialState) {
if (initialState.hasOwnProperty(key)) {
const keyName = 'set' + key.substring(0, 1).toUpperCase() + key.substring(1);
reducers[keyName] = (state, { payload }) => {
state[key] = payload;
};
}
}
return reducers;
},
createReducerSlice = ({
pageIdentify, // 页面 id
initialState = {}, // 定义 state 初始值
reducers = {}, // 可选项,自定义 reducer。其他更复杂的操作,比如对数组的添加/删除,如果不想在外部处理,那就在这里定义
extraReducers = {} // 可选项,如果 createAsyncThunk 中 return 了需要修改的 state,那么需要传递 extraReducers,统一修改 state;
}) => {
const updateState = createAction('updateState'),
reducerSlice = createSlice({
name: pageIdentify,
initialState: initialState,
// 简单reducer
reducers: {
// 简单 reducer: 一次只能修改一个状态
...createSetState(initialState),
// 其他更复杂的操作,比如对数组的添加/删除,如果不想在外部处理,那就在这里定义
...reducers
},
extraReducers: builder => {
// 复杂 reducer 一次修改多个 state
builder.addCase(updateState, (state, { payload }) => {
for (const stateKey in payload) {
if (payload.hasOwnProperty(stateKey)) {
state[stateKey] = payload[stateKey];
}
}
});
for (const actionCase in extraReducers) {
if (extraReducers.hasOwnProperty(actionCase)) {
builder.addCase(actionCase, (state, action) =>
extraReducers[actionCase](state, action)
);
}
}
}
});
// 自定义 caseReducer:通过 dispatch(caseReducer.updateState({ name: 'zhangsan', age: 20 })) 可一次修改多个 state
reducerSlice.caseReducer = {
updateState
};
return reducerSlice;
};
export default createReducerSlice;
// src/pages/page2/asyncActions.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import { actions } from './reducer';
// 模拟接口获取数据
const getDataService = (params) => {
console.log(params);
return new Promise((resolve, reject) => {
const res= {
data: [
{
id: 1,
name: 'zhangsan',
age: 20
},
{
id: 2,
name: 'lisi',
age: 30
}
]
};
setTimeout(() => {
resolve(res);
}, 3000)
});
};
// 定义异步 action
export const asyncGetData = createAsyncThunk('page2/fetchPosts', async (payload, { dispatch, getState }) => {
// 通过 payload 可以接收数据
const { params } = payload;
dispatch(asyncSetLocale({ locale: 'pl' }));
const response = await getDataService(params);
dispatch(actions.setDataList(response.data));
});
export const asyncSetLocale = createAsyncThunk('page2/setLocael', async (payload, { dispatch, getState }) => {
dispatch(actions.setLocale(payload.locale));
dispatch(actions.setMenuList([{ id: 11, menuName: '电站' }]));
});
// 导出异步 actions
const asyncActions = {
asyncGetData,
asyncSetLocale
};
export default asyncActions;
// src/pages/page2/reducer.js
// import asyncActions from './asyncAtions';
// import { createExtraReducers } from '@store/createExtraReducers.js';
import createReducerSlice from '@store/createReducerSlice.js';
// const extraReducers = createExtraReducers(asyncActions);
// 使用二次封装的 createReducerSlice
const reducerSlice = createReducerSlice({
pageIdentify: 'page2',
initialState: {
counter: 0,
todoList: [],
dataList: [], // 接口返回的数据
locale: 'en',
menuList: []
}, // 初始值
// extraReducers
});
// 普通 actions
export const actions = reducerSlice.actions;
export const caseReducer = reducerSlice.caseReducer;
// reducer
export default reducerSlice.reducer;