使用useEffect获取数据时避免旧数据 [英] Avoid old data when using useEffect to fetch data

查看:39
本文介绍了使用useEffect获取数据时避免旧数据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的问题是,当自定义钩子使用 useEffectuseState(例如获取数据)时,自定义钩子在依赖项之后返回陈旧数据(来自状态)更改但在 useEffect 被触发之前.

My problem is, when a custom hook uses useEffect with useState (e.g. to fetch data), the custom hook returns stale data (from the state), after dependencies change but before useEffect is fired.

你能提出一个正确/惯用的方法来解决这个问题吗?

Can you suggest a right/idiomatic way to resolve that?

我正在使用 React 文档和这些文章来指导我:

I'm using the React documentation and these articles to guide me:

我定义了一个函数,它使用 useEffect 并且旨在包装数据的获取——源代码是 TypeScript 而不是 JavaScript 但这并不重要——我认为这是按书":

I defined a function, which uses useEffect and which is meant to wrap the fetching of data -- the source code is TypeScript not JavaScript but that doesn't matter -- I think this is "by the book":

function useGet<TData>(getData: () => Promise<TData>): TData | undefined {

  const [data, setData] = React.useState<TData | undefined>(undefined);

  React.useEffect(() => {
    getData()
      .then((fetched) => setData(fetched));
  }, [getData]);

  // (TODO later -- handle abort of data fetching)

  return data;
}

