通過簡單的計數器應用來展示其使用。先來看沒有 Recoil 時如何實現。 首先建立示例專案 $ yarn create react-app recoil-app --template typescript
計數器考察如下計數器元件: Counter.tsx import React, { useState } from "react";
export const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
+
</button>
<button
onClick={() => {
setCount((prev) => prev - 1);
}}
>
-
</button>
</div>
);
};
跨元件共享資料狀態當想把 Counter.tsx export interface ICounterProps {
onAdd(): void;
onSubtract(): void;
}
export const Counter = ({ onAdd, onSubtract }: ICounterProps) => {
return (
<div>
<button onClick={onAdd}>+</button>
<button onClick={onSubtract}>-</button>
</div>
);
};
Display.tsx export interface IDisplayProps {
count: number;
}
export const Display = ({ count }: IDisplayProps) => {
return <div>{count}</div>;
};
App.tsx export function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<Display count={count} />
<Counter
onAdd={() => {
setCount((prev) => prev + 1);
}}
onSubtract={() => {
setCount((prev) => prev - 1);
}}
/>
</div>
);
}
可以看到,資料被提升到了父元件中進行管理,而對資料的操作,也一併進行了提升,子元件中只負責觸發改變資料的動作 這無疑增加了父元件的負擔,一是這樣的邏輯上升沒有做好元件功能的內聚,二是父元件在最後會沉澱大量這種上升的邏輯,三是這種上升的操作不適用於元件深層巢狀的情況,因為要逐級傳遞屬性。 當然,這裡可使用 Context 來解決。 使用 Context 進行資料狀態的共享新增 Context 檔案儲存需要共享的狀態: appContext.ts import { createContext } from "react";
export const AppContext = createContext({
count: 0,
updateCount: (val: number) => {},
});
注意這裡建立 Context 時,為了讓子元件能夠更新 Context 中的值,還額外建立了一個回撥 更新 App.tsx export function App() {
const [count, setCount] = useState(0);
const ctx = {
count,
updateCount: (val) => {
setCount(val);
},
};
return (
<AppContext.Provider value={ctx}>
<div className="App">
<Display />
<Counter />
</div>
</AppContext.Provider>
);
}
更新 Counter.tsx export const Counter = () => {
const { count, updateCount } = useContext(AppContext);
return (
<div>
<button
onClick={() => {
updateCount(count + 1);
}}
>
+
</button>
<button
onClick={() => {
updateCount(count - 1);
}}
>
-
</button>
</div>
);
};
更新 Display.tsx export const Display = () => {
const { count } = useContext(AppContext);
return <div>{count}</div>;
};
可以看出,Context 解決了屬性傳遞的問題,但邏輯上升的問題仍然存在。 同時 Context 還面臨其他一些挑戰,
export function App() {
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Recoil 的使用安裝新增 Recoil 依賴: $ yarn add recoil
RecoilRoot類似 Context 需要將子元件包裹到 ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
Atom & SelectorRecoil 中最小的資料元作為 Atom 存在,從 Atom 可派生出其他資料,比如這裡 建立 state 檔案用於存放這些 Recoil 原子資料: appState.ts import { atom } from "recoil";
export const countState = atom({
key: "countState",
default: 0,
});
通過 selector 可從基本的 atom 中派生出新的資料,假如還需要展示一個當前 import { atom, selector } from "recoil";
export const countState = atom({
key: "countState",
default: 0,
});
export const powerState = selector({
key: "powerState",
get: ({ get }) => {
const count = get(countState);
return count ** 2;
},
});
selector 的存在意義在於,當它依賴的 atom 發生變更時,selector 代表的值會自動更新。這樣程式中無須關於這些資料上的依賴邏輯,只負責更新最基本的 atom 資料即可。 而使用時,和 React 原生的 import { useRecoilState } from "recoil";
...
const [count, setCount] = useRecoilState(countState)
...
當只需要使用值而不需要對值進行修改時,可使用 Display.tsx import React from "react";
import { useRecoilValue } from "recoil";
import { countState, powerState } from "./appState";
export const Display = () => {
const count = useRecoilValue(countState);
const pwoer = useRecoilValue(powerState);
return (
<div>
count:{count} power: {pwoer}
</div>
);
};
由上面的使用可看到,atom 建立的資料和 selector 建立的資料,在使用上無任何區別。 當只需要對值進行設定,而又不進行展示時,則可使用 Conter.tsx import React from "react";
import { useSetRecoilState } from "recoil";
import { countState } from "./appState";
export const Counter = () => {
const setCount = useSetRecoilState(countState);
return (
<div>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
+
</button>
<button
onClick={() => {
setCount((prev) => prev - 1);
}}
>
-
</button>
</div>
);
};
非同步資料的處理Recoil 最方便的地方在於,來自非同步操作的資料可直接引數到資料流中。這在有資料來自於請求的情況下,會非常方便。 export const todoQuery = selector({
key: "todo",
get: async ({ get }) => {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const todos = res.json();
return todos;
},
});
使用時,和正常的 state 一樣: TodoInfo.tsx export function TodoInfo() {
const todo = useRecoilValue(todoQuery);
return <div>{todo.title}</div>;
}
但由於上面 App.tsx import React, { Suspense } from "react";
import { TodoInfo } from "./TodoInfo";
export function App() {
return (
<div className="app">
<Suspense fallback="loading...">
<TodoInfo />
</Suspense>
</div>
);
}
預設值前面看到無論 atom 還是 selector 都可在建立時指定預設值。而這個預設值甚至可以是來自非同步資料。 appState.ts export const todosQuery = selector({
key: "todo",
get: async ({ get }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos`);
const todos = res.json();
return todos;
},
});
export const todoState = atom({
key: "todoState",
default: selector({
key: "todoState/default",
get: ({ get }) => {
const todos = get(todosQuery);
return todos[0];
},
}),
});
使用: TodoInfo.tsx export function TodoInfo() {
const todo = useRecoilValue(todoState);
return <div>{todo.title}</div>;
}
不使用 Suspense 的示例當然也可以不使用 React Suspense,此時需要使用 App.tsx import React from "react";
import { useRecoilValueLoadable } from "recoil";
import "./App.css";
import { todoQuery } from "./appState";
export function TodoInfo() {
const todoLodable = useRecoilValueLoadable(todoQuery);
switch (todoLodable.state) {
case "hasError":
return "error";
case "loading":
return "loading...";
case "hasValue":
return <div>{todoLodable.contents.title}</div>;
default:
break;
}
}
給 selector 傳參上面請求 Todo 資料時 id 是寫死的,真實場景下,這個 id 會從介面進行獲取然後傳遞到請求的地方。 此時可先建立一個 atom 用以儲存該選中的 id。 export const idState = atom({
key: "idState",
default: 1,
});
export const todoQuery = selector({
key: "todo",
get: async ({ get }) => {
const id = get(idState);
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const todos = res.json();
return todos;
},
});
介面上根據互動更新 id,因為 export function App() {
const [id, setId] = useRecoilState(idState);
return (
<div className="app">
<input
type="text"
value={id}
onChange={(e) => {
setId(Number(e.target.value));
}}
/>
<Suspense fallback="loading...">
<TodoInfo />
</Suspense>
</div>
);
}
另外處情況是直接將 id 傳遞到 selector,而不是依賴於另一個 atom。 export const todoQuery = selectorFamily<{ title: string }, { id: number }>({
key: "todo",
get: ({ id }) => async ({ get }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const todos = res.json();
return todos;
},
});
App.tsx export function App() {
return (
<div className="app">
<Suspense fallback="loading...">
<TodoInfo id={1} />
<TodoInfo id={2} />
<TodoInfo id={3} />
</Suspense>
</div>
);
}
請求的重新整理selector 是冪等的,固定輸入會得到固定的輸出。即,拿上述情況舉例,對於給定的入參 id,其輸出永遠一樣。根據這個我,Recoil 預設會對請求的返回進行快取,在後續的請求中不會實際觸發請求。 這能滿足大部分場景,提升效能。但也有些情況,我們需要強制觸發重新整理,比如內容被編輯後,需要重新拉取。 有兩種方式來達到強制重新整理的目的,讓請求依賴一個人為的 RequestId,或使用 Atom 來存放請求結果,而非 selector。 RequestId一是讓請求的 selector 依賴於另一個 atom,可把這個 atom 作為每次請求唯一的 ID 亦即 RequestId。 appState.ts export const todoRequestIdState = atom({
key: "todoRequestIdState",
default: 0,
});
讓請求依賴於上面的 atom: export const todoQuery = selectorFamily<{ title: string }, { id: number }>({
key: "todo",
get: ({ id }) => async ({ get }) => {
+ get(todoRequestIdState); // 新增對 RequestId 的依賴
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const todos = res.json();
return todos;
},
});
然後在需要重新整理請求的時候,更新 RequestId 即可。 App.tsx export function App() {
const setTodoRequestId = useSetRecoilState(todoRequestIdState);
const refreshTodoInfo = useCallback(() => {
setTodoRequestId((prev) => prev + 1);
}, [setTodoRequestId]);
return (
<div className="app">
<Suspense fallback="loading...">
<TodoInfo id={1} />
<TodoInfo id={2} />
<TodoInfo id={3} />
<button onClick={refreshTodoInfo}>refresh todo 1</button>
</Suspense>
</div>
);
}
目前為止,雖然實現了請求的重新整理,但觀察發現,這裡的重新整理沒有按資源 ID 來進行區分,點選重新整理按鈕後所有資源都重新傳送了請求。 替換 atom 為 atomFamily 為其增加外部入參,這樣可根據引數來決定重新整理,而不是粗獷地全刷。 - export const todoRequestIdState = atom({
+ export const todoRequestIdState = atomFamily({
key: "todoRequestIdState",
default: 0,
});
export const todoQuery = selectorFamily<{ title: string }, { id: number }>({
key: "todo",
get: ({ id }) => async ({ get }) => {
- get(todoRequestIdState(id)); // 新增對 RequestId 的依賴
+ get(todoRequestIdState(id)); // 新增對 RequestId 的依賴
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const todos = res.json();
return todos;
},
});
更新 RequestId 時傳遞需要更新的資源: export function App() {
- const setTodoRequestId = useSetRecoilState(todoRequestIdState);
+ const setTodoRequestId = useSetRecoilState(todoRequestIdState(1)); // 重新整理 id 為 1 的資源
const refreshTodoInfo = useCallback(() => {
setTodoRequestId((prev) => prev + 1);
}, [setTodoRequestId]);
return (
<div className="app">
<Suspense fallback="loading...">
<TodoInfo id={1} />
<TodoInfo id={2} />
<TodoInfo id={3} />
<button onClick={refreshTodoInfo}>refresh todo 1</button>
</Suspense>
</div>
);
}
上面重新整理函式中寫死了資源 ID,真實場景下,你可能需要寫個自定義的 hook 來接收引數。 const useRefreshTodoInfo = (id: number) => {
const setTodoRequestId = useSetRecoilState(todoRequestIdState(id));
return () => {
setTodoRequestId((prev) => prev + 1);
};
};
export function App() {
const [id, setId] = useState(1);
const refreshTodoInfo = useRefreshTodoInfo(id);
return (
<div className="app">
<label htmlFor="todoId">
select todo:
<select
id="todoId"
value={String(id)}
onChange={(e) => {
setId(Number(e.target.value));
}}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</label>
<Suspense fallback="loading...">
<TodoInfo id={id} />
<button onClick={refreshTodoInfo}>refresh todo</button>
</Suspense>
</div>
);
}
使用 Atom 存放請求結果首先將獲取 todo 的邏輯抽取單獨的方法,方便在不同地方呼叫, export async function getTodo(id: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const todos = res.json();
return todos;
}
通過 atomFamily 建立一個存放請求結果的狀態: export const todoState = atomFamily<any, number>({
key: "todoState",
default: (id: number) => {
return getTodo(id);
},
});
展示時通過這個 TodoInfo.tsx export function TodoInfo({ id }: ITodoInfoProps) {
const todo = useRecoilValue(todoState(id));
return <div>{todo.title}</div>;
}
在需要重新整理的地方,更新 App.tsx function useRefreshTodo(id: number) {
const refreshTodoInfo = useRecoilCallback(({ set }) => async (id: number) => {
const todo = await getTodo(id);
set(todoState(id), todo);
});
return () => {
refreshTodoInfo(id);
};
}
export function App() {
const [id, setId] = useState(1);
const refreshTodo = useRefreshTodo(id);
return (
<div className="app">
...
<Suspense fallback="loading...">
<TodoInfo id={id} />
<button onClick={refreshTodo}>refresh todo</button>
</Suspense>
</div>
);
}
注意,因為請求回來之後更新的是 Recoil 狀態,所以需要在 useRecoilCallback 中進行。 異常處理前面的使用展示了 Recoil 與 React Suspense 結合用起來是多少順滑,介面上的載入態就像呼吸一樣自然,完全不需要編寫額外邏輯就可獲得。但還缺少錯誤處理。即,這些來自 Recoil 的非同步資料請求出錯時,介面上需要呈現。 而結合 React Error Boundaries 可輕鬆處理這一場景。 ErrorBoundary.tsx import React, { ReactNode } from "react";
// Error boundaries currently have to be classes.
/**
* @see https://reactjs.org/docs/error-boundaries.html
*/
export class ErrorBoundary extends React.Component<
{
fallback: ReactNode,
children: ReactNode,
},
{ hasError: boolean, error: Error | null }
> {
state = { hasError: false, error: null };
// eslint-disable-next-line @typescript-eslint/member-ordering
static getDerivedStateFromError(error: any) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
在所有需要錯誤處理的地方使用即可,理論上亦即所有出現 App.tsx <ErrorBoundary fallback="error :(">
<Suspense fallback="loading...">
<TodoInfo id={id} />
<button onClick={refreshTodo}>refresh todo</button>
</Suspense>
</ErrorBoundary>
ErrorBoudary 中展示錯誤詳情上面的 ErrorBoundary 元件來自 React 官方文件,稍加改良可讓其支援在錯誤處理時展示錯誤的詳情: ErrorBoundary.tsx export class ErrorBoundary extends React.Component<
{
fallback: ReactNode | ((error: Error) => ReactNode);
children: ReactNode;
},
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
// eslint-disable-next-line @typescript-eslint/member-ordering
static getDerivedStateFromError(error: any) {
return {
hasError: true,
error,
};
}
render() {
const { children, fallback } = this.props;
const { hasError, error } = this.state;
if (hasError) {
return typeof fallback === "function" ? fallback(error!) : fallback;
}
return children;
}
}
使用時接收錯誤引數並進行展示: App.tsx <ErrorBoundary fallback={(error: Error) => <div>{error.message}</div>}>
<Suspense fallback="loading...">
<TodoInfo id={id} />
<button onClick={refreshTodo}>refresh todo</button>
</Suspense>
</ErrorBoundary>
需要注意的問題selector 的巢狀與 Promise 的問題使用過程中遇到一個 selector 巢狀時 Promise 支援得不好的 bug,詳見 Using an async selector in another selector, throws an Uncaught promise #694。 正如 bug 中所說,當 selector 返回非同步資料,其他 selector 依賴於這個 selector 時,後續的 selector 會報 不過我發現,如果在後續 selector 中不使用 React Suspense 的 bug當使用文章前面提到的重新整理功能時,資料重新整理後,Suspense 中元件重新渲染,特定操作下會報
相關資源 |
The text was updated successfully, but these errors were encountered: |
Recoil 的使用
相關文章
- Recoil 預設值及資料級聯的使用
- Recoil 中預設值的正確處理
- Recoil Input 游標位置被重置到末尾的問題
- 細聊Concent & Recoil , 探索react資料流的新開發模式React模式
- Facebook 新一代 React 狀態管理庫 RecoilReact
- Recoil 中多級資料聯動及資料重置的合理做法
- Recoil 新一代的 React 函數語言程式設計 狀態管理工具React函數程式設計
- 再見了 Redux、Recoil、MobX、Zustand、Jotai 還有 Valtio,狀態管理還可以這樣做?ReduxAI
- Scrapy框架的使用之Scrapyrt的使用框架
- ActiveMQ的使用及整合spring的使用例項MQSpring
- Docker框架的使用系列教程(四)容器的使用Docker框架
- Urllib庫的使用一---基本使用
- ECharts的使用Echarts
- DbVisualizer的使用
- Typeof的使用
- iview 的使用View
- Trait 的使用AI
- lombok的使用Lombok
- MybatisGenerator的使用MyBatis
- valueForKeyPath的使用
- ThreadLocal的使用thread
- elasticsearch的使用Elasticsearch
- CoreData的使用
- joomla的使用OOM
- sqlmap的使用SQL
- echars的使用
- SVG 的使用SVG
- FlowableAPI的使用API
- pycnblog的使用
- netcat的使用
- jextract的使用
- pinia的使用
- pip 的使用
- DBV 的使用
- Docker的使用Docker
- Promise的使用Promise
- SVN的使用
- EndNote的使用