經過簡單的計數器應用來展現其使用。先來看沒有 Recoil 時如何實現。css 首先建立示例項目html $ yarn create react-app recoil-app --template typescript
計數器考察以下計數器組件:node Counter.tsxreact 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.tsxgithub 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.tsxtypescript export interface IDisplayProps {
count: number;
}
export const Display = ({ count }: IDisplayProps) => {
return <div>{count}</div>;
};
App.tsxshell 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>
);
}
能夠看到,數據被提高到了父組件中進行管理,而對數據的操做,也一併進行了提高,子組件中只負責觸發改變數據的動做 這無疑增長了父組件的負擔,一是這樣的邏輯上升沒有作好組件功能的內聚,二是父組件在最後會沉澱大量這種上升的邏輯,三是這種上升的操做不適用於組件深層嵌套的狀況,由於要逐級傳遞屬性。api 固然,這裏可以使用 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: |