使用useEffect获取数据时避免旧数据

Chr*_*isW 5 reactjs react-hooks

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

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


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

我定义了一个函数,它使用useEffect并用于包装数据的获取——源代码是 TypeScript 而不是 JavaScript 但这并不重要——我认为这是“书上写的”:

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;
}
Run Code Online (Sandbox Code Playgroud)

应用程序根据 URL 路由到各种组件——例如,这里是获取和显示用户配置文件数据的组件(当给定 URL 时,例如“userId”/sf/users/3495971/在哪里49942):

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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

然而,我看到的是:

  1. useGet第一次被调用——它返回undefined是因为useEffect尚未触发并且尚未获取数据
  2. useEffect 触发,获取数据,组件使用获取的数据重新渲染
  3. 如果userId更改然后useGet再次调用 -useEffect将触发(因为getData已更改),但尚未触发,因此现在useGet返回陈旧数据(即既不是新数据也不是undefined)-因此组件使用陈旧数据重新呈现
  4. 很快,useEffect触发,组件用新数据重新渲染

在步骤 3 中使用陈旧数据是不可取的。

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

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


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

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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

  1. useGet第一次被调用——它返回undefined是因为useEffect尚未触发并且尚未获取数据

    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data 'undefined'
    
    Run Code Online (Sandbox Code Playgroud)
  2. useEffect 触发,获取数据,组件使用获取的数据重新渲染

    useEffect starting
    mockServer getting /users/5/unknown
    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data for userId=5
    
    Run Code Online (Sandbox Code Playgroud)
  3. 如果userId更改然后useGet再次调用 -useEffect将触发(因为getData已更改),但尚未触发,因此现在useGet返回陈旧数据(即既不是新数据也不是undefined)-因此组件使用陈旧数据重新呈现

    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
    
    Run Code Online (Sandbox Code Playgroud)
  4. 很快,useEffect触发,组件用新数据重新渲染

    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
    
    Run Code Online (Sandbox Code Playgroud)

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

Eda*_*rit 0

useEffect每次在内部调用时都应该初始化数据useGet

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

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

  React.useEffect(() => {

    setData(undefinded) // initializing data to undefined

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

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

  return data;
}
Run Code Online (Sandbox Code Playgroud)