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

时间:2019-05-12 05:53:23

标签: reactjs react-hooks

我的问题是,当自定义钩子将useEffectuseState一起使用时(例如,获取数据),自定义钩子会在依赖项更改之后但在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;
}

该应用程序根据URL路由到各种组件-例如,这里是获取和显示用户个人资料数据的组件(当给定类似https://stackoverflow.com/users/49942/chrisw的URL时,其中49942是“ 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是在其依赖项数组中声明。

但是,我看到的是:

  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;
}

...很明显,组件调用如下:

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

...但是令我担心的是,我没有在已发表的文章中看到这一点-是否有必要并且是做到这一点的最佳方法?

此外,如果我要像这样显式返回undefined,是否有一种简便的方法来测试useEffect是否将要启动,即测试其依赖项数组是否已更改?我是否必须通过将旧的userId和/或getData函数显式存储为状态变量(如上面的useEffect函数所示)来复制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
  }
}

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

  1. useGet首次被调用-返回undefined,因为useEffect尚未触发,并且尚未提取数据

    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data 'undefined'
    
  2. useEffect触发,获取数据,并且组件使用获取的数据重新渲染

    useEffect starting
    mockServer getting /users/5/unknown
    User starting with userId=5
    useGet starting
    useGet returning
    User rendering data for userId=5
    
  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
    
  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
    

总而言之,清除操作确实是第4步的一部分(可能是安排了第二个useEffect时),但是为防止在第3步后{ {1}}发生了变化,并且在第二个userId被安排之前。

2 个答案:

答案 0 :(得分: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;
}

答案 1 :(得分:0)

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

  

如果您在render [和useEffect之外)中使用setState来摆脱过时的状态,则不应生成用户可观察的中间渲染。它将同步安排另一个重新渲染。因此您的解决方案应该足够了。

     

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

     

How do I implement getDerivedStateFromProps?

     

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

从该链接引用的文章You Probably Don't Need Derived State解释了问题的根本原因。

它说问题在于,期望传递给User的“受控”状态(即userId)与“不受控制”的内部状态(即效果返回的数据)相匹配。

更好的办法是依靠一个或另一个,而不要混在一起。

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