非同步資料查詢
Recoil 提供一種透過資料流程圖將狀態和衍生狀態對應到 React Components 的方式。真正厲害的是,此流程圖中的函式也可以是非同步的。如此一來,就可以在同步的 React Component 呈現函式中輕鬆使用非同步函式。Recoil 讓你可以在選擇器的資料流程圖中無縫結合同步函式和非同步函式。只要從選擇器的 get
回呼傳回一個 Promise(而不是值),介面仍然完全相同。由於這些僅為選擇器,因此其他選擇器也可以依賴它們來進一步轉換資料。
選擇器可以作為將非同步資料納入 Recoil 資料流程圖的一種方式。請記住,選擇器代表「冪等」的函式:對於某組輸入,它們應該始終產生相同的結果(至少在應用程式運作期間)。這非常重要,因為選擇器評估可能會儲存在快取中、重新啟動或執行多次。因此,選擇器通常是建構唯讀資料庫查詢的絕佳方法。針對可變動的資料,你可以使用 查詢更新。或者,若要同步可變動狀態、維持狀態或其他副作用,請考慮使用 原子效果 API 或 Recoil Sync 函式庫。
同步範例
例如,以下是簡單的同步 原子 和 選擇器,用來取得使用者名稱
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>;
}
選取器的介面是相同的,所以使用這個選取器的元件不需要處理它是以同步原子狀態、衍生選取器狀態,或非同步查詢做為後盾支援的!
但是,由於 React 渲染函式是同步的,所以承諾解析前會渲染什麼?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()
,因為嘗試設定提供的 快照
的狀態,對主代管環境 <RecoilRoot>
的即時狀態而言並不會有任何影響。
查詢預設原子值
一個常見模式是使用一個原子來表示本機可編輯狀態,但使用一個承諾來查詢預設值
const currentUserIDState = atom({
key: 'CurrentUserID',
default: myFetchCurrentUserID(),
});
或使用一個選擇器來延後查詢或依賴其他狀態。請注意,當使用選擇器時,預設原子值將保持動態,並隨選擇器更新而更新,直到原子被使用者明確設定為止。
const UserInfoState = atom({
key: 'UserInfo',
default: selector({
key: 'UserInfo/Default',
get: ({get}) => myFetchUserInfo(get(currentUserIDState)),
}),
});
這也可與原子家族搭配使用
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)),
}),
});
如果你想要雙向同步資料,請考慮 原子作用。
沒有 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;
}
請注意,Atom 目前不支援接受 Promise
作為新的數值。因此,如果你這樣希望的話,在你查詢更新時,不能把 Atom 放在 React Suspence 的待處理狀態。不過,你可以儲存一個物件,手動編碼目前的載入狀態和實際結果,來明確處理這一點。
也要考慮 Atom 作用 以進行 Atom 的查詢同步。
從錯誤訊息重試查詢
以下是一個有趣的範例,讓你可以在 <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>
);
}