我有一个对象数组,显示在网格中。目标是对数据进行多个筛选——一个按特定列中的文本进行筛选,一个按不同的日期范围进行筛选,然后按升序/降序对每一列进行排序,并进行分页(我已经有了相应的帮助函数)。我的第一种方法是使用多个useEffects,在其中过滤/排序数据并更新状态。问题是,设置状态的函数显然不会立即更新状态,所以在每次使用Effect时,我使用的不是前一个数据过滤的数据,而是原始数据。基本上,我需要在原始数组上按这个顺序将3种过滤方法链接起来,然后排序,然后分页,但除非用户更改了设置,否则我不想进行一些过滤/排序。任何关于解决这一问题的最佳方法的想法都将受到赞赏。这是一个快速的非工作原型。https://codesandbox.io/s/peaceful-nigh-jysdy用户可以随时更改过滤器,所有3个都应该生效,我还需要设置一个初始配置来保存初始过滤器值
您的代码有两个主要问题:
- reducer在每次调度时都会生成新的过滤数组
- 你滥用useEffect和useState,构建了一个非常复杂的数据流
让我解释一下这两个问题。
减速机过滤
您有n项处于减速器的初始状态。
[
{ id: "124", name: "Michael", dateCreated: "2019-07-07T00:00:00.000Z" },
{ id: "12", name: "Jessica", dateCreated: "2019-08-07T00:00:00.000Z" },
{ id: "53", name: "Olivia", dateCreated: "2019-01-07T00:00:00.000Z" }
]
当您dispatch
一个操作(如FILTER_BY_TEXT
)时,您正在生成一个新的过滤数组:
dispatch({ type: "FILTER_BY_TEXT", payload: { text: 'iv' } });
提供:
[ { id: "53", name: "Olivia", dateCreated: "2019-01-07T00:00:00.000Z" } ]
但是,如果您随后分派一个新值(在本例中为ae
,使其应与Michael匹配):
dispatch({ type: "FILTER_BY_TEXT", payload: { text: 'ae' } });
你得到一个空数组!
[]
这是因为您正在对已筛选的列表应用ae
筛选器!
卷积数据流
您的应用程序状态包括:
- 要显示、筛选和排序的数据
- 用户为过滤器和排序选择的当前值
对于每个过滤器/排序"XXX",您当前的方法使用以下模式:
function reducer(state, action) {
switch (action.type) {
case 'FILTER_BY_XXX': return filterByXXX(state, action);
default: return state;
}
}
function filterByXXX(state, action) { … }
function App() {
const [state, dispatch] React.useReducer(reducer, []);
// (1) Double state “slots”
const [xxx, setXXX] = React.useState(INITIAL_XXX);
const [inputXXX, setInputXXX] = React.useState(INITIAL_INPUT_XXX);
// (2) Synchronization effects (to apply filters)
React.useEffect(() => {
dispatch({ type: 'FILTER_BY_XXX', payload: xxx });
}, [xxx]);
return (
<input
value={inputXXX}
onChange={event => {
// (4) Store the raw input value
setInputXXX(event.currentTarget.value);
// (5) Store a computed “parsed” input value
setXXX((new Date(event.currentTarget.value));
}}
/>
);
}
让我展示一下谬论:
您不需要在(1)中使用双重状态,您只是过度复杂化了代码。
这对于字符串值(如
filterByText
和inputVal
)来说非常明显,但对于"解析"的值,您需要一点解释。将UI驱动状态与"已解析"状态分离是有意义的,但不需要将其存储在状态中!事实上,您总是可以根据实际输入状态重新计算它们。
请查看React文档的这一部分:主要概念›React中的思考›识别UI状态的最小(但完整)表示
"正确"的方法是移除(1)处的双槽,并移除(5)处的设置器。然后可以在渲染时重新计算值:
const [inputXXX, setInputXXX] = React.useState(INITIAL_INPUT_XXX);
// Here we just recompute a new Date value from the inputXXX state
const xxx = new Date(inputXXX);
return (
<input
value={inputXXX}
onChange={event => {
setInputXXX(event.currentTarget.value);
}}
/>
);
如前所述,您的reducer从完整的数据集开始,并在每次调度时被强制过滤。每次分派都指示应用程序在先前应用的筛选器之上添加另一个筛选器。
之所以会发生这种情况,是因为在该状态下,您只能得到以前操作的结果。这类似于如何将长算术运算视为一系列更简单的运算:
11 + 23 + 560 + 999 = 1593
可以重写为
( ( ( 11 + 23 ) + 560 ) + 999 ) = 1593 ( ( ( 34 ) + 560 ) + 999 ) = 1593 ( ( 594 ) + 999 ) = 1593
在每一步中,您都会丢失关于如何达到该值的信息,但您仍然会看到结果!
当您的应用程序希望"按文本筛选"时,它不希望"添加"筛选器,而是用新的文本筛选器替换以前的文本筛选器。
- 您通过同步"从状态到减速器状态"来对状态变化做出反应
这确实是useEffect的一个非常正确的使用,但如果您正在将从和同步到的两个值都定义在同一位置(或非常接近),那么您可能会无缘无故地使代码过于复杂。
例如,您可以在事件处理程序中进行分派:
return <input onChange={event => { dispatch( … ); } />;
但真正的问题是下一个。
您将计算结果存储为"状态",但它实际上是"计算值"。您的真实状态是整个数据集,过滤列表是"将过滤器应用于整个数据集"的工件。
您需要将自己从渲染时计算值的恐惧中解放出来:
const [inputVal, setInputVal] = React.useState("");
const [selectTimeVal, setSelectTimeVal] = React.useState("");
const [selectSortVal, setSelectSortVal] = React.useState("");
const data = [ {…}, {…}, {…} ];
let displayData = data;
displayData = filterByText(displayData, inputVal);
displayData = filterByTimeFrame(displayData, selectTimeVal);
displayData = sortByField(displayData, selectSortVal);
return (
<>
<input value={inputVal} onChange={………} />
<select value={selectTimeVal} onChange={………} />
<select value={selectSortVal} onChange={………} />
{displayData.map(data => <p key={data.id}>{data.name}</p>)}
</>
);
进一步阅读
一旦你理解了上面列出的主题,你可能想避免无用的重新计算,但只有当你真正遇到一些性能问题时!(请参阅React文档中关于性能优化的详细说明)
如果你做到了这一点,你会发现以下React API和钩子非常有用:
React.PureComponent
React.Component
的shouldComponentUpdate
方法React.memo
React.useMemo
吊钩React.useCallback
吊钩