仅在重新渲染组件时才反应状态更新

Posted

技术标签:

【中文标题】仅在重新渲染组件时才反应状态更新【英文标题】:React state updates only when component is re-rendered 【发布时间】:2021-10-15 11:15:02 【问题描述】:

我试图在每次调用 handleScroll() 时将 React 状态 setMoviesPage 的值增加 1。 我面临的问题是状态 setMoviesPage 仅更新组件重新呈现(通过导航到不同的 url 并返回)。

这是我的代码:

  const [moviesPage, setMoviesPage] = useState(1);


  async function handleScroll() 
    if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
    await setIsFetching(true);
    setMoviesPage(moviesPage + 1);
  

  useEffect(() => 
    if (!isFetching) return;
    fetchMovies(String(moviesPage))
    .then(prevState => updateMovies([...prevState]))
    .catch(() => updateMovies([]));
    
    setIsFetching(false);   
  , [isFetching, setIsFetching, moviesPage, updateMovies]);

如何解决?谢谢

【问题讨论】:

是调用setMoviesPage() 自动触发重新渲染的问题吧? await setIsFetching(true); 在做什么? isFetching 是其他 React 状态吗?如果是这样,React 状态更新是异步处理的,你不能等待它们。 @sungryeol 反过来,moviesPage 值仅在重新渲染组件时才会增加(通过导航到不同的 url 并返回) @DrewReese 是的,isFetching 是调用 useEffect() 所需的另一个状态(根据我的代码 sn-p),因为 isFetching 是 useEffect() 的依赖项。如果我可以等到setMoviesPageisFetching 被处理,我如何每次到达页面底部时增加moviesPage 的值(我想实现无限滚动) 那么你是否只需要在获取完成时将moviesPage 加一?或者,当然....有一个事件处理程序,当页面底部被点击时,moviesPage 的值会增加一个? 【参考方案1】:

问题

似乎在获取电影完成后,您只需覆盖现有状态。

fetchMovies(String(moviesPage))
  .then(prevState => updateMovies([...prevState])) // <-- overwrite state
  .catch(() => updateMovies([]));

使用函数组件和useState 挂钩,状态更新不会与现有状态合并,您必须 手动管理。

注意

与类组件中的setState 方法不同,useState 不会自动合并更新对象。你可以复制这个 通过将函数更新器形式与对象传播相结合的行为 语法:

const [state, setState] = useState();
setState(prevState => 
  // Object.assign would also work
  return ...prevState, ...updatedValues;
);

解决方案

更新滚动处理程序中的moviesPage

function handleScroll() 
  if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
  setMoviesPage(moviesPage + 1);

将获取逻辑分解为实用函数。使用功能状态更新从以前的状态进行更新,追加新数据到 new 数组引用。将加载状态的重置移入 Promise 链,使其在链的末尾正确重置,否则它只会取消开始加载的真实更新。 p>

const fetchNextMoviesPage = (page) => 
  if (!isFetching) 
    setIsFetching(true); // <-- start loading
    fetchMovies(String(page))
      .then(nextPage => 
        updateMovies(movies => movies.concat(nextPage)); // <-- append next page
      )
      .catch(() => updateMovies([]))
      .finally(() => setIsFetching(false)); // <-- end loading
  
;

useEffect(() => 
  fetchNextMoviesPage(moviesPage); 
, [moviesPage]);

【讨论】:

Drew,您提出的解决方案确实很优雅,非常感谢您的帮助。实际上,通过阅读您的代码,我学到了很多东西!但是,在实施您的解决方案后,项目数组仍在被重写,window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight 在它应该之前被执行。我将尝试使用 Intersection 观察器实现无限滚动,希望我会成功! @gufox 是的,使用 Intersection Observer 是在元素进入(阈值)视图时触发回调的方法。我怀疑你会有更好的运气/可靠性。干杯。 我已经使用 Intersection Observer 重新实现了无限滚动,这个解决方案似乎确实更可靠。此外,App() 组件中还有一个错误(因此您无法注意到该问题) - 我曾经为那里的前 20 个元素调用 fetchMovies()。很抱歉,再次感谢您的帮助!【参考方案2】:

当第一次查看你的代码时,我真正想告诉你的是你的 useEffect 似乎不是最佳实践。

这里我给你修改了一下,结构更好。

const fetchMovie = () => 
   setIsFetching(true)
   fetchMovies(String(moviesPage))
    .then(prevState => updateMovies([...prevState]))
    .catch(() => updateMovies([]));
   setIsFetching(false)


useEffect(() => 
   fetchMovie
, [moviesPage]);

请注意,您只需要添加依赖项数组即可使用所有更改的值来触发您在其中的操作。有时 linter 也会给你一些误报警告。

希望这个答案可以帮助您解决问题。

【讨论】:

感谢您的回答!我已经实现了您建议的结构,现在我的代码更有条理。如果我收到一个 linting 警告说我需要向useEffect() 添加更多依赖项,我应该使用// eslint-disable-next-line react-hooks/exhaustive-deps 吗?但是问题依然存在

以上是关于仅在重新渲染组件时才反应状态更新的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法让上下文仅在其数据更改时重新渲染?

更新 Redux 存储时反应组件不重新渲染

React Native this.setState() 仅在再次与组件交互后重新渲染

防止子级在更新父级状态时重新渲染,使用父级方法作为本机反应的道具

如何在反应中限制经常重新渲染的组件

Vue中重新渲染组件的方法总结