Recoil通過數(shù)據(jù)流圖將數(shù)據(jù)及衍生數(shù)據(jù)映射給組件胁勺,其真正強(qiáng)大之處是可異步,我們可以react組件中使用異步函數(shù)怔昨。Recoil允許我們在數(shù)據(jù)流圖的seletors中無縫混合使用同步和異步函數(shù)雀久。從selector 的get
回調(diào)中返回Promise對象,接口保持不變趁舀。
selectors可以用作將異步數(shù)據(jù)合并到recoil數(shù)據(jù)流圖中的一種方法赖捌。記住selectors是純函數(shù):對于給定的一組輸入,它們應(yīng)該總是產(chǎn)生相同的結(jié)果矮烹。這一點(diǎn)非常重要越庇,因?yàn)檫x擇器計(jì)算可能會執(zhí)行一次或多次,可能會重新啟動奉狈,也可能會被緩存卤唉。因此,selectors是建模只讀DB查詢的好方法仁期,這重復(fù)查詢可以提供一致的數(shù)據(jù)桑驱。如果您想同步本地和服務(wù)器狀態(tài),那么請參閱異步狀態(tài)同步或狀態(tài)持久蟀拷。
只讀數(shù)據(jù)
同步舉例
一個簡單的同步 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>
);
}
異步舉例
如果用戶名存儲在我們需要查詢的數(shù)據(jù)庫中碰纬,我們所需要做的就是返回一個Promise或使用異步函數(shù)。當(dāng)依賴變化時 selector會被重新評估并執(zhí)行新的查詢问芬,查詢結(jié)果會被緩存悦析,因此對于每個唯一的輸入,查詢只執(zhí)行一次此衅。
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的組件不需要關(guān)心它是由同步原子狀態(tài)、派生選擇器狀態(tài)還是異步查詢支持的挡鞍。
但是骑歹,由于React呈現(xiàn)函數(shù)是同步的,所以在Promise解析之前它會呈現(xiàn)什么?Recoil設(shè)計(jì)與React Suspense一起處理待處理數(shù)據(jù)墨微。用Suspense 邊界包裝組件道媚,將捕獲任何仍然掛起的派生組件,并回退給UI呈現(xiàn)翘县。
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>
);
}
帶參查詢
有時希望能夠基于參數(shù)進(jìn)行查詢最域,而不僅僅是基于派生狀態(tài)。例如锈麸,可能想要基于組件props進(jìn)行查詢镀脂。你以使用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>
);
}
數(shù)據(jù)流圖
當(dāng)狀態(tài)更新時,數(shù)據(jù)流圖將自動更新并重新呈現(xiàn)React組件忘伞。
下面的示例將呈現(xiàn)當(dāng)前用戶的名稱及其好友列表薄翅。如果一個朋友的名字被點(diǎn)擊沙兰,他們將成為當(dāng)前的用戶,名字和列表將自動更新翘魄。
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使用一個查詢來獲取每個朋友的信息。但是熟丸,通過在循環(huán)中這樣做训措,它們本質(zhì)上是序列化的。如果查找速度快光羞,也許沒問題,如果開銷較大绩鸣,您可以使用并發(fā)helper程序,如waitForAll纱兑、waitForNone或waitForAny呀闻,以并行地運(yùn)行它們或處理部分結(jié)果,它們接受數(shù)組和命名對象作為依賴。
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friends = get(waitForAll(
friendList.map(friendID => userInfoQuery(friendID))
));
return friends;
},
});
Pre-Fetching
出于性能考慮潜慎,您可能希望在呈現(xiàn)之前獲取數(shù)據(jù)捡多,這樣查詢就可以在我們開始渲染的時候進(jìn)行。React文檔給出了一些例子铐炫,這種模式也適用于Recoil垒手。
我們改變上面的例子,當(dāng)用戶點(diǎn)擊改變用戶的按鈕時倒信,就開始獲取下一個用戶信息:
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()鉤子來確定渲染期間的狀態(tài)。
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;
}
}
讀寫數(shù)據(jù)
與服務(wù)器同步狀態(tài)
我們可以訂閱遠(yuǎn)程狀態(tài)中的異步更改鳖悠,并更新atom值以匹配榜掌。
可以在react useEffect
實(shí)現(xiàn)。
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>
);
}
雙向同步
您還可以同步狀態(tài)乘综,以便在服務(wù)器上更新本地更改憎账。注意,這是一個簡化的示例卡辰,請注意避免反饋循環(huán)胞皱。
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;
}
帶參狀態(tài)同步
還可以使用atomFamily helper根據(jù)參數(shù)同步本地狀態(tài)。注意九妈,這個示例鉤子的每次調(diào)用都將創(chuàng)建一個訂閱朴恳,因此要注意避免冗余使用。
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);
};
}, []);
}
數(shù)據(jù)流圖
使用原子來表示遠(yuǎn)態(tài)的一個優(yōu)點(diǎn)是允蚣,你可以使用它作為其他導(dǎo)出態(tài)的輸入。下面的示例將顯示基于當(dāng)前服務(wù)器狀態(tài)的當(dāng)前用戶和好友列表呆贿。如果服務(wù)器改變了當(dāng)前用戶嚷兔,它將重新呈現(xiàn)整個列表森渐,如果它只改變了一個朋友的狀態(tài),那么只有該列表?xiàng)l目將被重新呈現(xiàn)冒晰。如果單擊列表項(xiàng)同衣,它將在本地更改當(dāng)前用戶并更新服務(wù)器狀態(tài)。
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>
);
}