应用程序根据 URL 路由到各种组件——例如这里是获取和显示用户配置文件数据的组件(当给定 URL 时,如 https://stackoverflow.com/users/49942/chrisw 其中 49942 是userId"):

The App routes to various components depending on the URL -- for example here is the component which fetches and displays user profile data (when given a URL like https://stackoverflow.com/users/49942/chrisw where 49942 is the "userId"):

export const User: React.FunctionComponent<RouteComponentProps> =
  (props: RouteComponentProps) => {

  // parse the URL to get the userId of the User profile to be displayed
  const userId = splitPathUser(props.location.pathname);

  // to fetch the data, call the IO.getUser function, passing userId as a parameter
  const getUser = React.useCallback(() => IO.getUser(userId), [userId]);

  // invoke useEffect, passing getUser to fetch the data
  const data: I.User | undefined = useGet(getUser);

  // use the data to render
  if (!data) {
    // TODO render a place-holder because the data hasn't been fetched yet
  } else {
    // TODO render using the data
  }
}

我认为这是标准的 -- 如果使用不同的 userId 调用组件,则 useCallback 将返回不同的值,因此 useEffect 将再次触发,因为getData 在其依赖数组中声明.

I think that's standard -- if the component is called with a different userId, then the useCallback will return a different value, and therefore the useEffect will fire again because getData is declared in its dependency array.

然而,我看到的是:

  1. useGet 第一次被调用——它返回 undefined 因为 useEffect 还没有被触发并且数据没有被触发尚未获取
  2. useEffect 触发,获取数据,组件使用获取的数据重新渲染
  3. 如果 userId 更改,则再次调用 useGet -- useEffect 将触发(因为 getData 已更改),但它还没有触发,所以现在 useGet 返回陈旧数据(即既不是新数据也不是 undefined)——所以组件用陈旧数据重新渲染
  4. 很快,useEffect 触发,组件使用新数据重新渲染
  1. useGet is called for the first time -- it returns undefined because the useEffect hasn't fired yet and the data hasn't been fetched yet
  2. useEffect fires, the data is fetched, and the component re-renders with fetched data
  3. If the userId changes then useGet is called again -- useEffect will fire (because getData has changed), but it hasn't fired yet, so for now useGet returns stale data (i.e. neither new data nor undefined) -- so the component re-renders with stale data
  4. Soon, useEffect fires, and the component re-renders with new data

在第 3 步中使用陈旧数据是不可取的.

Using stale data in step #3 is undesirable.

我怎样才能避免这种情况?有正常/惯用的方式吗?

How can I avoid that? Is there a normal/idiomatic way?

我在上面引用的文章中没有看到解决此问题的方法.

I don't see a fix for this in the articles I referenced above.

一个可能的解决方法(即这似乎有效)是重写 useGet 函数如下:

A possible fix (i.e. this seems to work) is to rewrite the useGet function as follows:

function useGet2<TData, TParam>(getData: () => Promise<TData>, param: TParam): TData | undefined {

  const [prev, setPrev] = React.useState<TParam | undefined>(undefined);
  const [data, setData] = React.useState<TData | undefined>(undefined);

  React.useEffect(() => {
    getData()
      .then((fetched) => setData(fetched));
  }, [getData, param]);

  if (prev !== param) {
    // userId parameter changed -- avoid returning stale data
    setPrev(param);
    setData(undefined);
    return undefined;
  }

  return data;
}

... 显然组件调用如下:

... which obviously the component calls like this:

  // invoke useEffect, passing getUser to fetch the data
  const data: I.User | undefined = useGet2(getUser, userId);

...但让我担心的是,我在已发表的文章中没有看到这一点——这样做有必要吗?这是最好的方法吗?

... but it worries me that I don't see this in the published articles -- is it necessary and the best way to do that?

此外,如果我要像那样显式返回 undefined,是否有一种简洁的方法来测试 useEffect 是否会触发,即测试其依赖关系数组变了?我是否必须通过将旧的 userId 和/或 getData 函数显式存储为状态变量(如上面的 useGet2 函数所示)来复制 useEffect 的作用?

Also if I'm going to explicitly return undefined like that, is there a neat way to test whether useEffect is going to fire, i.e. to test whether its dependency array has changed? Must I duplicate what useEffect does, by explicitly storing the old userId and/or getData function as a state variable (as shown in the useGet2 function above)?

为了澄清正在发生的事情并说明为什么添加清理挂钩"无效,我在 useEffectconsole.log 消息中添加了一个清理挂钩,因此源代码如下.

To clarify what's happening and to show why adding a "cleanup hook" is ineffective, I added a cleanup hook to useEffect plus console.log messages, so the source code is as follows.

function useGet<TData>(getData: () => Promise<TData>): TData | undefined {

  const [data, setData] = React.useState<TData | undefined>(undefined);

  console.log(`useGet starting`);

  React.useEffect(() => {
    console.log(`useEffect starting`);
    let ignore = false;
    setData(undefined);
    getData()
      .then((fetched) => {
        if (!ignore)
          setData(fetched)
      });
    return () => {
      console.log("useEffect cleanup running");
      ignore = true;
    }
  }, [getData, param]);

  console.log(`useGet returning`);
  return data;
}

export const User: React.FunctionComponent<RouteComponentProps> =
  (props: RouteComponentProps) => {

  // parse the URL to get the userId of the User profile to be displayed
  const userId = splitPathUser(props.location.pathname);

  // to fetch the data, call the IO.getUser function, passing userId as a parameter
  const getUser = React.useCallback(() => IO.getUser(userId), [userId]);

  console.log(`User starting with userId=${userId}`);

  // invoke useEffect, passing getUser to fetch the data
  const data: I.User | undefined = useGet(getUser);

  console.log(`User rendering data ${!data ? "'undefined'" : `for userId=${data.summary.idName.id}`}`);
  if (data && (data.summary.idName.id !== userId)) {
    console.log(`userId mismatch -- userId specifies ${userId} whereas data is for ${data.summary.idName.id}`);
    data = undefined;
  }

  // use the data to render
  if (!data) {
    // TODO render a place-holder because the data hasn't been fetched yet
  } else {
    // TODO render using the data
  }
}

这里是与我上面概述的四个步骤中的每一个相关的运行时日志消息:

And here are the run-time log messages associated with each of the four steps I outlined above:

  1. useGet 第一次被调用——它返回 undefined 因为 useEffect 还没有被触发并且数据还没有被提取

  1. useGet is called for the first time -- it returns undefined because the useEffect hasn't fired yet and the data hasn't been fetched yet

User starting with userId=5
useGet starting
useGet returning
User rendering data 'undefined'

  • useEffect 触发,获取数据,组件使用获取的数据重新渲染

  • useEffect fires, the data is fetched, and the component re-renders with fetched data

    useEffect starting
    mockServer getting /users/5/unknown
    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data for userId=5
    

  • 如果 userId 改变,则 useGet 再次被调用 -- useEffect 将触发(因为 getData> 已更改),但尚未触发,因此现在 useGet 返回陈旧数据(即既不是新数据也不是 undefined)——因此组件重新渲染旧数据

  • If the userId changes then useGet is called again -- useEffect will fire (because getData has changed), but it hasn't fired yet, so for now useGet returns stale data (i.e. neither new data nor undefined) -- so the component re-renders with stale data

    User starting with userId=1
    useGet starting
    useGet returning
    User rendering data for userId=5
    userId mismatch -- userId specifies 1 whereas data is for 5
    

  • 很快,useEffect 触发,组件使用新数据重新渲染

  • Soon, useEffect fires, and the component re-renders with new data

    useEffect cleanup running
    useEffect starting
    UserProfile starting with userId=1
    useGet starting
    useGet returning
    User rendering data 'undefined'
    mockServer getting /users/1/unknown
    User starting with userId=1
    useGet starting
    useGet returning
    User rendering data for userId=1
    

  • 总而言之,清理确实作为第 4 步的一部分运行(可能是在计划第二个 useEffect 时),但要防止在第 3 步结束时返回陈旧数据仍然为时已晚,在 userId 更改之后和在安排第二个 useEffect 之前.

    In summary the cleanup does run as part of step 4 (probably when the 2nd useEffect is scheduled), but that's still too late to prevent the returning of stale data at the end of step 3, after the userId changes and before the second useEffect is scheduled.

    推荐答案

    在回复中 在 Twitter 上,@dan_abramov 写道,我的 useGet2 解决方案或多或少是规范的:

    In a reply on Twitter, @dan_abramov wrote that my useGet2 solution is more-or-less canonical:

    如果您在渲染内部 [和 useEffect] 外部执行 setState 以摆脱陈旧状态,它不应该产生用户可观察的中间渲染.它将同步安排另一个重新渲染.所以你的解决方案应该足够了.

    If you do setState inside of render [and outside of useEffect] to get rid of stale state, it shouldn't ever produce a user-observable intermediate render. It will schedule another re-render synchronously. So your solution should be sufficient.

    这是派生状态的惯用解决方案,在您的示例中,状态是从 ID 派生的.

    It's the idiomatic solution for derived state, and in your example state is derived from ID.

    如何实现 getDerivedStateFromProps?

    (从长远来看,将有一个完全不同的数据获取解决方案,它不涉及效果或设置状态.但我正在描述我们今天所拥有的.)

    (In longer term there will be a different solution for data fetching altogether that doesn’t involve effects or setting state. But I'm describing what we have today.)

    该链接引用的文章 -- 您可能不需要派生状态 -- 解释了问题的根本原因.

    The article referenced from that link -- You Probably Don't Need Derived State -- explains what the root cause of the problem is.

    它说问题是,期待受控"的状态传入 User(即 userId)以匹配不受控制"内部状态(即效果返回的数据).

    It says that the problem is, expecting the "controlled" state passed-in to User (i.e. the userId) to match the "uncontrolled" internal state (i.e. the data returned by the effect).

    最好的办法是依赖其中一个,而不是混合它们.

    The better thing to do is to depend on one or the other but not mix them.

    所以我想我应该在内部和/或数据中返回一个 userId.

    So I suppose I should return a userId inside and/or with the data.

    这篇关于使用useEffect获取数据时避免旧数据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