跳至主要内容

异步数据查询

Recoil 提供了一种通过数据流图将状态和派生状态映射到 React 组件的方法。真正强大的是,图中的函数也可以是异步的。这使得在同步 React 组件渲染函数中使用异步函数变得容易。Recoil 允许你在选择器数据流图中无缝地混合同步和异步函数。只需从选择器 get 回调中返回一个 Promise 到值,而不是返回值本身,接口保持完全相同。因为这些只是选择器,其他选择器也可以依赖它们来进一步转换数据。

选择器可以用作将异步数据合并到 Recoil 数据流图中的一种方式。请记住,选择器代表“幂等”函数:对于给定的输入集,它们应该始终产生相同的结果(至少对于应用程序的生命周期)。这很重要,因为选择器评估可能会被缓存、重新启动或执行多次。因此,选择器通常是模拟只读数据库查询的好方法。对于可变数据,可以使用 查询刷新。或者为了同步可变状态、持久化状态或其他副作用,请考虑 Atom Effects API 或 Recoil 同步库.

同步示例

例如,这是一个简单的同步 atomselector 来获取用户名

const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
});

const currentUserNameState = selector({
key: 'CurrentUserName',
get: ({get}) => {
return tableOfUsers[get(currentUserIDState)].name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameState);
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<CurrentUserInfo />
</RecoilRoot>
);
}

异步示例

如果用户名存储在我们需要查询的某个数据库中,我们只需要返回一个 Promise 或使用一个 async 函数。如果任何依赖项发生变化,选择器将重新评估并执行新的查询。结果被缓存,因此查询只会在每个唯一的输入上执行一次。

const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}

选择器的接口是相同的,因此使用此选择器的组件不需要关心它是否是由同步 atom 状态、派生选择器状态或异步查询支持的!

但是,由于 React 渲染函数是同步的,在 Promise 解决之前它会渲染什么?Recoil 被设计为与 React Suspense 一起工作以处理挂起的數據。用 Suspense 边界包装你的组件将捕获任何仍然挂起的后代并渲染一个备用 UI

function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}

错误处理

但是如果请求出现错误?Recoil 选择器也可以抛出错误,如果组件尝试使用该值,则会抛出这些错误。这可以用 React <ErrorBoundary> 捕获。例如

const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}

带参数的查询

有时你希望能够根据不是基于派生状态的参数进行查询。例如,你可能希望根据组件 props 进行查询。你可以使用 selectorFamily() 帮助程序来实现这一点

const userNameQuery = selectorFamily({
key: 'UserName',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.name;
},
});

function UserInfo({userID}) {
const userName = useRecoilValue(userNameQuery(userID));
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<UserInfo userID={1}/>
<UserInfo userID={2}/>
<UserInfo userID={3}/>
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}

数据流图

请记住,通过将查询建模为选择器,我们可以构建一个混合了状态、派生状态和查询的数据流图!该图将随着状态更新自动更新并重新渲染 React 组件。

以下示例将渲染当前用户的姓名及其朋友列表。如果单击朋友的姓名,他们将成为当前用户,并且姓名和列表将自动更新。

const currentUserIDState = atom({
key: 'CurrentUserID',
default: null,
});

const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response;
},
});

const currentUserInfoQuery = selector({
key: 'CurrentUserInfoQuery',
get: ({get}) => get(userInfoQuery(get(currentUserIDState))),
});

const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
return friendList.map(friendID => get(userInfoQuery(friendID)));
},
});

function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);
const setCurrentUserID = useSetRecoilState(currentUserIDState);
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
);
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}

并发请求

如果你注意到上面的示例,friendsInfoQuery 使用查询来获取每个朋友的信息。但是,通过在循环中执行此操作,它们本质上是串行的。如果查找速度很快,也许可以。如果它很昂贵,可以使用并发帮助程序(例如 waitForAll)并行运行它们。此帮助程序接受依赖项的数组和命名对象。

const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friends = get(waitForAll(
friendList.map(friendID => userInfoQuery(friendID))
));
return friends;
},
});

你可以使用 waitForNone 来处理使用部分数据的 UI 增量更新

const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friendLoadables = get(waitForNone(
friendList.map(friendID => userInfoQuery(friendID))
));
return friendLoadables
.filter(({state}) => state === 'hasValue')
.map(({contents}) => contents);
},
});

预取

出于性能原因,你可能希望在渲染之前启动获取。这样,查询就可以在开始渲染时进行。React 文档 提供了一些示例。此模式也适用于 Recoil。

让我们更改上面的示例,以便在用户单击更改用户的按钮后立即启动对下一个用户信息的获取

function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);

const changeUser = useRecoilCallback(({snapshot, set}) => userID => {
snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info
set(currentUserIDState, userID); // change current user to start new render
});

return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => changeUser(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
);
}

