【问题标题】:Functional component chaining multiple filters and sorting of an array链接多个过滤器和数组排序的功能组件
【发布时间】:2020-04-19 01:04:49
【问题描述】:

我有一个显示在网格中的对象数组。目标是对数据进行多个过滤器 - 一个按特定列中的文本,一个按不同的日期范围,然后按升序/降序对每一列进行排序,并且还有分页(我已经有了这个的帮助函数到位)。 我的第一种方法是有多个 useEffects,我在其中过滤/排序我的数据并更新状态。问题是设置状态的函数显然不会立即更新状态,因此在每个下一个 useEffect 中,我不是使用前一个数据过滤的,而是使用原始数据。基本上我需要在原始数组上链接 3 个过滤方法,然后按此顺序排序,然后分页,但我不想做一些过滤/排序,除非用户更改了设置。 任何有关解决此问题的最佳方法的想法都值得赞赏。这是一个快速的非工作原型。 https://codesandbox.io/s/peaceful-nigh-jysdy 用户可以随时更改过滤器,所有 3 都应该生效,我还需要设置一个初始配置来保存初始过滤器值

【问题讨论】:

    标签: arrays reactjs react-hooks chaining react-functional-component


    【解决方案1】:

    您的代码有两个主要问题:

    1. reducer 在每次调度时都会生成新的过滤数组,
    2. 您正在滥用 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 过滤器应用于已过滤列表!

    复杂的数据流

    您的应用程序状态由以下部分组成:

    1. 您要显示、过滤和排序的数据,
    2. 用户为过滤器和排序选择的当前值。

    对于每个过滤器/排序“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. 你不需要在 (1) 处的双重状态,你只是让你的代码过于复杂。

      这对于字符串值很明显,例如filterByTextinputVal,但对于“解析”的值,您需要稍微解释一下。

      确实将 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);
        }}
      />
    );
    
    1. 您的 reducer,如前所述,从完整的数据集开始,并在每次调度时强制过滤。每次调度都会指示应用程序在之前应用的过滤器之上添加另一个过滤器。

      发生这种情况是因为在该状态下您只有先前操作的结果。这类似于您如何将长算术运算视为一系列更简单的运算:

      11 + 23 + 560 + 999 = 1593
      

      可以改写为

      ( ( ( 11 + 23 ) + 560 ) + 999 ) = 1593
      ( ( (   34    ) + 560 ) + 999 ) = 1593
      ( (         594       ) + 999 ) = 1593
      

      在每一步中,您都会丢失关于如何达到该值的信息,但您仍然看到了结果!

      当您的应用程序想要“按文本过滤”时,它不想“添加”过滤器,而是用新的文本过滤器替换之前的文本过滤器。

      1. 您通过将“从状态到减速器的状态”与效果同步来对状态变化做出反应。

      这确实是一个非常正确的 useEffect 用法,但是如果您同步的 fromto 值都定义在同一个地方(或非常接近)为在这种情况下,您可能无缘无故地让代码过于复杂。

      例如,您可以在事件处理程序中分派:

    return <input onChange={event => { dispatch( … ); } />;
    

    但真正的问题是下一个。

    1. 您将计算结果存储为“状态”,但它实际上是“计算值”。您的真实状态是整个数据集,过滤后的列表是“将过滤器应用于整个数据集”的产物。

      您需要摆脱在渲染时计算值的恐惧:

    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 非常有用:

    【讨论】:

    • 感谢您提供非常详细的回答。我仍在努力理解第 2 点和第 4 点。在第 2 点“当您的应用程序想要“按文本过滤”时,它不想“添加”过滤器,而是用新的文本过滤器替换以前的文本过滤器。”我怎样才能做到这一点?也许我根本不需要减速器?如果用户只更改了一个设置(例如时间范围),我实际上想要多个 useEffects,只更新数组中的那个,而不是再次通过文本过滤器和排序,但我想这是不可能的。
    • 在 4. 如果我过滤和排序的数据不是状态,并且我摆脱了 useReducer,当用户更改设置时我在哪里计算过滤/排序数组 - 不应该在使用效果。如果是这样,按什么顺序,因为我想让它们中的每一个相互叠加
    • 正确的方法是在渲染期间只计算过滤后的版本:直接在你的功能组件主体中(或 render 类组件的方法)。性能方面,没有办法单独计算每个过滤器或排序:它们实际上只是相互堆叠。顺序应该不重要,因为每个过滤器都会检查单个项目的值,它不依赖于其他过滤器。
    • 您的问题解决了吗?如果我的回答确实对您有所帮助,是否愿意将其标记为解决方案答案?否则,最好展示一下您是如何解决的,以便其他人看到您的解决方案。
    • @PierPaoloRamon,感谢您提供此示例。在您的第 4 点中,onChange={.....} 部分应该做什么?对于selectTimeVal 字段,它的onChang 应该调用setSelectTimeValfilterByTimeFrame 还是完全其他的?我不太清楚displayData 是如何更新用户交互的。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-07
    • 2014-02-14
    • 1970-01-01
    • 2017-08-12
    • 1970-01-01
    • 1970-01-01
    • 2017-05-06
    相关资源
    最近更新 更多