您的代码有两个主要问题:
- reducer 在每次调度时都会生成新的过滤数组,
- 您正在滥用 useEffect 和 useState,并且构建了一个非常复杂的数据流。
让我解释这两个问题。
reducer 过滤
你有 n 个项目处于 reducer 的初始状态。
[
{ 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 文档的这一部分:Main Concepts › Thinking in React › Identify The Minimal (but complete) Representation Of UI State
“正确”的做法是去掉(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 用法,但是如果您同步的 from 和 to 值都定义在同一个地方(或非常接近)为在这种情况下,您可能无缘无故地让代码过于复杂。
例如,您可以在事件处理程序中分派:
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>)}
</>
);
进一步阅读
一旦您了解了上面列出的主题,您可能希望避免无用的重新计算,但前提是您确实遇到了一些性能问题! (见the very detailed instructions about performance optimizations in the React docs)
如果你达到了这一点,你会发现以下 React API 和 Hooks 非常有用: