我的问题是,当自定义钩子将useEffect
与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;
}
该应用程序根据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
是在其依赖项数组中声明。
但是,我看到的是:
useGet
首次被调用-返回undefined
,因为useEffect
尚未触发,并且尚未提取数据useEffect
触发,获取数据,并且组件使用获取的数据重新渲染userId
发生更改,则再次调用useGet
-useEffect
将触发(因为getData
已更改),但尚未触发,因此现在useGet
返回陈旧的数据(即既不是新数据也不是undefined
)-因此组件将使用陈旧的数据重新渲染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
的作用?
为澄清情况并说明为什么添加“清理钩子”无效,我在useEffect
和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
如果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
很快,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
被安排之前。
答案 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。