Skip to content

在Redux中编写可读的reducer

本文的示例基于Redux,其中描述的问题更为常见。由于这些问题不限于Redux,如果您在代码中遇到复杂性和可读性维护方面的困难,您可能仍然会从所提供的技巧和解决方案中获益。

在处理代码中的状态时,您可能经常遇到维护复杂性、保持代码可读性甚至弄清楚如何正确测试代码的问题。通常情况下,如果您退后一步并确定问题的根源,这些问题很容易解决。

让我们从一个Redux reducer的示例开始。在本文中,我们将沿用这个示例,并进行更改和改进,所以在继续之前,请确保您理解它。

const initialState = {
  id: null,
  name: '',
  properties: {},
};

const generateID = () => Math.floor(Math.random() * 1000);

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'createID':
      return {
        ...state,
        id: generateID(),
      };
    case 'setName':
      return {
        ...state,
        name: action.name,
      };
    case 'addProperty':
      return {
        ...state,
        properties: {
          ...state.properties,
          [action.propertyName]: action.propertyValue,
        },
      };
    case 'removeProperty':
      return {
        ...state,
        properties: Object.keys(state.properties).reduce((acc, key) => {
          if (key !== action.propertyName) acc[key] = state.properties[key];
          return acc;
        }, {}),
      };
    default:
      return state;
  }
};

识别问题

虽然示例中的代码现在并不复杂,但随着应用程序需要处理更多的action类型,复杂性可能会迅速增加。这是因为每个action.type的逻辑都嵌套在reducer函数内部,因此每个新的action都会增加更多的代码和复杂性。

我们还可以发现另一个问题,即每个action具有不同的结构,这增加了未来维护者的认知负担,因为他们必须记住他们的action需要具有哪些键。还有一个额外的问题是可能遇到的情况,即可能需要使用action.type将实际数据传递给状态(例如,state.type可能存在)。

最后,我们的action.type值在reducer函数中是硬编码的,这使得很难记住并在其他文件和组件中同步。这可能看起来是我们问题中最不重要的一点,但它可能是最容易解决的,所以我们从这里开始。

定义action类型

首先,我们可以通过将action.type的硬编码字符串移除,将它们提取到一个对象中,从而使代码更易于维护和阅读:

const ACTION_TYPES = {
  CREATE_ID: 'createID',
  SET_NAME: 'setName',
  ADD_PROPERTY: 'addProperty',
  REMOVE_PROPERTY: 'removeProperty'
};

创建一个通用的action结构

我们的action对象在结构上并不一致,除了共享一个type键用于标识每个action之外。如果我们希望减少心理压力和减少头痛,我们应该使它们更一致。最简单的方法是将整个action的payload放在一个顶级键下,并将传递给action的任何值嵌套在其中:

// 传递给我们的reducer函数的任何action的结构
const action = {
  // 之前定义的任何action类型
  type: ACTION_TYPES.CREATE_ID,
  // 将name、propertyValue和propertyKey嵌套在这个对象中
  payload: { /* ... */ }
}

如果你立即将其插入到之前的代码中,可能一开始会感到违反直觉,但请耐心等待一分钟。很快一切都会串联在一起。

提取嵌套逻辑

最后,我们准备实施最彻底的修复,前面两个更改将帮助我们实现这个修复 - 提取嵌套逻辑。我们确定的第一个问题是每个action.type的逻辑都嵌套在reducer函数中。我们可以通过将每个case移动到自己的函数中来解决这个问题:

const createID = state => ({
  ...state,
  id: generateID(),
});

const setName = (state, { name }) => ({
  ...state,
  name,
});

const addProperty = (state, { propertyName, propertyValue }) => ({
  ...state,
  [propertyName]: propertyValue,
});

const removeProperty = (state, { propertyName }) => {
  const properties = Object.keys(state.properties).reduce((acc, key) => {
    if (key !== propertyName) acc[key] = state.properties[key];
    return acc;
  }, {});
  return { ...state, properties };
};

每个函数都有一个单一的职责。与每个action.type相关的任何复杂性现在都是由负责该特定action.type的函数处理的。现在,测试这些较小的函数要容易得多,因为它们专注于一个单一的任务,而不是嵌套在一个更大、更复杂的reducer中。

将所有内容整合在一起

在实现了上述更改之后,让我们来看看我们最终的代码是什么样的:

const initialState = {
  id: null,
  name: '',
  properties: {},
};

const ACTION_TYPES = {
  CREATE_ID: 'createID',
  SET_NAME: 'setName',
  ADD_PROPERTY: 'addProperty',
  REMOVE_PROPERTY: 'removeProperty'
};

const generateID = () => Math.floor(Math.random() * 1000);

const createID = state => ({ ...state, id: generateID(), });

const setName = (state, { name }) => ({ ...state, name, });

const addProperty = (state, { propertyName, propertyValue }) => ({ ...state,

});

const removeProperty = (state, { propertyName }) => { const properties = Object.keys(state.properties).reduce((acc, key) => { if (key !== propertyName) acc[key] = state.properties[key]; return acc; }, {}); return { ...state, properties }; };

const reducer = (state = initialState, action) => { switch (action.type) { case TYPES.CREATE_ID: return createID(state, action.payload); case TYPES.SET_NAME: return setName(state, action.payload); case TYPES.ADD_PROPERTY: return addProperty(state, action.payload); case TYPES.REMOVE_PROPERTY: return removeProperty(state, action.payload); default: return state; } };