通过简单的计数器应用来展示其使用。先来看没有 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>
);
};

跨组件共享数据状态

当想把 count 的展示放到其他组件时,就涉及到跨组件共享数据状态的问题,一般地,可以将需要共享的状态向上提取到父组件中来实现。

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>
);
}

可以看到,数据被提升到了父组件中进行管理,而对数据的操作,也一并进行了提升,子组件中只负责触发改变数据的动作 onAddonSubtract,而真实的加减操作则从父组件传递下去。

这无疑增加了父组件的负担,一是这样的逻辑上升没有做好组件功能的内聚,二是父组件在最后会沉淀大量这种上升的逻辑,三是这种上升的操作不适用于组件深层嵌套的情况,因为要逐级传递属性。

当然,这里可使用 Context 来解决。

使用 Context 进行数据状态的共享

添加 Context 文件保存需要共享的状态:

appContext.ts

import { createContext } from "react";

export const AppContext = createContext({
count: 0,
updateCount: (val: number) => {},
});

注意这里创建 Context 时,为了让子组件能够更新 Context 中的值,还额外创建了一个回调 updateCount

更新 App.tsx 向子组件传递 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 通过 Context 获取需要的值和更新 Context 的回调:

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 从 Conext 获取需要展示的 count 字段:

Display.tsx

export const Display = () => {
const { count } = useContext(AppContext);
return <div>{count}</div>;
};

可以看出,Context 解决了属性传递的问题,但逻辑上升的问题仍然存在。

同时 Context 还面临其他一些挑战,

  • 更新 Context 需要单独提供一个回调以在子组件中进行调用
  • 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 需要将子组件包裹到 Provider 中,需要将组件包含在 <RecoilRoot> 中以使用 Recoil。

ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);

Atom & Selector

Recoil 中最小的数据元作为 Atom 存在,从 Atom 可派生出其他数据,比如这里 count 就是最原子级别的数据。

创建 state 文件用于存放这些 Recoil 原子数据:

appState.ts

import { atom } from "recoil";

export const countState = atom({
key: "countState",
default: 0,
});

通过 selector 可从基本的 atom 中派生出新的数据,假如还需要展示一个当前 count 的平方,则可创建如下的 selector:

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 原生的 useState 保持了 API 上的一致,使用 Recoil 中的 useRecoilState 可进行无缝替换。

import { useRecoilState } from "recoil";

...
const [count, setCount] = useRecoilState(countState)
...

当只需要使用值而不需要对值进行修改时,可使用 useRecoilValue

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 创建的数据,在使用上无任何区别。

当只需要对值进行设置,而又不进行展示时,则可使用 useSetRecoilState

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>;
}

但由于上面 TodoInfo 组件依赖的数据来自异步,所以需要结合 React Suspense 来进行渲染。

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,此时需要使用 useRecoilValueLoadable 并且自己处理数据的状态。

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,因为 todoQuery 依赖于这个 id atom,当 id 变更后,会自动触发新的请求从而更新 todo 数据。即,使用的地方只需要关注 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);
},
});

展示时通过这个 todoState 来获取 todo 的详情:

TodoInfo.tsx

export function TodoInfo({ id }: ITodoInfoProps) {
const todo = useRecoilValue(todoState(id));
return <div>{todo.title}</div>;
}

在需要刷新的地方,更新 todoState 即可:

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;
}
}

在所有需要错误处理的地方使用即可,理论上亦即所有出现 <Suspense> 的地方:

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 会报 Uncaught (in promise) 的错误。

不过我发现,如果在后续 selector 中不使用 async 而是直接返回原始的 Promise 可以临时规避这一问题。

React Suspense 的 bug

当使用文章前面提到的刷新功能时,数据刷新后,Suspense 中组件重新渲染,特定操作下会报 Unable to find node on an unmounted component. 的错误。经后续定位与 Recoil 无关,实为 React Suspense 的 bug,已在 16.9 及之后的版本修复。

