【问题标题】:Change internal component state after redux store change but before component renders在 redux 存储更改之后但在组件渲染之前更改内部组件状态
【发布时间】:2020-01-30 23:18:29
【问题描述】:

假设我有一个连接到 redux 存储的反应组件,但也管理一些内部状态。 内部状态包含要获取的dataloading 标志,用于呈现“正在加载...”文本或获取后的数据。 我还需要selectedItems,这是我从 redux 商店获得的。这些项目也用于应用程序的其他部分,例如侧边栏显示当前选择的项目,因此用户始终知道哪些项目被选中,并可以使用侧边栏选择或删除项目。

在这个组件中,selectedItems 用于帮助映射我获取的数据。

所以组件看起来像这样:

export default () => {
  const selectedItems = useSelector(state => state.itemsReducer.selectedItems);

  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchData('someApi/data').then(res => {
      setData(res);
      setLoading(false);
    });
  }, [selectedItems]);

  return (
    <div>
      {loading ? (<div>Loading...</div>) : (<div>{data}</div>)}
    </div>
  );
}

现在假设在&lt;div&gt;{data}&lt;/div&gt; 部分我实际上做了一些复杂的映射,我使用来自selectedItems 的信息来正确映射数据。

我遇到的问题是:如果用户通过侧边栏添加了一个项目,然后更改了商店中的selectedItems,这意味着当前的data 不再有效,我需要获取新数据。因此,在商店更改后,我想立即将loading 设置为true 以显示“正在加载...”文本,然后在组件安装后获取新数据,因此在useEffect 中。所以,我知道 useEffect 只有在组件渲染后才会被调用。但是,我在 JSX 中映射 data,它使用已经根据商店更改更新的 selectedItems,但 data 仍然是旧的 useEffect 尚未被调用。在更改selectedItems 并获取新数据之间的时间段内,我根本不想映射data。所以我基本上希望在这两个事件之间将loading 设置为true,但是我将如何去做呢?在useEffect 中设置它为时已晚,因为组件已经被渲染/更新,但在此之前,它已经知道selectedItems 中的更改(这实际上会导致错误,因为它试图访问@987654343 中的某些属性@ 基于新选择的项目,但 data 仍然是旧项目,因此不包含该项目。

这整个事情似乎是一个反应应用程序要处理的常见情况,所以我想我会很快找到解决方案,但是,还没有成功。

更新基于@brendangannon 的回答

所以我可能提供了一个过于简单的例子。复杂映射是指将数据映射到 JSX。首先我映射到data 并且在selectedItems 上有一个嵌套映射,它创建了一个类似表格的结构,其中行基于data,除了第一列之外,列数基于@987654348 @。

我没有提到但现在看起来很相关的一件事是,实际上data 通过另一个钩子到达组件。

所以我有这样的东西,而不是上面的useEffect

export default () => {
  // ...
  const {data, loading} = useLoadData(...someParams);
  // ...
}

钩子保持一个内部状态,并在它自己的依赖于selectedItemsuseEffect中获取数据,如果selectedItems发生变化,则根据这些项目获取新数据。

在我的组件中也保留内部状态是否有意义,这将是data 的副本,我们现在将其命名为dataToMap,它将在 JSX 中使用?我还会添加一个useEffect 和一个依赖[data],它会在加载新数据时设置这个组件的状态,基本上更新dataToMap。因此,如果selectedItems 更改,此组件将在使用陈旧的dataToMap 时重新渲染,然后useLoadData 的内部useEffect 将开始在内部加载新数据,同时给我loading === true,即将到来的渲染将用于显示“正在加载...”文本,然后当获取新的data 时,我的钩子由于[data] 的依赖变化而触发,将新的dataToMap 设置为内部状态,这会触发另一个重新渲染现在可以(并且将由于useLoadData 在内部设置loading===false)安全地映射到状态中的新数据。

所以组件看起来像这样:

export default () => {
  // ...
  const selectedItems = useSelector(state => state.itemsReducer.selectedItems);

  const {data, loading} = useLoadData(...someParams);

  const [dataToMap, setDataToMap] = useState([]);

  useEffect(() => {
    if (loading === false) {
      // new data fetched
      setDataToMap(data);
    }
  }, [data]);

  return (
    <div>
      {loading ? (<div>Loading...</div>) : (
        <div>
         {dataToMap.map(dataEl => {
           return (
             <>
             // render some row stuff
             {selectedItems.map(selItem => {
               // render some column stuff
             })}
             </>
           )
         })}
        </div>
      )}
    </div>
  );
}

这给我留下了两个问题:

  1. 如果我保留useLoadData 挂钩,这是一个好方法吗?
  2. 考虑可能删除useLoadData,是否有更好的方法?

更新 2

刚刚意识到这会导致同样的问题 - 当陈旧的 dataToMap 被映射而 selectedItems 已经更改(存在新项目)时,它会尝试访问不在 dataToMap 中的数据。

我想我将在内部将selectedItems 存储在useLoadData 状态并将其返回到我的组件以进行映射,例如selectedItemsForMapping。当selectedBuckets 更改时,它仍会使用旧的selectedBucketsForMapping 来渲染陈旧的数据,然后它会返回新的selectedItemsForMappingloading===true,这样我就可以显示“正在加载...”和其余的已经在上面讨论过了。

【问题讨论】:

    标签: reactjs redux react-hooks use-effect


    【解决方案1】:

    我想你会想要稍微改变你的设计。 “复杂映射”不应该(通常)发生在渲染中——无论您如何管理状态,您都不想在每个渲染上重新执行该映射。最好在原始数据更改的点计算派生数据(在这种情况下,来自 API 调用的结果),并在数据“进入”组件时准备好使用数据(在这种情况下,当它设置为状态)。将数据处理与渲染分开是单一/单独职责的一个很好的例子,并且使测试更容易。

    所以它可能看起来像:

    export default () => {
      const selectedItems = useSelector(state => state.itemsReducer.selectedItems);
    
      const [data, setData] = useState([]);
      const [loading, setLoading] = useState(true);
    
      useEffect(() => {
        fetchData('someApi/data').then(res => {
          const modifiedData = doComplexMapping(res, selectedItems);
          setData(modifiedData);
          setLoading(false);
        });
      }, [selectedItems]);
    
      return (
        <div>
          {loading ? (<div>Loading...</div>) : (<div>{data}</div>)}
        </div>
      );
    }
    

    在这种情况下,render 所做的只是从 state 中渲染一些东西,所以只要 state 中有数据,它就可以安全地执行,无论它是否是陈旧的。当 selectedItems 在 store 中发生变化时,组件会再次渲染(显示旧数据),然后立即设置一个 'loading' 状态,渲染正在加载的 UI,然后它会获取新数据,处理它,将其设置为状态,并渲染新数据。前两个渲染将发生得足够快,以至于您可能不会注意到过时数据的重新渲染;它会或多或少地立即显示“正在加载”的 UI。

    但是,由于对 selectedItems 的任何更改都会触发新的 api 调用,因此将这个 api 调用移出该组件并进入更改 selectedItems 的操作创建者可能是有意义的。该组件中当前发生的任何复杂映射也将转移到操作创建器,并且您当前在该组件中生成的派生数据将改为设置在 redux 存储中,该组件将通过选择器将其拉出。

    这样,对应用程序状态的更改(selectedItems 由于用户交互而发生了更改)直接与重新获取数据(通过 api 调用)相关联,而不是通过组件渲染下游的副作用间接连接状态变化。通过在原始状态更改发生的同一位置更改派生数据,代码更清楚地反映了应用程序的因果性质。这取决于整个应用程序的设计,但在 react/redux 应用程序中,以这种方式在动作创建者级别进行数据获取是很常见的,尤其是当涉及更改和呈现相关状态的多个 UI 组件时.

    【讨论】:

    • 这似乎是个好主意。我实际上试图提供一个简化的例子来说明这个问题,可能有点过于简化了。复杂映射是指映射到 JSX,首先是在 data 上,然后是在 selectedItems 上的嵌套映射。顺便说一句,selectedItems 并不总是触发这个 api 调用,只是在应用程序的一个特定页面上,而带有selectedItems 的侧边栏几乎可以在所有页面上看到。我会稍微更新一下我的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-18
    • 1970-01-01
    相关资源
    最近更新 更多