在 redux 存储更改之后但在组件呈现之前更改内部组件状态

Change internal component state after redux store change but before component renders

所以假设我有一个连接到 redux 存储的反应组件,但也管理一些内部状态。 内部状态包含要获取的 data 和用于呈现 'Loading...' 文本或获取后的数据的 loading 标志。 我还需要 selectedItems 这是我从 redux 商店得到的东西。这些项目也用于应用程序的其他部分,例如侧边栏显示当前 selected 项目,因此用户始终知道哪些项目已 selected 并且可以使用侧边栏 select 或删除项目。

在此组件中,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>
  );
}

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

我遇到的问题是:如果用户通过侧边栏添加一个项目,然后在商店中更改 selectedItems,这意味着当前的 data 不再有效,我需要获取新数据。因此,在商店更改后,我想立即将 loading 设置为 true 以显示 'Loading...' 文本,然后在组件安装后获取新数据,因此在 useEffect 中。所以,我知道 useEffect 仅在组件呈现后才被调用。但是,我在 JSX 中映射 data,它使用已经根据商店更改更新的 selectedItems,但 data 仍然是旧的 useEffect还没有被调用。在 selectedItems 更改和获取新数据之间的时间里,我根本不想映射 data。所以我基本上想在这两个事件之间将 loading 设置为 true,但我该怎么做呢?在 useEffect 中设置它为时已晚,因为组件已经 rendered/updated,但在此之前,它已经知道 selectedItems 中的更改(这实际上会导致错误,因为它试图根据新的 selected 项目访问 data 中的一些属性,但 data 仍然是旧的,因此它不包含那个。

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

更新 根据@brendangannon

的回答

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

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

所以我没有上面的 useEffect,而是这样的:

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

挂钩保持内部状态并在其自身 useEffect 中获取依赖于 selectedItems 的数据,并在 selectedItems 发生变化时根据这些项目获取新数据.

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

所以组件看起来像这样:

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 状态,并将其 return 存储到我的组件中以进行映射,例如selectedItemsForMapping。当 selectedBuckets 发生变化时,它仍然会使用旧的 selectedBucketsForMapping 来渲染陈旧的数据,之后它会 return 新的 selectedItemsForMapping 以及 loading===true,即我可以显示 'Loading...' 的方式,其余的已经在上面讨论过。

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

所以它可能看起来像:

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 所做的只是从状态中渲染一些东西,所以只要状态中有数据,它就可以安全地执行,无论它是否过时。当store中selectedItems发生变化时,组件会再次渲染(显示旧数据),然后立即设置一个'loading'状态,渲染加载UI,然后它会获取新数据,对其进行处理,将其设置为状态,然后呈现新数据。前两次渲染的速度足够快,您可能不会注意到陈旧数据的重新渲染;它会立即或多或少地显示 'loading' UI。

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

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