Redux 减少样板代码实战:Actions、Action Creators 与 Reducers 的取舍与演进
2026/9/18 22:43:24 网站建设 项目流程

Redux 减少样板代码实战:Actions、Action Creators 与 Reducers 的取舍与演进

【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux

Redux 受 Flux 启发(见 PriorArt),而 Flux 最常见的抱怨就是样板代码太多。本指南以仓库文档 ReducingBoilerplate 为核心,系统讲解在 Redux 中如何按个人风格、团队偏好与长期可维护性,自由选择代码的"啰嗦程度":从内联 action 对象、字符串常量、action creator,到基于 redux-thunk 的异步 action creator,再到自定义中间件与 reducer 生成器。读完你将掌握一整套可落地、可裁剪的样板代码削减方案,并理解每条路径背后的设计原理与源码依据。

Actions:对象本身不是样板,而是设计基石

Action 是描述应用"发生了什么"的普通对象,也是表达"修改数据意图"的唯一途径。"action 必须是可 dispatch 的对象"并不是样板代码,而是 Redux 的三大设计原则之一

典型 action 形如:

{ type: 'ADD_TODO', text: 'Use Redux' } { type: 'REMOVE_TODO', id: 42 } { type: 'LOAD_ARTICLE', response: { ... } }

一些号称"类似 Flux"但取消 action 对象概念的框架,在可预测性上反而是一种倒退:没有可序列化的纯对象 action,就无法录制和重放用户会话,也无法实现带时间旅行的热重载。如果你更想直接修改数据,其实不需要 Redux。

从源码看,仓库的 createStore.ts 对 dispatch 的参数做了三道强校验,直接印证了"action 必须是可序列化纯对象"这一约定:

  1. 必须是纯对象isPlainObject(action)不通过时抛出Actions must be plain objects...,提示你可能需要中间件(如 redux-thunk)来处理函数等非对象值;
  2. type不能是undefinedtypeof action.type === 'undefined'时抛出Actions may not have an undefined "type" property. You may have misspelled an action type string constant.
  3. type必须是字符串:非字符串类型(如数字或 Symbol)会直接抛错。

其中isPlainObject的实现位于 isPlainObject.ts,它通过沿原型链逐层上溯并比较原型,严格区分"纯对象"与类实例、数组等;isAction(见 isAction.ts)则是isPlainObject与字符串type的组合判断。

用字符串常量而非 Symbol 定义 action type

业界惯例是让 action 拥有一个常量类型,供 reducer(或 Flux 的 Store)识别。Redux 官方推荐使用字符串而非 Symbol:字符串可序列化,使用 Symbol 会使录制与重放变得不必要的困难。

Flux 传统做法是把每个 action type 定义为字符串常量:

const ADD_TODO = 'ADD_TODO' const REMOVE_TODO = 'REMOVE_TODO' const LOAD_ARTICLE = 'LOAD_ARTICLE'

常被质疑"没必要"——对小型项目这或许成立;但对大型项目,集中定义 action 常量有实实在在的好处:

  • 所有 action type 汇聚在一处,保持命名一致
  • 开发新功能前可以快速浏览全部已有 action,避免重复造轮子——团队里可能已经有人加过你需要的 action;
  • Pull Request 中新增、删除、修改的 action type 清单,帮助全员把握新功能的范围与实现
  • 导入常量时一旦拼错,会得到undefined。此时 dispatch 会立即抛错(对应上面第 2 条校验),错误暴露得更早

值得注意的是,仓库内部自身也以这种方式工作:@@redux/INIT@@redux/REPLACE等私有 action type 就是在 actionTypes.ts 中以字符串常量(并拼接随机串避免冲突)定义,createStore创建时会 dispatchINIT让每个 reducer 返回初始状态(见 createStore.ts)。

选择哪种约定完全由你决定:可以先内联字符串,再演进为常量,最后集中到单文件。Redux 对此不持立场,凭最佳判断即可。

Action Creators:把"发起 action"封装成函数

另一常见惯例是:不在 dispatch 的位置内联创建 action 对象,而是编写生成它们的函数。

例如,不在事件处理器中直接 dispatch 对象字面量:

// somewhere in an event handler dispatch({ type: 'ADD_TODO', text: 'Use Redux' })

而是把 action creator 放进独立文件并导入组件:

actionCreators.js
export function addTodo(text) { return { type: 'ADD_TODO', text } }
AddTodo.js
import { addTodo } from './actionCreators' // somewhere in an event handler dispatch(addTodo('Use Redux'))

