百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

「官方」Redux 编码规范(redux详解)

suiw9 2025-03-24 20:38 12 浏览 0 评论

翻译官方文档:Style Guide | Redux 中文官网,并做一些简单解读


  1. 必须遵守(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对象


  1. 强烈推荐(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? 这里给出一些建议

  1. Do other parts of the application care about this data?(有模块间共享的需求)
  2. Do you need to be able to create further derived data based on this original data?(有数据派生的需求)
  3. Is the same data being used to drive multiple components?(数据需要组件间共享)
  4. Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?(时间旅行,用于调试就可以了)
  5. Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?(数据缓存,性能优化)
  6. 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有以下特点(优势)

  1. The history log of dispatched actions(actions历史,方便回溯)
  2. The contents of each action(action内容,代替console.log、debugger)
  3. The final state after an action was dispatched(state状态,了解整个应用的状态)
  4. The diff in the state after an action(prevState 和 currentState的 diff,有助于判断符合预期)
  5. 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库 - 知乎


  1. 推荐(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数据结构,它只有下四个属性

  1. type,action type
  2. payload,纯对象数据,如果发生了错误,则payload为空
  3. error,错误信息
  4. 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中,如控制弹窗的显示与否

相关推荐

10款超实用JavaScript音频库(js播放音频代码)

HTML5提供了一种新的音频标签实现和规范用一个简单的HTML对象而无需音频插件来控制音频。这只是一个简单的整合这些新的HTML5音频特征及使用JavaScript来创建各种播放控制。下面将介绍10款...

Howler.js,一款神奇的 JavaScript 开源网络音频工具库

o...

PROFINET转Modbus网关——工业协议融合的智能枢纽

三格电子SG-PNh750-MOD-221,无缝连接Profinet与Modbus,赋能工业物联产品概述...

简单实用的Modbus类库,支持从站和DTU

一、简介...

[西门子PLC] S7-200 SMART PROFINET :通过GSD组态PLC设备

从S7-200SMARTV2.5版本开始,S7-200SMART开始支持做PROFINETIO通信的智能设备。从而,两个S7-200SMART之间可以进行PROFINETI...

Modbus(RTU / TCP)有什么异同(modbus tcp和tcp)

Modbus是一种广泛使用的工业自动化通信协议,它支持设备之间的数据交换。Modbus协议有两个主要的变体:ModbusRTU(二进制模式)和ModbusTCP(基于TCP/IP网络的模式)。尽管...

Modbus通信调试步骤详解(modbus调试工具怎么用)

Modbus通信调试步骤详解  Modbus通信分为串口和以太网,无论是串口还是以太网,只要是标准Modbus,就可以用Modbus模拟器进行调试。按以下几步进行调试。...

理解Intel手册汇编指令(intel 汇编指令手册)

指令格式...

「西门子PLC」S7-200 SMART的Modbus RTU通讯

S7-200SMART集成的RS485端口(端口0)以及SBCM01RS485/232信号板(端口1)两个通信端口可以同时做MODBUSRTU主站,或者一个做MODBUSRTU主站一个做MO...

InfiniBand网络运维全指南:从驱动安装到故障排查

一、InfiniBand网络概述InfiniBand(直译为“无限带宽”技术,缩写为IB)是一种用于高性能计算的计算机网络通信标准,具有极高的吞吐量和极低的延迟,用于计算机与计算机之间的数据互连。它...

一加回归 OPPO,背后的秘密不可告人

有这样一个手机品牌,它诞生于互联网品牌。在大众群体看来,它的身世似乎模糊不清,许多人以为它是国外品牌。它的产品定位是极客群体,深受国内发烧友,甚至国外极客玩家喜爱。...

[西门子PLC] S7-200SMART快速高效的完成Modbus通信程序的设计

一、导读Modbus通信是一种被广泛应用的通信协议,在变频器、智能仪表还有其他一些智能设备上都能见到它的身影。本文呢,就把S7-200SMART系列PLC当作Modbus主站,把...

狂肝10个月手搓GPU,他们在我的世界中玩起我的世界,梦想成真

梦晨衡宇萧箫发自凹非寺量子位|公众号QbitAI自从有人在《我的世界》里用红石电路造出CPU,就流传着一个梗:...

[西门子PLC] 博途TIA portal SCL编程基础入门:1-点动与自锁

一、S7-SCL编程语言简介...

工作原理系列之:Modbus(modbus工作过程)

MODBUS是一种在自动化工业中广泛应用的高速串行通信协议。该协议是由Modion公司(现在由施耐德电气公司获得)于1979年为自己的可编程逻辑控制器开发的。该协议充当了PLCS和智能自动化设备之间的...

取消回复欢迎 发表评论: