Моя проблема в том, что когда пользовательский хук использует 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-адреса - например, вот компонент, который выбирает и отображает данные профиля пользователя (когда ему присваивается URL-адрес, например https://stackoverflow.com/users/49942/chrisw
, где 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
, то есть проверить, изменился ли его массив зависимостей? Должен ли я дублировать то, что делает 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
}
}
И вот сообщения журнала времени выполнения, связанные с каждым из четырех шагов, которые я обрисовал выше:
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 (возможно, когда запланировано 2-е useEffect
), но еще слишком поздно, чтобы предотвратить возврат устаревших данных в конце шага 3, после userId
изменения и до запланированного второго useEffect
.