目錄
Jotai是甚麼
Jotai和Redux一樣是用於全域狀態管理
的套件
全域狀態管理是指將狀態存在外部的store
,因此不會像useState、useContext和組件切不開
故可以用全域性的一次只出現一個的snackbar、dialog上
Jotai有以下幾個特色
bundle size較小
npm trend將一大塊的全域
狀態
(state tree),切成小塊
變成atom只有
與引入
某個atom的component
會在該atom被更新時re-render
,降低效能衝擊
- 寫法類似useState、Redux,心智負擔較低
Jotai用法
atom
用於存放基本值
interface ClothesDetail {
color: string;
material: string;
type?: string;
brand?: string;
price?: number;
}
const clothesDetailAtom = atom<ClothesDetail>({
color:'black',
material: 'cutton,
});
使用時
const [clothesDetail, setClothesDetail] = useAtom(clothesDetailAtom);
read-only atom & write-only
read-only atom和write-only atom可以
定義成只存取或更新
atom的部分
屬性
也就是說如果需要的話你甚至可以一個屬性拆出一個atom出來
// read-only
const getClothesDetailAtom = atom((get) =>
get(clothesAtom),
);
// write-only
const changeClothesDetailAtom = atom(
null,
(get, set, update: Partial<ClothesDetail>) => {
const clothesDetail = get(clothesAtom);
set(clothesAtom, { ...clothesDetail, ...update });
}
);
使用時
const [clothesDetail] = useAtom(getClothesDetailAtom);
const [, changeClothesDetail] = useAtom(changeClothesDetailAtom);
select atom
用於存取物件,尤其是物件中包了物件的結構
,或者避免陷入infinite loop
接受2個callback,第一個是selector function,第二個是equality function
預設情況下,當參照的atom變動時
,selector function也會執行
,因此可以得到參照的atom的部分current value
如果希望selector function只在reference一樣時執行
,可以傳equality function
去做比較
而reference一樣是傳址之意,當參照的atom值整個被重新賦值時,就是不一樣的reference
const coordinationAtom = atom({
top:{
type:'t-shirt'
color:'black',
material:'cotton'
},
bottom::{
type:'gaucho pants'
color:'white',
material:'linen'
}
})
const topCoordinationAtom = selectAtom(coordinationAtom, (coordination) => coordination.top);
const bottomCoordinationAtom = selectAtom(coordinationAtom, ({bottom}) => bottom);
使用時
const [topCoordination] = useAtom(topCoordinationAtom);
const [{color, material, type}] = useAtom(bottomCoordinationAtom);
至於避免infinite loop
,是因為select atom可以提供一個穩定的reference
,因此就可以避免這個問題
split atom
用於陣列
,共有remove、insert、move這幾種類型的事件可以用
const shoppingCartAtom = atom([{
type:'t-shirt'
color:'black',
material:'cotton',
price:300
},
{
type:'long skirt'
color:'navy',
material:'denim',
price:500
}
]);
const shoppingCartListAtom = splitAtom(shoppingCartAtom);
使用時
const shoppingCart = () => {
const [shoppingCartList, dispatch] = useAtom(shoppingCartListAtom);
return (
<ul>
{shoppingCartList.map((item) => (
<CardItem
item={item}
// 類似Redux的dispatch
remove={() => dispatch({ type: "remove", atom: item })}
/>
))}
</ul>
);
};
async atom
Jotai支援非同步的讀與寫
傳非同步的callback給atom就能建立一個async atom
用於存取非同步的值
(ex:打API撈資料、一些需要使用Promise處理的特殊情境)
// async read-only
const fetchUrlAtom = atom(
async (get) => {
const response = await fetch('your API url here')
return await response.json()
}
);
使用時
const [apiData] = useAtom(fetchUrlAtom)
(2023/6/13更新)
從v2開始
,async atom變成就只是個回傳值為promise的atom,它並不會處理promise
,所以asyn atom的read function需要加上await或者.then()
(2023/6/13更新)
store API
從v2開始移除了JotaiProvider的scope props,並新增了store API的功能
可以把它想像成是Redux的store的概念,store可以傳給jotai Provider
使用createStore建立store
import { createStore } from 'jotai' // or from 'jotai/vanilla'
const store = createStore();
// createStore接收初始值,並回傳store物件
// 也可以建立React Context並傳給store
store有三個方法,get、set、sub
- get
store.get(fooAtom)
- set
store.set(fooAtom, 1)
- sub
監聽
state的改變const unsub = store.sub(fooAtom, () => { console.log('fooAtom value in store is changed') })
getDefaultStore & Provider
jotai Provider
組件提供state給component sub tree
,且多個
Provider是可
以同時獨立
存在的
使用Provider有幾個優點
- 拆分sub tree的state,避免互相污染
- 重新渲染時清空atom
如果某個atom不在Provider下,這就是provider-less mode
,這時會使用default的state
default state也可以用getDefaultStore方法取得
情境
假設有3個組件分別如下
└── CreatePhotoWork(上傳作品(照片url、敘述)的頁面)
├── FileUpload(上傳圖片的組件)
└── ImageCrop(切裁圖片的組件)
操作流程則如下
- 在FileUpload選擇圖片,觸發input的onChange
- 從input取得File
- 把File丟給ImageCrop,然後轉成切裁的圖
- a. 切裁完按下ImageCrop的OK後,(呼叫setPreview方法)繪製canvas
b. 將canvas轉成Blob,再轉成File - (這一步在下方例子會省略)
a. 按下FileUpload的upload,跳出loading
b. 把從ImageCrop得到的File傳給後端)
使用context實作
context可以讓多個組件共享資料,而不必一層一層地把props傳到最底下
呼叫useContext的組件能夠取得最靠近的Context.Provider的value
然而缺點是,若子組件
需要能更新context的值
,就必須把set function從父組件傳下去
給子組件
如果用以上的情境來說,使用context可以這麼做
- 在父組件(頁面組件)建立context,一開始可為空
import { createContext } from "react"; export const PhotoStateContext = createContext<PhotoState | null>(null);
在父組件使用useState建立存context值的變數、更新context值的set function
export type PhotoState = { previewSrc: string; photoToUpload?: File; }; const Context = () => { const [photoState, setPhotoState] = useState<PhotoState>({ previewSrc: "", photoToUpload: undefined });
- 在父組件用context建立Context.Provider並把步驟2的變數傳給value
return ( <PhotoStateContext.Provider value={photoState}> // 省略 </PhotoStateContext.Provider> );
a. 在需要能更新context值的子組件開個props,傳入步驟2的set function
<PhotoStateContext.Provider value={photoState}> <FileUpload inputRef={inputRef} setPhotoState={setPhotoState} /> <ImageCrop src={photoState.previewSrc} input={inputRef.current} setPhotoState={setPhotoState} /> </PhotoStateContext.Provider>
b. 在需要取得context值的子組件使用useContext,並傳入步驟1建立的context
import { createContext } from "react"; interface ImageCropProps { src: string; input: HTMLInputElement | null; setPhotoState: (data: PhotoState) => void; } const ImageCrop: FunctionComponent<ImageCropProps> = ({ src, input, setPhotoState }) => { const photoState = useContext(PhotoStateContext);
使用Jotai實作
如果用以上的情境來說,使用Jotai可以這麼做
開一個存放atom的檔案(ex: store.ts),並使用atom建立一個存放state的atom
import { atom } from "jotai"; interface PhotoState { photoToUpload?: File; previewSrc: string; } export const photoStateAtom = atom<PhotoState>({ previewSrc: "" });
在store.ts視需求撰寫write或read atom
// 這是write-only atom // getter為null,setter為function,所以只能更新 export const changePhotoStateAtom = atom( null, (get, set, update: Partial<PhotoState>) => { const photoState = get(photoStateAtom); set(photoStateAtom, { ...photoState, ...update }); } );
在需要取得、更新atom值的地方引入atom
之後的用法就類似useState
import { useAtom } from "jotai"; import { photoStateAtom } from "../../store"; const Jotai = () => { // 第一個元素是atom值,第二個是set function const [{ previewSrc, photoToUpload }] = useAtom(photoStateAtom);
import { useAtom } from "jotai"; import { changePhotoStateAtom } from "../../store"; interface ImageCropProps { src: string; input: HTMLInputElement | null; } const ImageCrop: FunctionComponent<ImageCropProps> = ({ src, input }) => { // write-only atom // 第一個元素為null,第二個為set function const [, setPhotoState] = useAtom(changePhotoStateAtom);