Action creator 常被批评为样板代码——其实你完全可以不写它们,用对象字面量也完全可以。但了解下面这个好处会改变你的判断:

假设设计师在评审原型后提出"最多只允许三个待办"。你可以把addTodo改写为配合 redux-thunk 中间件的回调形式并提前退出:

function addTodoWithoutCheck(text) { return { type: 'ADD_TODO', text } } export function addTodo(text) { // This form is allowed by Redux Thunk middleware // described below in “Async Action Creators” section. return function (dispatch, getState) { if (getState().todos.length === 3) { // Exit early return } dispatch(addTodoWithoutCheck(text)) } }

我们修改了addTodo的行为,而调用方完全无感知——不必逐个排查所有添加待办的位置来补校验。Action creator 把"dispatch 之外的附加逻辑"与"发出这些 action 的组件"解耦,在需求频繁变化的重度开发阶段尤其好用。

用工厂函数生成 Action Creators

像 Flummox 这类框架会从 action creator 函数定义中自动生成类型常量,免去同时定义ADD_TODO常量和addTodo()函数。但这类方案只是把常量隐式生成,多了一层间接性,容易造成困惑。Redux 官方推荐显式创建 action type 常量

手写简单 action creator 确实乏味且容易产生冗余代码:

export function addTodo(text) { return { type: 'ADD_TODO', text } } export function editTodo(id, text) { return { type: 'EDIT_TODO', id, text } } export function removeTodo(id) { return { type: 'REMOVE_TODO', id } }

你完全可以写一个"生成 action creator"的函数:

function makeActionCreator(type, ...argNames) { return function (...args) { const action = { type } argNames.forEach((arg, index) => { action[argNames[index]] = args[index] }) return action } } const ADD_TODO = 'ADD_TODO' const EDIT_TODO = 'EDIT_TODO' const REMOVE_TODO = 'REMOVE_TODO' export const addTodo = makeActionCreator(ADD_TODO, 'text') export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text') export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')

此外还有 redux-act、redux-actions 等工具库可以辅助生成 action creator、减少样板代码并强制遵循 Flux Standard Action (FSA) 规范。

源码补充:bindActionCreators帮你少写 dispatch

仓库还自带一个与 action creator 配套的官方 API——bindActionCreators.ts。它把"以 action creator 为值、以 action 名为键"的对象,原地转换成键不变、但每个函数都被dispatch包装的新对象,从而可以直接调用addTodo(text)而不必写dispatch(addTodo(text))。实现核心是bindActionCreator

function bindActionCreator(actionCreator, dispatch) { return function (this, ...args) { return dispatch(actionCreator.apply(this, args)) } }

它同时支持传入单个函数(返回被包装的单个函数)和对象(import * as ActionCreators的写法天然契合),并在入参类型错误时抛出带kindOf类型提示的错误(见 bindActionCreators.ts)。这一 API 正是"减少样板"理念在官方 API 层面的体现。

Async Action Creators:中间件让异步逻辑可复用

中间件允许你在每个 action 被 dispatch 之前注入自定义逻辑来解释它。异步 action 是中间件最常见的用例

没有任何中间件时,dispatch只接受纯对象,所以 AJAX 调用只能写在组件内部:

actionCreators.js
export function loadPostsSuccess(userId, response) { return { type: 'LOAD_POSTS_SUCCESS', userId, response } } export function loadPostsFailure(userId, error) { return { type: 'LOAD_POSTS_FAILURE', userId, error } } export function loadPostsRequest(userId) { return { type: 'LOAD_POSTS_REQUEST', userId } }
UserInfo.js
import { Component } from 'react' import { connect } from 'react-redux' import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators' class Posts extends Component { loadData(userId) { // Injected into props by React Redux `connect()` call: const { dispatch, posts } = this.props if (posts[userId]) { // There is cached data! Don't do anything. return } // Reducer can react to this action by setting // `isFetching` and thus letting us show a spinner. dispatch(loadPostsRequest(userId)) // Reducer can react to these actions by filling the `users`. fetch(`http://myapi.com/users/${userId}/posts`).then( response => dispatch(loadPostsSuccess(userId, response)), error => dispatch(loadPostsFailure(userId, error)) ) } componentDidMount() { this.loadData(this.props.userId) } componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) { this.loadData(this.props.userId) } } render() { if (this.props.isFetching) { return <p>Loading...</p> } const posts = this.props.posts.map(post => ( <Post post={post} key={post.id} /> )) return <div>{posts}</div> } } export default connect(state => ({ posts: state.posts, isFetching: state.isFetching }))(Posts)

