[note] Recoil
此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料:
- recoil @ official website
- 👍 learn recoil
概念
Atoms
keywords: useRecoilState
Atoms 是 state 的最小單位,它們是可被更新和訂閱的:只要 atom 被更新時,所有有訂閱該 atom 的元件都會 re-rendered 並取得最新的值。Atoms 可以用來取代 React 元件中的 local state。當有多個元件共用相同的 atom 時,這些 components 都可以共享 state,並取得相同的 state。
只要有使用到某 atom 的元件,等同於該元件會自動訂閱該 atom,一旦該 atom 有任何更新,都會導致該元件 re-render。
const darkModeState = atom<boolean>({
key: 'darkMode',
default: false,
})
const Foo = () => {
const [darkMode, setDarkMode] = useRecoilState(darkModeState);
return (
/* ... */
)
}
Selectors
keywords: useRecoilValue
selectors 是一個可以接收 atoms 或其他 selectors 做為參數的 pure function, 它會根據 state 來計算取的新的值。我們會盡可能讓 state 保持乾淨而不保存其他多餘的狀態,如果有些資料是需要根據這些 state 計算產生的時候,則可以使用 selector。
當有新的 atoms 或 selectors 更新時,selector function 就會重新被執行,並取得最新的 state。如同 atoms 一樣,React 元件也可以訂閱 selectors,當 selectors 改變時,這些元件也會 re-rendered。
selectors 會追蹤哪些元件會用到它們,以及它們相依於哪些 state,因此可以很有效率的去執行這個函式。
selectors 給我的感覺很像 Vue 中的 computed。簡單來說,如果有需要更新 atom 中的資料,則使用 useRecoilState,如果只需根據 state 取得狀態的話,就使用 selector 提供的 useRecoilValue。
const usdAtom = atom<number>({
key: 'usd',
default: 1,
})
const eurSelector = selector<number>({
key: 'eur',
get: ({get}) => {
let usd = get(usdAtom)
return usd * exchangeRate
},
set: ({set, get}, newEurValue) => {
// @ts-expect-error
const newUsdValue = newEurValue / exchangeRate
set(usdAtom, newUsdValue)
},
})
const Foo = () => {
const [usd, setUsd] = useRecoilState(usdAtom)
const [eur, setEur] = useRecoilState(eurSelector)
return (
/* ... */
)
}
Atom Family
keywords: atomFamily
function atomFamily<T, Parameter>({
key: string,
default:
| RecoilValue<T>
| Promise<T>
| T
| (Parameter => T | RecoilValue<T> | Promise<T>),
effects_UNSTABLE?:
| $ReadOnlyArray<AtomEffect<T>>
| (P => $ReadOnlyArray<AtomEffect<T>>),
dangerouslyAllowMutability?: boolean,
}): Parameter => RecoilState<T>
呼叫 atomFamily 函式時,帶入的參數(Parameter)一定要是可以被序列化的。

