我有一个显示在网格中的对象数组。目标是对数据进行多个过滤器 - 一个按特定列中的文本,一个按不同的日期范围,然后按升序/降序对每一列进行排序,并且还有分页(我已经有了这个的帮助函数到位)。我的第一种方法是有多个 useEffects,我在其中过滤/排序我的数据并更新状态。问题是设置状态的函数显然不会立即更新状态,因此在每个下一个 useEffect 中,我没有使用前一个数据过滤的数据,而是使用原始数据。基本上我需要在原始数组上链接 3 个过滤方法,然后按此顺序排序,然后分页,但我不想进行一些过滤/排序,除非用户更改了设置。任何有关解决此问题的最佳方法的想法都值得赞赏。这是一个快速的非工作原型。https://codesandbox.io/s/peaceful-nigh-jysdy用户可以随时更改过滤器,所有 3 都应该生效,我还需要设置一个初始配置来保存初始过滤器值
1 回答
您的代码有两个主要问题:
- 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
andinputVal
,但是对于“解析”的值,你需要一点解释。将 UI 驱动状态与“已解析”状态分开是有意义的,但您不需要将其存储在状态中!事实上,您总是可以从实际输入状态重新计算它们。
请查看 React 文档的这一部分:主要概念 › React 中的思考 › 识别 UI 状态的最小(但完整)表示
“正确”的做法是在 (1) 处移除双槽并在 (5) 处移除 setter。然后,您可以在渲染时重新计算值:
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 和 Hooks 非常有用: