Recoil通过数据流图将数据及衍生数据映射给组件,其真正强大之处是可异步,我们可以react组件中使用异步函数。Recoil允许我们在数据流图的seletors中无缝混合使用同步和异步函数。从selector 的get
回调中返回Promise对象,接口保持不变。
selectors可以用作将异步数据合并到recoil数据流图中的一种方法。记住selectors是纯函数:对于给定的一组输入,它们应该总是产生相同的结果。这一点非常重要,因为选择器计算可能会执行一次或多次,可能会重新启动,也可能会被缓存。因此,selectors是建模只读DB查询的好方法,这重复查询可以提供一致的数据。如果您想同步本地和服务器状态,那么请参阅异步状态同步或状态持久。
只读数据
同步举例
一个简单的同步 atom 和 selector 来获取user name.
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或使用异步函数。当依赖变化时 selector会被重新评估并执行新的查询,查询结果会被缓存,因此对于每个唯一的输入,查询只执行一次。
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>;
}
selector的接口是相同的,因此使用此selector的组件不需要关心它是由同步原子状态、派生选择器状态还是异步查询支持的。
但是,由于React呈现函数是同步的,所以在Promise解析之前它会呈现什么?Recoil设计与React Suspense一起处理待处理数据。用Suspense 边界包装组件,将捕获任何仍然挂起的派生组件,并回退给UI呈现。
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}
错误处理 <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 helper来完成:
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);
const friends = [];
for (const friendID of friendList) {
const friendInfo = get(userInfoQuery(friendID));
friends.push(friendInfo);
}
return friends;
},
});
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使用一个查询来获取每个朋友的信息。但是,通过在循环中这样做,它们本质上是序列化的。如果查找速度快,也许没问题,如果开销较大,您可以使用并发helper程序,如waitForAll、waitForNone或waitForAny,以并行地运行它们或处理部分结果,它们接受数组和命名对象作为依赖。
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friends = get(waitForAll(
friendList.map(friendID => userInfoQuery(friendID))
));
return friends;
},
});
Pre-Fetching
出于性能考虑,您可能希望在呈现之前获取数据,这样查询就可以在我们开始渲染的时候进行。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>
);
}
不使用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;
}
}
读写数据
与服务器同步状态
我们可以订阅远程状态中的异步更改,并更新atom值以匹配。
可以在react useEffect
实现。
function CurrentUserIDSubscription() {
const setCurrentUserID = useSetRecoilState(currentUserIDState);
useEffect(() => {
RemoteStateAPI.subscribeToCurrentUserID(setCurrentUserID);
// Specify how to cleanup after this effect
return function cleanup() {
RemoteServerAPI.unsubscribeFromCurrentUserID(setCurrentUserID);
};
}, []);
return null;
}
function MyApp() {
return (
<RecoilRoot>
<CurrentUserIDSubscription />
<CurrentUserInfo />
</RecoilRoot>
);
}
双向同步
您还可以同步状态,以便在服务器上更新本地更改。注意,这是一个简化的示例,请注意避免反馈循环。
function CurrentUserIDSubscription() {
const [currentUserID, setCurrentUserID] = useRecoilState(currentUserIDState);
const knownServerCurrentUserID = useRef(currentUserID);
// Subscribe server changes to update atom state
useEffect(() => {
function handleUserChange(id) {
knownServerCurrentUserID.current = id;
setCurrentUserID(id);
}
RemoteStateAPI.subscribeToCurrentUserID(handleUserChange);
// Specify how to cleanup after this effect
return function cleanup() {
RemoteServerAPI.unsubscribeFromCurrentUserID(handleUserChange);
};
}, [knownServerCurrentUserID]);
// Subscribe atom changes to update server state
useEffect(() => {
if (currentUserID !== knownServerCurrentUserID.current) {
knownServerCurrentID.current = currentUserID;
RemoteServerAPI.updateCurrentUser(currentUserID);
}
}, [currentUserID, knownServerCurrentUserID.current]);
return null;
}
带参状态同步
还可以使用atomFamily helper根据参数同步本地状态。注意,这个示例钩子的每次调用都将创建一个订阅,因此要注意避免冗余使用。
const friendStatusState = atomFamily({
key: 'Friend Status',
default: 'offline',
});
function useFriendStatusSubscription(id) {
const setStatus = useSetRecoilState(friendStatusState(id));
useEffect(() => {
RemoteStateAPI.subscribeToFriendStatus(id, setStatus);
// Specify how to cleanup after this effect
return function cleanup() {
RemoteServerAPI.unsubscribeFromFriendStatus(id, setStatus);
};
}, []);
}
数据流图
使用原子来表示远态的一个优点是,你可以使用它作为其他导出态的输入。下面的示例将显示基于当前服务器状态的当前用户和好友列表。如果服务器改变了当前用户,它将重新呈现整个列表,如果它只改变了一个朋友的状态,那么只有该列表条目将被重新呈现。如果单击列表项,它将在本地更改当前用户并更新服务器状态。
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async ({get}) => {
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 friendColorState = selectorFamily({
key: 'FriendColor',
get: friendID => ({get}) => {
const [status] = useRecoilState(friendStatusState(friendID));
return status === 'offline' ? 'red' : 'green';
}
})
function FriendStatus({friendID}) {
useFriendStatusSubscription(friendID);
const [status] = useRecoilState(friendStatusState(friendID));
const [color] = useRecoilState(friendColorState(friendID));
const [friend] = useRecoilState(userInfoQuery(friendID));
return (
<div style={{color}}>
Name: {friend.name}
Status: {status}
</div>
);
}
function CurrentUserInfo() {
const {name, friendList} = useRecoilValue(currentUserInfoQuery)
const setCurrentUserID = useSetRecoilState(currentUserIDState);
return (
<div>
<h1>{name}</h1>
<ul>
{friendList.map(friendID =>
<li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
<React.Suspense fallback={<div>Loading...</div>}>
<FriendStatus friendID={friendID} />
</React.Suspense>
</li>
)}
</ul>
</div>
);
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserIDSubscription />
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
网友评论