翻译官方文档:Style Guide | Redux 中文官网,并做一些简单解读
- 必须遵守(Essential)
Do Not Mutate State
翻译:不要直接修改Redux State
说明:这个大家都能达成共识,但可能会无意修改,造成不可预估的BUG,分享最近解决的一个BUG
// badcase
function init() {
const storeData = store.getState();
// 这里将redux的状态赋给实例变量
this.moduleDetail = storeData.moduleDetail;
// 在某个地方,可能会通过 this.moduleDetail.xxx = 'xxx' 修改变量,本以为是安全的,须不知间接修改了Redux State
}
// goodcase
function init() {
const storeData = store.getState();
// 正确的做法拷贝一份,moduleDetail对象不含深层次的嵌套,所以不用深拷贝
this.moduleDetail = { ...storeData.moduleDetail };
}
Reducers Must Not Have Side Effects
翻译:Reducers不要有任何异步逻辑,如 AJAX calls, timeouts, promises
说明:这个很好理解,也不容易出错
Do Not Put Non-Serializable Values in State or Actions
翻译:state 或 action对象不含不可序列化的数据
说明:避免使用不可序列化的数据如 Promises, Symbols, Maps/Sets, functions, or class instances .简单理解就是只存那些能序列化/反序列化的数据
Only One Redux Store Per App
翻译:单一Store
说明:一个应用只有一个全局的Store对象
- 强烈推荐(Strongly Recommended)
Use Redux Toolkit for Writing Redux Logic
翻译:使用Redux Toolkit工具包简化Redux逻辑
说明:toolkit能够简化写法,不用写action type,action和reducers合二为一,一个slice文件就可以了
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
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) => {
state.value += action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
Use Immer for Writing Immutable Updates
翻译:使用Immer库确保Redux State不被修改
说明:Reduc Toolkit已经内置Immer,在reduder可以随意修改state,不需要解构
// badcase just use redux
export default handleActions({
// 需要手动处理不可变state,写法很不优雅,且容易忘记
[`${types.SET_USER_ACTION}`]: (state, action) => ({
...state,
...action.payload
}),
[`${types.RESET_USER_ACTION}`]: (state, action) => ({
...state,
...action.payload
})
}, initialState);
// goodcase use redux toolkit
reducers: {
setPagination: (state, action) => {
// 可以直接修改state,这一点很香。。。
state.pagination = action.payload;
},
setFilters: (state, action) => {
state.filters = action.payload;
},
setCommonState: (state, action) => {
Object.assign(state, action.payload);
},
},
Structure Files as Feature Folders with Single-File Logic
翻译:将redux action/reducers 代码放在业务组件目录下
说明:集中式管理redux state有利有弊,集中管理违背了Redux的组件化思想,项目现在是集中式管理的,维护比较麻烦,导致很多逻辑都不会放在redux,比如网络请求
// 官方推荐目录结构
src
-- store
---- index.js // combine所有的reducers
-- pages
---- UserManagement // Slice放在页面组件下,将相关逻辑放在一起,而不是统一管理
------ userSlice.js // export reducer and actions
// store/index.js
import userSlice from "../pages/UserManagement/userSlice";
const reducers = combineReducers({
theme,
project,
userGroup: userGroupReducer,
user: userSlice,
});
Put as Much Logic as Possible in Reducers
翻译:将更多逻辑放在reducers
说明:redux的reducer不是一个无情的setState函数,它还可以做很多事情,将更多的逻辑放在reducer,可以简化视图层逻辑,状态修改也不容易出错
// badcase, reducer仅提供setter逻辑, 逻辑放在业务组件处理
const onTodoClicked = id => {
const newTodos = todos.map(todo => {
if (todo.id !== id) return todo
return { ...todo, completed: !todo.completed }
})
dispatch({ type: 'todos/toggleTodo', payload: { todos: newTodos } })
}
case "todos/toggleTodo":
return action.payload.todos;
// goodcase,业务层仅dispath一个事件,逻辑放在reducers处理
const onTodoClicked = (id) => {
dispatch({type: "todos/toggleTodo", payload: {id}})
}
case "todos/toggleTodo": {
return state.map(todo => {
if(todo.id !== action.payload.id) return todo;
return {...todo, completed: !todo.completed };
})
}
扩展阅读:将逻辑放在reducers处理有什么好处?
Reducers Should Own the State Shape
翻译:Reducer需返回可控的State
说明:简单来说,就是state的属性字段是明确的,返回的state不能增加或减少字段,避免直接 return action.payload 或 return {...state, ...action.payload},这样容易出错
const initialState = {
firstName: null,
lastName: null,
age: null,
};
export default usersReducer = (state = initialState, action) {
switch(action.type) {
// new state理论上需要包含3个字段 firstName/lastName/age
// 但这里直接返回payload,我们无法保证payload包含以上三个字段
// 一旦出错,则很多地方的逻辑都会有问题
case "users/userLoggedIn": {
return action.payload;
}
default: return state;
}
}
// error dispatch, 比如这里dispatch了另一个对象
// PS,用TypeScript类型校验能很好的规避这个问题
dispatch({
type: 'users/userLoggedIn',
payload: {
id: 42,
text: 'Buy milk'
}
})
Name State Slices Based On the Stored Data
翻译:根据业务领域名称命名Slice文件名
说明:命名规范,domain name 不要使用reduer或slice后缀
// badcase
const rootReducer = combineReducers({
usersReducer,
postsReducer
})
// goodcase
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer
})
Organize State Structure Based on Data Types, Not Components
翻译:状态树按domain模块拆分,而不是组件拆分
说明:简单来说,就是按照后端的表结构来设计,比如用户表、项目表的名称,而不是业务组件名称
// badcase,一个userAction,没有意义
import userAction from './homepage/reducer';
const reducers = combineReducers({
userAction
});
// goodcase,project是一个业务名称
const reducers = combineReducers({
project: userAction,
});
Treat Reducers as State Machines
翻译:用状态机驱动reducers
说明:根据当前状态和action计算出下一个状态,比如当一个请求正在进行中时不允许再发起请求(或者要先取消当前请求),比如训练状态已完成的状态不能再变为进行中,比如按钮的防止重复点击。
PS:在状态比较复杂的场景,使用Redux State来驱动状态变更特别有用
const IDLE_STATUS = 'idle';
const LOADING_STATUS = 'loading';
const SUCCESS_STATUS = 'success';
const FAILURE_STATUS = 'failure';
const fetchIdleUserReducer = (state, action) => {
// state.status is "idle"
switch (action.type) {
case FETCH_USER:
return {
...state,
status: LOADING_STATUS // 驱动下一个状态
}
}
default:
return state;
}
}
const fetchUserReducer = (state, action) => {
switch (state.status) {
case IDLE_STATUS:
return fetchIdleUserReducer(state, action);
case LOADING_STATUS: // 如果当前是loading状态,则action不会发生任何变化
return fetchLoadingUserReducer(state, action);
case SUCCESS_STATUS:
return fetchSuccessUserReducer(state, action);
case FAILURE_STATUS:
return fetchFailureUserReducer(state, action);
default:
// this should never be reached
return state;
}
}
Normalize Complex Nested/Relational State
翻译:数据结构不要嵌套太深,扁平化处理
说明:这里说的就是State 范式化, State 范式化 | Redux 中文官网,我们项目中Redux中有些数据是冗余的,导致很难维护,也很难确保数据的一致性.。
PS:完全范式化处理有利有弊,在实际业务场景中,我们应尽量往范式化思路去设计我们的State,但如果是简单的场景,比如不需要根据id查找,那么直接存整个List数组是没问题的
// badcase,保存整个parent对象
moduleDetail: {
id: '2833',
type: 'xxx',
parentModuleId: '2832',
name: 'xxx',
status: 'xxx',
parent: {
id: '2832',
type: 'root',
parentModuleId: '0',
name: 'root',
status: 'xxx',
},
},
// goodcase,.只保存parentID,需要用到时通过索引找到parent对象
moduleDetail: {
id: '2833',
type: 'xxx',
parentModuleId: '2832',
name: 'xxx',
parentID: '2832',
status: 'xxx',
},
moduleList: [
//...
]
// good good case , Normalize,进一步,范式化处理
posts : {
byId : {
"2833" : {
id : "2833",
type: 'ocr',
parentModuleId: '2832',
name: 'OCR1',
},
"2832" : {
id: '2832',
type: 'root',
parentModuleId: '0',
name: 'root',
status: 'NO_INPUT',
trained: false
}
}
allIds : ["2833", "2832"]
selectedModuleId: '2833'
},
Model Actions as Events, Not Setters
翻译:action名称应该是事件,而不是具体动作
说明:描述发生了什么,而不是要怎么做,不过用了redux toolkit后,也不用太纠结事件名称了
// badcase,pure setters
{
type: "orders/setPizzasOrdered",
payload: {
amount: getState().orders.pizza + 1,
}
}
{
type: "orders/setCokesOrdered",
payload: {
amount: getState().orders.coke + 1,
}
}
// good case,dispatch 一个order add 事件,数据放在payload
{ type: "food/orderAdded", payload: {pizza: 1, coke: 1} }
Write Meaningful Action Names
翻译:定义有意义的action name
说明:Actions should be written with meaningful, informative, descriptive type fields
// badcase
"SET_DATA" or "UPDATE_STORE"
Allow Many Reducers to Respond to the Same Action
翻译:允许同一个事件在多个reducer处理
说明:一般不会这么用,但RESET事件可以这么做,比如但退出登录的时候,需要清空Redux状态,那么就可以抛一个Reset事件,相关的reducers收到这个事件后返回initailState
Avoid Dispatching Many Actions Sequentially
翻译:避免一次性dispatch多个action
说明:如有这种需求,建议通过batch函数来处理,能够确保只re-render一次,而不是多次
import { batch } from 'react-redux'
function myThunk() {
return (dispatch, getState) => {
// should only result in one combined re-render, not two
batch(() => {
dispatch(increment())
dispatch(increment())
})
}
}
Evaluate Where Each Piece of State Should Live
翻译:合理评估每一个状态应该放在哪里(这是很难界定的)
说明:状态是应该放在redux还是local,什么时候需要将state放在redux? 这里给出一些建议
- Do other parts of the application care about this data?(有模块间共享的需求)
- Do you need to be able to create further derived data based on this original data?(有数据派生的需求)
- Is the same data being used to drive multiple components?(数据需要组件间共享)
- Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?(时间旅行,用于调试就可以了)
- Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?(数据缓存,性能优化)
- Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?(热更新时恢复状态)
Use the React-Redux Hooks API
翻译:使用 redux-redux hooks
说明:Hooks用 useSelector / useDispatch, Component 用 connect
const dispatch = useDispatch();
const { dataSources, isTableLoading, total, pagination } = useSelector(selectUser);
Connect More Components to Read Data from the Store
翻译:在子组件读取Redux Stats,而不是通过父组件传递给子组件
说明:本质上是为了较少渲染,提升性能
// badcase,把子组件当成一个纯组件看待
const UserList = () => {
const users = useSelector( state => state.user.userList);
return users.map(item => {
return
});
}
// goodcase,父组件不关心整个对象,而是直接将id传给子组件
const UserList = () => {
const userIds = useSelector( state => state.user.allIds);
return userIds.map(id => {
return
});
}
// 子组件再通过useSelector获取具体的user对象
const UserItem = ({userId}) => {
const user = useSelector(state => state.user.allUsers[userId]);
// ...
}
Use the Object Shorthand Form of mapDispatch with connect
翻译:使用connect时,mapDispatchToProps传一个对象而不是函数
说明:React-Redux会自动将dispatch注入ActionCreators
// badcase,实际上没必要
const mapDispatchToProps = dispatch => ({
setImageStatus: arg => dispatch(setImageStatus(arg)),
setUploadNewLabel: arg => dispatch(setUploadNewLabel(arg))
});
// goodcase
const mapDispatchToProps = {
setImageStatus,
setUploadNewLabel
);
Call useSelector Multiple Times in Function Components
翻译:通过useSelector获取精确的值,而不是一个大的对象
说明:只获取组件需要用到的值,可以减少渲染,(当然,如果需要获取一个大对象的大部分的值,则还是建议直接使用大对象)
// badcase,user下可能含有多个属性,如果其他属性变化了,那么这个组件也会re-render,而这是没必要的
const UserList = () => {
const { userList: users } = useSelector( state => state.user);
return users.map(item => {
return
});
}
// goodcase,只用到users对象,这样但state.user的其他属性发生变化时,这个组件是不会re-render的
const UserList = () => {
const users = useSelector( state => state.user.userList);
return users.map(item => {
return
});
}
Use Static Typing
翻译:使用类型推断,如TypeScript 或 Flow
说明:React Toolkit完全使用TypeScript写的,使用它能够更好地做到类型安全,并且只需很少的类型声明,Usage With TypeScript | Redux 中文官网
Use the Redux DevTools Extension for Debugging
翻译:使用Redux DevTools Extension调试Redux应用
说明:DevTools有以下特点(优势)
- The history log of dispatched actions(actions历史,方便回溯)
- The contents of each action(action内容,代替console.log、debugger)
- The final state after an action was dispatched(state状态,了解整个应用的状态)
- The diff in the state after an action(prevState 和 currentState的 diff,有助于判断符合预期)
- The function stack trace showing the code where the action was actually dispatched(action堆栈,方便快速定位代码)
Use Plain JavaScript Objects for State
翻译:用纯对象作为state
说明:简单来说,就是用Immer.js 代替 Immutable.js,而Immer是Redux Toolkit内置的,对我们是无感知的(immer.js:也许更适合你的immutable js库 - 知乎)
- 推荐(Recommended)
Write Action Types as domain/eventName
翻译:action type的命名规范:domain/eventName
说明:domain是一个领域模型的名称,而不是组件的名称,现阶段,可以类比一张表;eventName是事件名称,不需要用大写常量来定义
// badcase,没必要用常量来定义
dispatch({type: 'ADD_TODO' }); /// eventName是大写常量,可读性较差
// goodcase
dispatch({type: 'todo/addTodo' }); // eventName正常大小写
Write Actions Using the Flux Standard Action Convention
翻译:使用FSA作为action的数据结构
说明:Redux Toolkit就是使用FSA数据结构,它只有下四个属性
- type,action type
- payload,纯对象数据,如果发生了错误,则payload为空
- error,错误信息
- meta,额外信息
扩展:在toolkit中,发送异步请求,请求正常的数据结构,type名称以'fulfilled'结尾,数据放在payload字段
请求异常的数据结构,type名称以rejected结尾,异常放在error字段
Use Action Creators
翻译:使用 action creators
说明:实际上,当我们使用createSlice的时候,就不用关心action creator了,它会自动生成action creators
Use Thunks for Async Logic
翻译:使用redux-thunk写异步逻辑
说明:异步逻辑完全可以写在redux里面,它能直接拿到Redux State,无需外部传入
export const fetchUsers = createAsyncThunk('user/fetchUsers', async (_, { dispatch, getState }) => {
// pagination和filter都可以从state中取,无需外部传入
const { pagination, filters } = getState().user;
const result = await http.userService.getPaginationUsers({
params: {
PageOffset: pagination?.current,
PageSize: pagination?.pageSize,
PageNum: 1,
NamePattern: transferURL(filters?.Username),
TimeType: filters?.RemainDay,
},
});
return result.data;
});
Move Complex Logic Outside Components
翻译:将更多逻辑放在组件外部
说明:逻辑从组件抽离出去,有多种方式,一种是utils,一种是自定义hooks,一种是redux、mobx等状态管理库,在redux中,可以将更多逻辑放在reducers和thunks中,让组件更纯粹
Use Selector Functions to Read from Store State
翻译:使用selector函数读取state数据
说明:用selector有个好处是可以结合Reselect库,让数据具备缓存的能力,在特定场景下非常有用
PS:在大部分场景下并不需要用到数据缓存,性能几乎可以忽略不急,只要在极少数场景下用到,而一旦用到,带来的效果是非常可观的。
Name Selector Functions as selectThing
翻译:selector名称用selectXXX开头
说明:如selectTodos, selectVisibleTodos, and selectTodoById
Avoid Putting Form State In Redux
翻译:不要将form state放在Redux
说明:Form中的数据都是临时的,没法在其它地方用到,没必要放在Redux,另外一些临时性的数据也不需要放在redux中,如控制弹窗的显示与否