透過 Atom Family 可以快速建立多個「長相相同」的 Atoms:
atomFamily<T, U>:T 指的是每個 atom 的型別, U 指的是用來辨別每個 Atom 的型別(即,每個 Atom 的 id)useRecoilState(elementStateFamily(<id>)):這裡的id會是唯一不會重複的,每一個不同的 id 都表示的是 Atom Family 中不同的 Atom
export type ElementStyle = {
position: {top: number; left: number}
size: {width: number; height: number}
}
export type Element = {style: ElementStyle}
export const elementStateFamily = atomFamily<Element, number>({
key: 'element',
default: {
style: {
position: {top: 0, left: 0},
size: {width: 50, height: 50},
},
},
})
export const Rectangle = ({id}: {id: number}) => {
const [selectedElement, setSelectedElement] = useRecoilState(selectedElementState)
// 指定 atomFamily 的方式就是把該 atom 的 id 帶進去
const [element, setElement] = useRecoilState(elementStateFamily(id))
return (
/* ... */
)
}
同樣的在 selector 中也能透過 atomFamily 的 id 取得該 atom:
const selectorElementPropertiesSelector = selector({
key: 'selectorElementProperties',
get: ({ get }) => {
// 這裡帶入 id 即可取得 AtomFamily 內的 Atom
const element = get(elementStateFamily(<id>))
}
})
Selector Family
- 當 selector 中需要帶入參數,並根據參數產生不同的 selector 時,就可以想到要用 selectorFamily
function selectorFamily<T, Parameter>({
key: string,
get: Parameter => ({get: GetRecoilValue}) => Promise<T> | RecoilValue<T> | T,
set: Parameter => (
{
get: GetRecoilValue,
set: SetRecoilValue,
reset: ResetRecoilValue,
},
newValue: T | DefaultValue,
) => void,
dangerouslyAllowMutability?: boolean,
}): Parameter => RecoilState<T>
selectorFamily 中的 Parameter 一定要是可以被序列化的(serializable),因為該 Parameter 可以是物件,recoil 為了要比較這兩個物件是否完全相同,會以 value 而非 reference 的方式來進行比較。
- Selector Family 的概念和 Atom Family 的概念類似,在 Atom Family 中,可以針對相同型別的 state 產生多個不同的 Atoms;Selector Family 則是可以針對相似邏輯的 selector,根據不同的參數(例如,下面的
path)來進行些微不同的操作。
Selector Family: A
selectorFamilyis a powerful pattern that can create similar selectors that behave slightly differently depending on the parameter (from learn recoil).
selectorFamily<T, P>:T是回傳的型別,P是用來判斷是哪個 selector(i.e.,key,id)的型別
import {selectorFamily, useRecoilValue} from 'recoil'
const editPropertyState = selectorFamily<number, string>({
key: 'editProperty',
get:
(path) =>
({get}) => {
return 10;
},
set:
(path) =>
({get, set}, newValue) => {
const selectedElementId = get(selectedElementState)
if (selectedElementId === null) return
const element = get(elementStateFamily(selectedElementId))
// prevent lodash mutate the element object
const newElement = produce(element, (draft) => {
_set(draft, path, newValue)
})
// set value to elementStateFamily
set(elementStateFamily(selectedElementId), newElement)
},
})
const Foo = () => {
const property = useRecoilValue(editPropertyState('some-id'))
return (/* ... */)
}
Fetching Data
預設的情況下,Recoil 在處理 async 的情況下,會使用 React 的 Suspense,因此開發者不需要額外透過 loading state 來檢視資料載入的狀態。
但也因此需要把在 recoil 中有處理到 async 的 React 元件包在 <Suspense /> 中。
加上 Suspense
import { Suspense } from 'react';
const Foo = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<SomeComponentHaveWait />
</Suspense>
</div>
);
};
在 selector 中呼叫 API:atom + selector
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// 1. 這個狀態是給使用者切換用的
const userIdState = atom<number | undefined>({
key: 'userId',
default: undefined,
});
// 2. 透過 selector 來 fetch data
const userState = selector({
key: 'user',
get: async ({ get }) => {
const userId = get(userIdState);
if (userId === undefined) return;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
const userData = await res.json();
return userData;
},
});
export const Async = () => {
const [userId, setUserId] = useRecoilState(userIdState);
const user = useRecoilValue(userState);
return (
// 3. onchange 時會改變 userId,同時觸發 selector 去拉取資料
<Select
placeholder="Choose a user"
mb={4}
value={userId}
onChange={(event) => {
const value = event.target.value;
setUserId(value ? parseInt(value) : undefined);
}}
>
<option value="1">User 1</option>
<option value="2">User 2</option>
<option value="3">User 3</option>
</Select>
);
};
在 selector 中呼叫 API:selectorFamily
Data Fetching Basic - replace dependencies of Atom with React State by selectorFamily
除了像上面一樣讓 selector 相依於 Atom 的 state 之外,也可以讓 selector 是相依於 React 元件本身的 state,這時候因為會需要把參數帶入到 selector 中,因此會用到 selectorFamily:
diff --git a/src/examples/Async.tsx b/src/examples/Async.tsx
-const userIdState = atom<number | undefined>({
- key: 'userId',
- default: undefined,
-})
-
-const userState = selector({
+const userState = selectorFamily({
key: 'user',
- get: async ({get}) => {
- const userId = get(userIdState)
- console.log({
- userId,
- })
- if (userId === undefined) return
-
+ get: (userId: number) => async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
const userData = await res.json()
return userData
},
})
-const UserData = () => {
- const user = useRecoilValue(userState)
-
- if (!user) return null
+const UserData = ({userId}: {userId: number}) => {
+ const user = useRecoilValue(userState(userId))
return (
<div>
@@ -44,7 +31,7 @@ const UserData = () => {
}
export const Async = () => {
- const [userId, setUserId] = useRecoilState(userIdState)
+ const [userId, setUserId] = useState<number>()
return (
<Container py={10}>
@@ -67,9 +54,11 @@ export const Async = () => {
<option value="2">User 2</option>
<option value="3">User 3</option>
</Select>
- <Suspense fallback={<div>Loading...</div>}>
- <UserData />
- </Suspense>
+ {userId !== undefined && (
+ <Suspense fallback={<div>Loading...</div>}>
+ <UserData userId={userId} />
+ </Suspense>
+ )}
</Container>
)
}
相依的 selector
Data Fetching Advanced - selector of WeatherState depends on selector of UserState
- 當兩個會呼叫 async function 的 selector 彼此相依時,recoil 會等到前一個 selector 的值已經 resolve 後才接著執行下一個 selector。以下面的例子來說:
weatherState會相依於userState,但 recoil 會等userState的結果 resolve 之後,才接著執行weatherState這個 selector 的 get function
- Suspense 會等到該元件內的所有資料都 resolve 後才解開
import {Suspense, useState} from 'react'
import {selectorFamily, useRecoilValue} from 'recoil'
+import {getWeather} from './fakeAPI'
const userState = selectorFamily({
key: 'user',
@@ -12,8 +13,20 @@ const userState = selectorFamily({
},
})
+const weatherState = selectorFamily({
+ key: 'weather',
+ get:
+ (userId: number) =>
+ async ({get}) => {
+ // 這裡並不需要加上 await,recoil 會在取得 userState 後才接著執行這裡的 get function
+ const user = get(userState(userId))
+
+ const weather = await getWeather(user.address.city)
+ return weather
+ },
+})
+
const UserData = ({userId}: {userId: number}) => {
const user = useRecoilValue(userState(userId))
+ const weather = useRecoilValue(weatherState(userId))
return (
<div>
@@ -26,6 +39,11 @@ const UserData = ({userId}: {userId: number}) => {
<Text>
<b>Phone:</b> {user.phone}
</Text>
+ <Text>
+ <b>
+ Weather for {user.address.city}: {weather}°C
+ </b>
+ </Text>
</div>
)
}
@@ -55,6 +73,7 @@ export const Async = () => {
<option value="3">User 3</option>
</Select>
{userId !== undefined && (
+ // Suspense 會在 UserData 中的所有 data 都 resolve 後才解開
<Suspense fallback={<div>Loading...</div>}>
<UserData userId={userId} />
</Suspense>
更新資料:refresh
Data Fetching Advanced - create requestWeatherId to make weatherState refresh
由於 selector 有一個概念是當 input 相同時,會直接把上次 cache 的結果拿出來用,而不會再次執行 selector 內的 get function,這樣使用者只有在第一次請求資料時需要等待,後續都不用在重新 fetch data。
但如果這個資料是有時效性的,需要一直更新的話該怎麼辦呢?其中一種方式是建立額外的一個 id,透過改變這個 id,來讓 selector 內的資料有改變,以觸發 selector 中的 get function 重新執行。
由於 selector 的結果會被 cache,因此當你預期這個資料是會隨著時間改變時,並不適合使用單一個 selector,因為 recoil 會略過再去 fetch 一次最新的資料,並直接回傳前一次的結果(cache)給你。
舉例來說:
- 額外建立一個狀態是
weatherRequestId - 讓
weatherState相依於weatherRequestId - 如此,一旦
weatherRequestId改變時,recoil 才會重新執行weatherState這個 selector 中的 get function,並且重新拉資料
import {Container, Heading, Text} from '@chakra-ui/layout'
import {Select} from '@chakra-ui/select'
import {Suspense, useState} from 'react'
+import {atomFamily, selectorFamily, useRecoilValue, useSetRecoilState} from 'recoil'
import {getWeather} from './fakeAPI'
const userState = selectorFamily({
@@ -13,11 +13,24 @@ const userState = selectorFamily({
},
})
+// 1. 建立 weatherRequestId
+const weatherRequestIdState = atomFamily({
+ key: 'weatherRequestId',
+ default: 0,
+})
+const useRefetchWeather = (userId: number) => {
+ const setRequestId = useSetRecoilState(weatherRequestIdState(userId))
+ return () => setRequestId((id) => id + 1)
+}
+
const weatherState = selectorFamily({
key: 'weather',
get:
(userId: number) =>
async ({get}) => {
+ // 3. 由於 weatherState 相依於 weatherRequestId,因此一旦此值改變,recoil 就會再次執行此 selector 中的 get function
+ get(weatherRequestIdState(userId))
+
const user = get(userState(userId)) // 這裡並不需要加上 await,recoil 會在取得 userState 後才接著執行這裡的 get function
const weather = await getWeather(user.address.city)
return weather
@@ -28,10 +41,16 @@ const WeatherDate = ({userId}: {userId: number}) => {
const user = useRecoilValue(userState(userId))
const weather = useRecoilValue(weatherState(userId))
+ // 2. 使用者呼叫此 function 時,weatherRequestId 會改變
+ const refetch = useRefetchWeather(userId)
+
return (
- <Text>
- <b>Weather for {user.address.city}:</b> {weather}°C
- </Text>
+ <div>
+ <Text>
+ <b>Weather for {user.address.city}:</b> {weather}°C
+ </Text>
+ <Text onClick={refetch}>(refresh weather)</Text>
+ </div>
)
}
重要概念:Idempotence
selector 回傳的結果會 cache,如果是相同 input 會直接回傳相同結果。
在 selector 中有一個很重要的概念是,當代入同樣的 input 時,回傳的結果會被快取起來,因此當下次有同樣的 input 進來時,selector 會直接 return 相同的結果,而不會再跑一次 selector 內的 get 函式。
因此以這個例子來說,同樣的 input 代入時,就不會再去 fetch 一次資料,因此也不會再顯示 loading 的狀態:

資料來源:learn recoil