Recoil通過數據流圖將數據及衍生數據映射給組件,其真正強大之處是可異步,我們可以react組件中使用異步函數。Recoil允許我們在數據流圖的seletors中無縫混合使用同步和異步函數。從selector 的get回調中返回Promise對象,接口保持不變。
selectors可以用作將異步數據合并到recoil數據流圖中的一種方法。記住selectors是純函數:對于給定的一組輸入,它們應該總是產生相同的結果。這一點非常重要,因為選擇器計算可能會執(zhí)行一次或多次,可能會重新啟動,也可能會被緩存。因此,selectors是建模只讀DB查詢的好方法,這重復查詢可以提供一致的數據。如果您想同步本地和服務器狀態(tài),那么請參閱異步狀態(tài)同步或狀態(tài)持久。
只讀數據
同步舉例
一個簡單的同步 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會被重新評估并執(zhí)行新的查詢,查詢結果會被緩存,因此對于每個唯一的輸入,查詢只執(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的組件不需要關心它是由同步原子狀態(tài)、派生選擇器狀態(tài)還是異步查詢支持的。
但是,由于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>
);
}
帶參查詢
有時希望能夠基于參數進行查詢,而不僅僅是基于派生狀態(tài)。例如,可能想要基于組件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>
);
}
數據流圖
當狀態(tài)更新時,數據流圖將自動更新并重新呈現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使用一個查詢來獲取每個朋友的信息。但是,通過在循環(huán)中這樣做,它們本質上是序列化的。如果查找速度快,也許沒問題,如果開銷較大,您可以使用并發(fā)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()鉤子來確定渲染期間的狀態(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;
}
}
讀寫數據
與服務器同步狀態(tài)
我們可以訂閱遠程狀態(tài)中的異步更改,并更新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>
);
}
雙向同步
您還可以同步狀態(tài),以便在服務器上更新本地更改。注意,這是一個簡化的示例,請注意避免反饋循環(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根據參數同步本地狀態(tài)。注意,這個示例鉤子的每次調用都將創(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);
};
}, []);
}
數據流圖
使用原子來表示遠態(tài)的一個優(yōu)點是,你可以使用它作為其他導出態(tài)的輸入。下面的示例將顯示基于當前服務器狀態(tài)的當前用戶和好友列表。如果服務器改變了當前用戶,它將重新呈現整個列表,如果它只改變了一個朋友的狀態(tài),那么只有該列表條目將被重新呈現。如果單擊列表項,它將在本地更改當前用戶并更新服務器狀態(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>
);
}