请注意,此预取通过触发 selectorFamily() 来启动异步查询并填充选择器的缓存。如果你正在使用 atomFamily(),则可以通过设置原子或依赖原子效果进行初始化,然后你应该使用 useRecoilTransaction_UNSTABLE() 而不是 useRecoilCallback(),因为尝试设置提供的 Snapshot 的状态将对主机 <RecoilRoot> 中的实时状态没有影响。

查询默认 Atom 值

一种常见的模式是使用 atom 来表示本地可编辑状态,但使用 promise 来查询默认值

const currentUserIDState = atom({
key: 'CurrentUserID',
default: myFetchCurrentUserID(),
});

或者使用选择器来推迟查询或依赖其他状态。请注意,当使用选择器时,默认原子值将保持动态,并随着选择器更新而更新,直到原子被用户显式设置。

const UserInfoState = atom({
key: 'UserInfo',
default: selector({
key: 'UserInfo/Default',
get: ({get}) => myFetchUserInfo(get(currentUserIDState)),
}),
});

这也可以与 atom 家族一起使用

const userInfoState = atomFamily({
key: 'UserInfo',
default: id => myFetchUserInfo(id),
});
const userInfoState = atomFamily({
key: 'UserInfo',
default: selectorFamily({
key: 'UserInfo/Default',
get: id => ({get}) => myFetchUserInfo(id, get(paramsState)),
}),
});

如果你希望双向同步数据,请考虑使用 atom effects.

没有 React Suspense 的异步查询

在渲染时处理挂起的异步选择器,使用 React Suspense 不是必需的。你也可以使用 useRecoilValueLoadable() 钩子来确定当前状态

function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}

查询刷新

当使用选择器来模拟数据查询时,选择器评估应该始终为给定状态提供一致的值。选择器代表从其他原子和选择器状态派生的状态。因此,选择器评估函数应该对于给定输入是幂等的,因为它可能被缓存或执行多次。但是,如果选择器从数据查询中获取数据,那么它们可能需要重新查询以便使用更新的数据刷新或在失败后重试。有几种方法可以实现这一点

useRecoilRefresher()

useRecoilRefresher_UNSTABLE() 钩子可用于获取一个回调,你可以调用它来清除任何缓存并强制重新评估。

const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.data;
}
})

function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRecoilRefresher_UNSTABLE(userInfoQuery(currentUserID));

return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={() => refreshUserInfo()}>Refresh</button>
</div>
);
}

使用请求 ID

选择器评估应该根据其输入(依赖状态或家族参数)为给定状态提供一致的值。因此,你可以将请求 ID 作为家族参数或依赖项添加到你的查询中。例如

const userInfoQueryRequestIDState = atomFamily({
key: 'UserInfoQueryRequestID',
default: 0,
});

const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async ({get}) => {
get(userInfoQueryRequestIDState(userID)); // Add request ID as a dependency
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.data;
},
});

function useRefreshUserInfo(userID) {
const setUserInfoQueryRequestID = useSetRecoilState(userInfoQueryRequestIDState(userID));
return () => {
setUserInfoQueryRequestID(requestID => requestID + 1);
};
}

function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRefreshUserInfo(currentUserID);

return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={refreshUserInfo}>Refresh</button>
</div>
);
}

使用 Atom

另一个选择是使用 atom 而不是选择器来模拟查询结果。你可以根据你的刷新策略使用命令式地用新的查询结果更新 atom 状态。

const userInfoState = atomFamily({
key: 'UserInfo',
default: userID => fetch(userInfoURL(userID)),
});

// React component to refresh query
function RefreshUserInfo({userID}) {
const refreshUserInfo = useRecoilCallback(({set}) => async id => {
const userInfo = await myDBQuery({userID});
set(userInfoState(userID), userInfo);
}, [userID]);

// Refresh user info every second
useEffect(() => {
const intervalID = setInterval(refreshUserInfo, 1000);
return () => clearInterval(intervalID);
}, [refreshUserInfo]);

return null;
}

请注意,原子目前不支持接受 Promise 作为新值。因此,如果你希望这种行为,你目前无法将原子置于挂起状态以供 React Suspense 使用,直到查询刷新挂起。但是,你可以存储一个对象,该对象手动编码当前加载状态以及实际结果,以便显式地处理这种情况。

还可以考虑使用 atom effects 来同步原子的查询。

从错误消息重试查询

这是一个有趣的示例,用于根据在 <ErrorBoundary> 中抛出和捕获的错误查找和重试查询

function QueryErrorMessage({error}) {
const snapshot = useRecoilSnapshot();
const selectors = useMemo(() => {
const ret = [];
for (const node of snapshot.getNodes_UNSTABLE({isInitialized: true})) {
const {loadable, type} = snapshot.getInfo_UNSTABLE(node);
if (loadable != null && loadable.state === 'hasError' && loadable.contents === error) {
ret.push(node);
}
}
return ret;
}, [snapshot, error]);
const retry = useRecoilCallback(({refresh}) =>
() => selectors.forEach(refresh),
[selectors],
);

return selectors.length > 0 && (
<div>
Error: {error.toString()}
Query: {selectors[0].key}
<button onClick={retry}>Retry</button>
</div>
);
}