问题很快暴露:不同组件从同一 API 端点取数据,这段逻辑高度重复;而且"有缓存数据就提前退出"这类逻辑想被多个组件复用也无处安放。

中间件让我们写出更具表现力、甚至异步的 action creator:它可以 dispatch 纯对象以外的值并解释它们。例如中间件可以"接住"被 dispatch 的 Promise,将其转化为一对 request 与 success/failure action。

redux-thunk:把 action creator 写成函数返回函数

最简单的中间件示例是 redux-thunk。"Thunk"中间件允许把 action creator 写成"thunk",即返回函数的函数。这反转了控制权:你会拿到dispatch作为参数,因此可以写出多次 dispatch 的 action creator。

Note

Thunk 中间件只是中间件的一个例子。中间件并不是"允许你 dispatch 函数",而是"允许你 dispatch 任何你所用的特定中间件知道如何处理的值"。Thunk 中间件在你 dispatch 函数时增加特定行为,但具体能处理什么,取决于你使用的中间件。

上面的代码用 redux-thunk 重写:

actionCreators.js
export function loadPosts(userId) { // Interpreted by the thunk middleware: return function (dispatch, getState) { const { posts } = getState() if (posts[userId]) { // There is cached data! Don't do anything. return } dispatch({ type: 'LOAD_POSTS_REQUEST', userId }) // Dispatch vanilla actions asynchronously fetch(`http://myapi.com/users/${userId}/posts`).then( response => dispatch({ type: 'LOAD_POSTS_SUCCESS', userId, response }), error => dispatch({ type: 'LOAD_POSTS_FAILURE', userId, error }) ) } }
UserInfo.js
import { Component } from 'react' import { connect } from 'react-redux' import { loadPosts } from './actionCreators' class Posts extends Component { componentDidMount() { this.props.dispatch(loadPosts(this.props.userId)) } componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) { this.props.dispatch(loadPosts(this.props.userId)) } } render() { if (this.props.isFetching) { return <p>Loading...</p> } const posts = this.props.posts.map(post => ( <Post post={post} key={post.id} /> )) return <div>{posts}</div> } } export default connect(state => ({ posts: state.posts, isFetching: state.isFetching }))(Posts)

这省了大量输入!如果你愿意,仍然可以保留loadPostsSuccess这类"纯"action creator,供容器化的loadPosts内部使用。

从源码看,applyMiddleware的实现(见 applyMiddleware.ts)正是这一机制的底层支撑:它先创建 store,然后把{ getState, dispatch }组成的middlewareAPI依次注入每个中间件,最后通过compose把中间件链叠加到store.dispatch上——这就是为什么 thunk 能在dispatch(loadPosts(...))时拦截到函数并为其注入dispatch/getState。需要把applyMiddleware接入 store 时,参考 applyMiddleware 文档 与 createStore 文档 即可。

自定义中间件:把异步流程声明式化

最后,你可以编写自己的中间件。假如想把上面的模式泛化,让异步 action creator 这样描述自己:

