[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
selectorFamily
is 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>
)
}