在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; } };