export function loadPosts(userId) { return { // Types of actions to emit before and after types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], // Check the cache (optional): shouldCallAPI: state => !state.posts[userId], // Perform the fetching: callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), // Arguments to inject in begin/end actions payload: { userId } } }

解释此类 action 的中间件可以这样写:

function callAPIMiddleware({ dispatch, getState }) { return next => action => { const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action if (!types) { // Normal action: pass it on return next(action) } if ( !Array.isArray(types) || types.length !== 3 || !types.every(type => typeof type === 'string') ) { throw new Error('Expected an array of three string types.') } if (typeof callAPI !== 'function') { throw new Error('Expected callAPI to be a function.') } if (!shouldCallAPI(getState())) { return } const [requestType, successType, failureType] = types dispatch( Object.assign({}, payload, { type: requestType }) ) return callAPI().then( response => dispatch( Object.assign({}, payload, { response, type: successType }) ), error => dispatch( Object.assign({}, payload, { error, type: failureType }) ) ) } }

注意这里遵循的中间件签名({ dispatch, getState }) => next => action => ...与仓库 middleware 类型定义 完全一致,applyMiddleware正是按此约定逐个调用中间件并串成链条(见 applyMiddleware.ts)。

把它通过applyMiddleware(...middlewares)接入一次后,所有调用 API 的 action creator 都能写成同一套声明式风格:

export function loadPosts(userId) { return { types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], shouldCallAPI: state => !state.posts[userId], callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), payload: { userId } } } export function loadComments(postId) { return { types: [ 'LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE' ], shouldCallAPI: state => !state.comments[postId], callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`), payload: { postId } } } export function addComment(postId, message) { return { types: [ 'ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE' ], callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, { method: 'post', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }), payload: { postId, message } } }

更进一步,仓库还提供了compose(见 compose.ts)用于组合多个中间件/增强器,相关用法可查阅 compose 文档。多个中间件(如日志中间件 + thunk + 自定义 API 中间件)通过compose串联后一并传入applyMiddleware即可。

Reducers:函数比对象、类比 Flux Store 简单得多

Redux 把更新逻辑描述为纯函数,从而大幅削减了 Flux Store 的样板代码——函数比对象简单,比类更简单。

看一个 Flux store:

const _todos = [] const TodoStore = Object.assign({}, EventEmitter.prototype, { getAll() { return _todos } }) AppDispatcher.register(function (action) { switch (action.type) { case ActionTypes.ADD_TODO: const text = action.text.trim() _todos.push(text) TodoStore.emitChange() } }) export default TodoStore

用 Redux,同样的更新逻辑只是一个 reducer 函数:

export function todos(state = [], action) { switch (action.type) { case ActionTypes.ADD_TODO: const text = action.text.trim() return [...state, text] default: return state } }

switch语句并不是真正的样板。Flux 真正的样板是概念层面的:需要主动 emit 更新、需要把 Store 注册到 Dispatcher、需要 Store 是对象(而这在构建同构应用时会带来种种麻烦)。注意上面的 Redux 版本还严格遵循了default: return state约定——这正是仓库 combineReducers 在初始化时用PROBE_UNKNOWN_ACTION探测各 reducer 的硬性要求:对未知 action 必须返回当前 state,否则抛错。

如果连switch都不喜欢,用单个函数即可解决(见下文)。

createReducer生成 Reducers

写一个函数,把 reducer 表达为"action type 到处理函数"的映射。例如希望todosreducer 这样定义:

export const todos = createReducer([], { [ActionTypes.ADD_TODO]: (state, action) => { const text = action.text.trim() return [...state, text] } })

可以这样实现辅助函数:

function createReducer(initialState, handlers) { return function reducer(state = initialState, action) { if (handlers.hasOwnProperty(action.type)) { return handlersaction.type } else { return state } } }

并不难,对吧?Redux 默认不提供这类辅助函数,因为写法太多样:也许你想自动把纯 JS 对象转成 Immutable 对象以水合服务端状态;也许你想把返回的 state 与当前 state 合并;也许你对"兜底 handler"有不同思路——这些都取决于团队在具体项目上选择的约定。Redux 的 reducer API 就是(state, action) => newState,但如何创建 reducer 由你决定

把多个这样的 reducer 合并为根 reducer 时,可以直接使用官方combineReducers(见 combineReducers.ts 起的主体实现与 combineReducers 文档),它会逐个调用子 reducer 并按同形键聚合出完整状态树。

总结:一条渐进的样板削减路径

从本文可以看到,Redux 削减样板代码的路径是递进且可选的

  1. Actions 层:从内联字符串起步,项目变大后收敛为集中的字符串常量(不要用 Symbol);
  2. Action Creators 层:用函数封装 action 构造,需要附加逻辑(缓存检查、业务前置条件)时借助 redux-thunk 写成 thunk;纯机械的 creator 可用makeActionCreator之类的工厂函数或 redux-actions 等库生成,官方bindActionCreators可进一步省去手写dispatch(...)
  3. 异步层:无中间件时逻辑散落在组件中,引入 thunk 或自定义 API 中间件后,异步流程被集中到 action creator 内部并可在组件间复用;
  4. Reducers 层:用(state, action) => newState函数取代 Flux 的 Store 类与 Dispatcher 注册机制,必要时用createReducer把 switch 改写为 handlers 映射。

每一步都对应仓库源码中的真实机制:dispatch 的强校验(createStore.ts)、中间件链的组合(applyMiddleware.ts)、action creator 的绑定(bindActionCreators.ts)以及私有 action type 的处理(actionTypes.ts)。具体取舍没有标准答案——按团队约定与项目规模选择即可,这正是 Redux 设计哲学的体现:核心机制保持可预测,而表达方式把选择权留给你

【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询