我的问题是,当自定义挂钩使用useEffect
with useState
(例如,为了获取数据),在依赖项更改之后但在 useEffect 被触发之前,自定义挂钩会返回过时的数据(来自状态)。
您能建议一种正确/惯用的方法来解决这个问题吗?
我正在使用 React 文档和这些文章来指导我:
- useEffect 完整指南 https://overreacted.io/a-complete-guide-to-useeffect/
- 如何使用 React Hooks 获取数据? https://www.robinwieruch.de/react-hooks-fetch-data/
我定义了一个函数,它使用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 路由到各种组件 - 例如,这里是获取并显示用户配置文件数据的组件(当给定像这样的 URL 时https://stackoverflow.com/users/49942/chrisw
where 49942
是“用户 ID”):
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
在其依赖项数组中声明。
然而,我看到的是:
-
useGet
第一次被调用——它返回undefined
因为useEffect
尚未触发且数据尚未获取
-
useEffect
触发,获取数据,并且组件使用获取的数据重新渲染
- If the
userId
然后改变useGet
再次被召唤——useEffect
会开火(因为getData
已更改),但尚未触发,所以现在useGet
返回陈旧数据(即既不返回新数据也不返回undefined
) -- 因此组件会使用过时的数据重新渲染
- Soon,
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
将要触发,即测试其依赖项数组是否已更改?我必须复制什么吗useEffect
通过将旧的 userId 和/或 getData 函数显式存储为状态变量(如useGet2
上面的函数)?
为了澄清发生的情况并说明为什么添加“清理钩子”无效,我添加了一个清理钩子useEffect
plus console.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
}
}
以下是与我上面概述的四个步骤中的每一个相关的运行时日志消息:
-
useGet
第一次被调用——它返回undefined
因为useEffect
尚未触发且数据尚未获取
User starting with userId=5
useGet starting
useGet returning
User rendering data 'undefined'
-
useEffect
触发,获取数据,并且组件使用获取的数据重新渲染
useEffect starting
mockServer getting /users/5/unknown
User starting with userId=5
useGet starting
useGet returning
User rendering data for userId=5
-
If the 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
-
Soon, 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 结束时返回陈旧数据。userId
变化和第二个之前useEffect
已安排。