Redux 减少样板代码实战:Actions、Action Creators 与 Reducers 的取舍与演进 📅 发布时间:2026/9/18 22:43:28 👁 浏览次数: Redux 减少样板代码实战Actions、Action Creators 与 Reducers 的取舍与演进【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/reduxRedux 受 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 必须是可序列化纯对象这一约定必须是纯对象isPlainObject(action)不通过时抛出Actions must be plain objects...提示你可能需要中间件如 redux-thunk来处理函数等非对象值type不能是undefinedtypeof action.type undefined时抛出Actions may not have an undefined type property. You may have misspelled an action type string constant.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避免重复造轮子——团队里可能已经有人加过你需要的 actionPull 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.jsexport function addTodo(text) { return { type: ADD_TODO, text } }AddTodo.jsimport { 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))。实现核心是bindActionCreatorfunction 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.jsexport 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.jsimport { 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! Dont 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 pLoading.../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。NoteThunk 中间件只是中间件的一个例子。中间件并不是允许你 dispatch 函数而是允许你 dispatch 任何你所用的特定中间件知道如何处理的值。Thunk 中间件在你 dispatch 函数时增加特定行为但具体能处理什么取决于你使用的中间件。上面的代码用 redux-thunk 重写actionCreators.jsexport function loadPosts(userId) { // Interpreted by the thunk middleware: return function (dispatch, getState) { const { posts } getState() if (posts[userId]) { // There is cached data! Dont 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.jsimport { 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 pLoading.../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 storeconst _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 削减样板代码的路径是递进且可选的Actions 层从内联字符串起步项目变大后收敛为集中的字符串常量不要用 SymbolAction Creators 层用函数封装 action 构造需要附加逻辑缓存检查、业务前置条件时借助 redux-thunk 写成 thunk纯机械的 creator 可用makeActionCreator之类的工厂函数或 redux-actions 等库生成官方bindActionCreators可进一步省去手写dispatch(...)异步层无中间件时逻辑散落在组件中引入 thunk 或自定义 API 中间件后异步流程被集中到 action creator 内部并可在组件间复用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),仅供参考