Fix a crash inside findDOMNode for components wrapped in . (@acdlite in #15312)

-- React 16.9 release change log 中的记录

相关资源

The text was updated successfully, but these errors were encountered:

Recoil 的使用的更多相关文章

  1. 14072202(带IK的Recoil)

    [目标] 带IK的Recoil [思路] 1 继承于USkelControlLimb和UGameSkelCtrl_Recoil 2 效果对比  以这个骨骼为例 Recoil Limb 可见,Recoi ...

  2. JELLY技术周刊 Vol.24 -- 技术周刊 &#183; 实现 Recoil 只需百行代码?

    蒲公英 · JELLY技术周刊 Vol.24 理解一个轮子最好的方法就是仿造一个轮子,很多框架都因此应运而生,比如面向 JS 开发者的 AI 工具 Danfo.js:参考 qiankun 的微前端框架 ...

  3. Recoil & React official state management

    Recoil & React official state management Redux Recoil.js https://recoiljs.org/ A state managemen ...

  4. Recoil 默认值及数据级联的使用

    Recoil 中默认值及数据间的依赖 通过 Atom 可方便地设置数据的默认值, const fontSizeState = atom({ key: 'fontSizeState', default: ...

  5. Recoil 中多级数据联动及数据重置的合理做法

    前情回顾 书接上回,前面引出了在数据存在级联的情况下,各下拉框之间的默认值及值变化的处理.简单回顾一下: 场景是: 地域下拉决定可选的可用区 默认选中第一个地域,通过设置 atom 的 default ...

  6. Recoil 中默认值的正确处理

    继续使用 Recoil 默认值及数据级联的使用 的地域可用区级联的例子. 地域变更后可用区随之联动,两个下拉框皆默认选中第一个可选项. 从 URL 获取默认值 考虑这种情况,当 URL 中带了 que ...

  7. Recoil Input 光标位置被重置到末尾的问题

    考察如下代码,页面中有个输入框,通过 Recoil Atom 来存储输入的值. App.tsx function NameInput() { const [name, setName] = useRe ...

  8. 14073102(CCDIKRecoil)

    [目标] CCDIKRecoil [思路] 1 CCDIK和Recoil的结合 2 Recoil的回弹机制,逐渐回到原来位置 3 添加一个Recoil基类 [步骤] 1 将\Src\GameFrame ...

  9. apex-utility-ai-unity-survival-shooter

    The AI has the following actions available: Action Function Shoot Fires the Kalashnikov Reload Reloa ...

随机推荐

  1. how to get svg text tspan x,y position value in js

    how to get svg text tspan x,y position value in js <svg xmlns="http://www.w3.org/2000/svg&qu ...

  2. Flutter: random color

    import 'dart:math' as math; import 'package:flutter/material.dart'; void main() => runApp(App()); ...

  3. 你真的了解URLEncode吗?

    使用浏览器进行Http网络请求时,若请求query中包含中文,中文会被编码为 %+16进制+16进制形式,但你真的深入了解过,为什么要进行这种转义编码吗?编码的原理又是什么? 例如,浏览器中进行百度搜 ...

  4. Docker中配置MySQL并实现远程访问

    Docker配置MySQL容器 拉取MySQL镜像 docker pull mysql:5.6 有可能会因为网络问题失败,重复尝试. 创建容器 docker run -d --name selfdef ...

  5. SpringBoot源码解析

    1.@SpringBootApplication springboot采用注解方式开发的,当创建了一个springboot项目时,在启动类上会有一个注解@SpringBootApplication,这 ...

  6. 如何理解JavaScript中的函数

    转: 如何理解JavaScript中的函数 JS中的函数简介 JS中的函数是一种通过调用来完成具体业务的一段代码块.最核心的目的是将可重复执行的操作进行封装,然后供调用方无限制的调用. JS中的函数的 ...

  7. dapr学习:dapr介绍

    该部分主要是给出学习dapr的入门,描述dapr全貌告诉你dapr是啥以及介绍dapr的主要功能与组件 该部分分为两章: 第一章:介绍dapr 第二章:调试dapr的解决方案项目 1. 介绍dapr ...

  8. POJ-3259(最短路+Bellman-Ford算法判负圈)

    Wormholes POJ-3259 这题是最短路问题中判断是否存在负圈的模板题. 判断负圈的一个关键就是理解:如果在图中不存在从s可达的负圈,最短路径不会经过一个顶点两次.while循环最多执行v- ...

  9. JVM 中的异常

    StackOverflowError 在 JVM 的栈中,如果线程要创建的栈帧大小大于栈容量的大小时,就会抛出 java.lang.StackOverflowError.比如下面的代码 public ...

  10. 前端学习 node 快速入门 系列 —— 初步认识 node

    其他章节请看: 前端学习 node 快速入门 系列 初步认识 node node 是什么 node(或者称node.js)是 javaScript(以下简称js) 运行时的一个环境.不是一门语言. 以 ...