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是在其依赖项数组中声明的。
然而,我看到的是:
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;
}
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上面的函数所示)来复制什么?
为了澄清发生了什么并说明为什么添加“清理挂钩”无效,我在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
}
}
Run Code Online (Sandbox Code Playgroud)
以下是与我上面概述的四个步骤中的每一个相关的运行时日志消息:
useGet第一次被调用——它返回undefined是因为useEffect尚未触发并且尚未获取数据
User starting with userId=5
useGet starting
useGet returning
User rendering data 'undefined'
Run Code Online (Sandbox Code Playgroud)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)如果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)很快,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计划第二个之前返回陈旧数据仍然为时已晚。
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)
| 归档时间: |
|
| 查看次数: |
2375 次 |
| 最近记录: |