├── static ├── CNAME └── img │ ├── favicon.ico │ ├── og-image.png │ ├── oss_logo.png │ ├── logo.svg │ ├── undraw_docusaurus_tree.svg │ ├── undraw_docusaurus_mountain.svg │ └── undraw_docusaurus_react.svg ├── .prettierignore ├── docs ├── api-reference │ ├── utils │ │ ├── noWait.md │ │ ├── waitForAll.md │ │ ├── waitForAny.md │ │ ├── waitForNone.md │ │ ├── constSelector.md │ │ ├── errorSelector.md │ │ ├── selectorFamily.md │ │ └── atomFamily.md │ └── core │ │ ├── useRecoilStateLoadable.md │ │ ├── useResetRecoilState.md │ │ ├── useRecoilCallback.md │ │ ├── isRecoilValue.md │ │ ├── RecoilRoot.md │ │ ├── useRecoilValue.md │ │ ├── useSetRecoilState.md │ │ ├── atom.md │ │ ├── useRecoilState.md │ │ ├── useRecoilValueLoadable.md │ │ └── selector.md ├── guides │ ├── usage-flow.md │ ├── writing-test.md │ ├── code-splitting.md │ ├── migrating │ │ ├── from-mobx.md │ │ ├── from-redux.md │ │ └── from-react-state.md │ ├── usage-typescript.md │ ├── persistence.md │ ├── asynchronous-state-sync.md │ └── asynchronous-data-queries.md ├── basic-tutorial │ ├── demo.md │ ├── intro.md │ ├── atoms.mdx │ ├── performance.md │ └── selectors.md ├── introduction │ ├── installation.md │ ├── motivation.md │ ├── core-concepts.md │ └── getting-started.mdx └── mdx.md ├── .prettierrc ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── src ├── pages │ ├── styles.module.css │ └── index.js ├── css │ └── custom.css └── components │ ├── introduction │ └── GettingStarted.jsx │ └── basic-tutorial │ ├── TodoListAtoms.jsx │ └── TodoListSelectors.jsx ├── .eslintrc.js ├── .all-contributorsrc ├── package.json ├── README.md ├── sidebars.js └── docusaurus.config.js /static/CNAME: -------------------------------------------------------------------------------- 1 | recoil.js.cn 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .docusaurus 4 | README.md 5 | -------------------------------------------------------------------------------- /docs/api-reference/utils/noWait.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: noWait() 3 | --- 4 | -------------------------------------------------------------------------------- /docs/api-reference/utils/waitForAll.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: waitForAll() 3 | --- 4 | -------------------------------------------------------------------------------- /docs/api-reference/utils/waitForAny.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: waitForAny() 3 | --- 4 | -------------------------------------------------------------------------------- /docs/api-reference/utils/waitForNone.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: waitForNone() 3 | --- 4 | -------------------------------------------------------------------------------- /docs/api-reference/utils/constSelector.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: constSelector() 3 | --- 4 | -------------------------------------------------------------------------------- /docs/api-reference/utils/errorSelector.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: errorSelector() 3 | --- 4 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjavac/recoil/HEAD/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjavac/recoil/HEAD/static/img/og-image.png -------------------------------------------------------------------------------- /static/img/oss_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjavac/recoil/HEAD/static/img/oss_logo.png -------------------------------------------------------------------------------- /docs/api-reference/core/useRecoilStateLoadable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRecoilStateLoadable() 3 | --- 4 | -------------------------------------------------------------------------------- /docs/guides/usage-flow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage with Flow 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/guides/writing-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writing Tests 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/basic-tutorial/demo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Demo (Todo List) 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/guides/code-splitting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Code Splitting 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/guides/migrating/from-mobx.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: From MobX 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/guides/migrating/from-redux.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: From Redux 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/guides/usage-typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage with TypeScript 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /docs/guides/migrating/from-react-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: From React State 3 | --- 4 | 5 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": false, 4 | "jsxBracketSameLine": true, 5 | "printWidth": 80, 6 | "proseWrap": "never", 7 | "singleQuote": true, 8 | "trailingComma": "all" 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # ESLint 23 | .eslintcache 24 | -------------------------------------------------------------------------------- /docs/introduction/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 安装 3 | --- 4 | 5 | Recoil 安装包已经发布在 npm。可以通过下面命令来安装 Recoil 的最新稳定版: 6 | 7 | ```shell 8 | npm install recoil 9 | ``` 10 | 11 | 或者使用 yarn: 12 | 13 | ```shell 14 | yarn add recoil 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/basic-tutorial/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 简介 3 | --- 4 | 5 | 这一部分假设你已经安装了 Recoil 和 React。想要了解如何从零开始 Recoil 和 React,请查看[起步](/docs/introduction/getting-started)页。接下来部分的组件假定在父树中已经有``。 6 | 7 | 在这个教程中,我们将会构建一个简单的待办事项应用程序,我们的应用将有以下功能: 8 | 9 | - 增加待办事项 10 | - 编辑待办事项 11 | - 删除待办事项 12 | - 过滤待办事项 13 | - 显示有用的统计数据 14 | 15 | 同时,我们将介绍 Recoil API 的 atoms, selectors, atom families, 和 hooks。我们还将介绍优化。 16 | -------------------------------------------------------------------------------- /docs/api-reference/core/useResetRecoilState.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useResetRecoilState() 3 | --- 4 | 5 | Returns a function that will reset the value of the given state to its default value. 6 | 7 | --- 8 | 9 | - `state`: a writeable Recoil state 10 | 11 | ### Example 12 | 13 | ```jsx 14 | import {todoListState} from '../atoms/todoListState'; 15 | 16 | const TodoResetButton = () => { 17 | const resetList = useResetRecoilState(todoListState); 18 | return ; 19 | }; 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/mdx.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mdx 3 | title: Powered by MDX 4 | --- 5 | 6 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 7 | 8 | export const Highlight = ({children, color}) => ( {children} ); 14 | 15 | Docusaurus green and Facebook blue are my favorite colors. 16 | 17 | I can write **Markdown** alongside my _JSX_! 18 | -------------------------------------------------------------------------------- /docs/api-reference/core/useRecoilCallback.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRecoilCallback() 3 | --- 4 | 5 | ### Example 6 | 7 | ```jsx 8 | import {atom, useRecoilCallback} from 'recoil'; 9 | 10 | const itemsInCart = atom({ 11 | key: 'itemsInCart', 12 | default: 0, 13 | }); 14 | 15 | function CartInfoDebug() { 16 | const logCartItems = useRecoilCallback(async ({getPromise}) => { 17 | const numItemsInCart = await getPromise(itemsInCart); 18 | 19 | console.log('Items in cart: ', numItemsInCart); 20 | }); 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/api-reference/core/isRecoilValue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: isRecoilValue(value) 3 | sidebar_label: isRecoilValue() 4 | --- 5 | 6 | Returns `true` if `value` is either an atom or selector and `false` otherwise. 7 | 8 | --- 9 | 10 | ### Example 11 | 12 | ```jsx 13 | import {atom, isRecoilValue} from 'recoil'; 14 | 15 | const counter = atom({ 16 | key: 'myCounter', 17 | default: 0, 18 | }); 19 | 20 | const strCounter = selector({ 21 | key: 'myCounterStr', 22 | get: ({get}) => String(get(counter)), 23 | }); 24 | 25 | isRecoilValue(counter); // true 26 | isRecoilValue(strCounter); // true 27 | 28 | isRecoilValue(5); // false 29 | isRecoilValue({}); // false 30 | ``` 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12 13 | - run: yarn --frozen-lockfile 14 | - run: yarn ci 15 | - run: yarn build 16 | 17 | - name: Deploy 18 | if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'justjavac/recoil' 19 | uses: peaceiris/actions-gh-pages@v3 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | publish_dir: ./build 23 | -------------------------------------------------------------------------------- /docs/introduction/motivation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 初衷 3 | --- 4 | 5 | 秉承简单与兼容性至上的原则,最好的状态管理方式当然是直接使用 React 内置能力而不是外部全部状态。然而 React 确实存在着一些问题: 6 | 7 | - 组件状态只能与其祖先组件进行共享,这可能会带来组件树中大量的重绘开销。 8 | - Context 只能保存一个特定值而不是与其 Consumer 共享一组不确定的值。 9 | - 以上两点导致组件树顶部组件(状态生产者)与组件树底部组件(状态消费者)之间的代码拆分变得非常困难。 10 | 11 | 我们希望在尽可能保持 React 代码风格和语义化的前提下解决以上问题。 12 | 13 | Recoil 在组件树中定义了一个正交且内聚的单向图谱。状态变更通过以下方法从图谱的底部(`atoms`)通过纯函数(`selectors`)进入组件: 14 | 15 | - 我们提供了一些无依赖的方法,这些方法像 React 局部状态一样暴露相同的 get/set 接口(简单理解为 `reducers` 之类的概念亦可)。 16 | - 我们能够与一些 React 新功能(比如并发模式)兼容。 17 | - 状态定义是可伸缩和分布式的,代码拆分成为可能。 18 | - 不用修改组件即可派生数据状态。 19 | - 派生数据状态支持同步和异步。 20 | - 我们把跳转看作一级概念,甚至可以对链接中的状态流转进行编码。 21 | - 所以可以简单地使用向后兼容的方式来持久化整个应用的状态,应用变更时持久化状态也可以因此得以保留。 22 | -------------------------------------------------------------------------------- /src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | /** 11 | * CSS files with the .module.css suffix will be treated as CSS modules 12 | * and scoped locally. 13 | */ 14 | 15 | .heroBanner { 16 | padding: 4rem 0; 17 | text-align: center; 18 | position: relative; 19 | overflow: hidden; 20 | } 21 | 22 | @media screen and (max-width: 966px) { 23 | .heroBanner { 24 | padding: 2rem; 25 | } 26 | } 27 | 28 | .buttons { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | } 33 | 34 | .features { 35 | display: flex; 36 | align-items: center; 37 | padding: 2rem 0; 38 | width: 100%; 39 | } 40 | 41 | .featureImage { 42 | height: 200px; 43 | width: 200px; 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | const OFF = 0; 11 | // const WARNING = 1; 12 | const ERROR = 2; 13 | 14 | module.exports = { 15 | env: { 16 | browser: true, 17 | commonjs: true, 18 | jest: true, 19 | node: true, 20 | }, 21 | parser: 'babel-eslint', 22 | parserOptions: { 23 | allowImportExportEverywhere: true, 24 | }, 25 | extends: ['airbnb', 'prettier', 'prettier/react'], 26 | plugins: ['react-hooks'], 27 | rules: { 28 | // Ignore certain webpack alias because it can't be resolved 29 | 'import/no-unresolved': [ 30 | ERROR, 31 | {ignore: ['^@theme', '^@docusaurus', '^@generated']}, 32 | ], 33 | 'import/extensions': OFF, 34 | 'react/jsx-closing-bracket-location': OFF, // Conflicts with Prettier. 35 | 'react/jsx-filename-extension': OFF, 36 | 'react-hooks/rules-of-hooks': ERROR, 37 | 'react/prop-types': OFF, // PropTypes aren't used much these days. 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#贡献者)", 8 | "contributorTemplate": "\" target=\"_blank\">\" width=\"<%= options.imageSize %>px;\" alt=\"\"/>
<%= contributor.name %>(@<%= contributor.login %>)
", 9 | "contributors": [ 10 | { 11 | "login": "justjavac", 12 | "name": "迷渡", 13 | "avatar_url": "https://avatars1.githubusercontent.com/u/359395?v=4", 14 | "profile": "https://twitter.com/justjavac", 15 | "contributions": [ 16 | "translation" 17 | ] 18 | }, 19 | { 20 | "login": "bayunjiang", 21 | "name": "八云酱", 22 | "avatar_url": "https://avatars3.githubusercontent.com/u/19381311?v=4", 23 | "profile": "https://www.bayun.org", 24 | "contributions": [ 25 | "translation" 26 | ] 27 | } 28 | ], 29 | "contributorsPerLine": 7, 30 | "projectName": "recoil", 31 | "projectOwner": "justjavac", 32 | "repoType": "github", 33 | "repoHost": "https://github.com", 34 | "skipCi": true 35 | } 36 | -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | /** 11 | * Any CSS included here will be global. The classic template 12 | * bundles Infima by default. Infima is a CSS framework designed to 13 | * work well for content-centric websites. 14 | */ 15 | 16 | /* You can override the default Infima variables here. */ 17 | :root { 18 | --ifm-color-primary: #3578e5; 19 | --ifm-color-primary-dark: #1d68e1; 20 | --ifm-color-primary-darker: #1b62d4; 21 | --ifm-color-primary-darkest: #1751af; 22 | --ifm-color-primary-light: #4e89e8; 23 | --ifm-color-primary-lighter: #5a91ea; 24 | --ifm-color-primary-lightest: #80aaef; 25 | --ifm-code-font-size: 95%; 26 | } 27 | 28 | .docusaurus-highlight-code-line { 29 | background-color: rgb(72, 77, 91); 30 | display: block; 31 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 32 | padding: 0 var(--ifm-pre-padding); 33 | } 34 | 35 | .hero__title, 36 | .hero__subtitle, 37 | .hero__button { 38 | color: white; 39 | } 40 | 41 | .hero__button.button.button--secondary.button--outline:not(.button--active):not(:hover) { 42 | color: white; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/introduction/GettingStarted.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | RecoilRoot, 4 | atom, 5 | selector, 6 | useRecoilState, 7 | useRecoilValue, 8 | } from 'recoil'; 9 | 10 | const textState = atom({ 11 | key: 'textState', // 唯一标识 12 | default: '', // 默认值 13 | }); 14 | 15 | const charCountState = selector({ 16 | key: 'charCountState', // 唯一标识 17 | get: ({get}) => { 18 | const text = get(textState); 19 | 20 | return text.length; 21 | }, 22 | }); 23 | 24 | function App() { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | function CharacterCounter() { 33 | return ( 34 |
35 | 36 | 37 |
38 | ); 39 | } 40 | 41 | function CharacterCount() { 42 | const count = useRecoilValue(charCountState); 43 | 44 | return <>输入长度: {count}; 45 | } 46 | 47 | function TextInput() { 48 | const [text, setText] = useRecoilState(textState); 49 | 50 | const onChange = (event) => { 51 | setText(event.target.value); 52 | }; 53 | 54 | return ( 55 |
56 | 57 |
58 | 输入文本: {text} 59 |
60 | ); 61 | } 62 | 63 | export default App; 64 | -------------------------------------------------------------------------------- /docs/api-reference/core/RecoilRoot.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | sidebar_label: 4 | --- 5 | 6 | Provides the context in which atoms have values. Must be an ancestor of any component that uses any Recoil hooks. Multiple roots may co-exist; atoms will have distinct values within each root. If they are nested, the innermost root will completely mask any outer roots. 7 | 8 | --- 9 | 10 | - `props` 11 | - `initializeState?`: `({set, setUnvalidatedAtomValues}) => void`. 12 | - A function that will be called when RecoilStore is first rendered which can set initial values for atoms. It is provided with two arguments: 13 | - `set`: `(RecoilValue, T) => void` 14 | - Sets the initial value of a single atom to the provided value. 15 | - `setUnvalidatedAtomValues`: `(Map) => void` 16 | - Sets the initial value for any number of atoms whose keys are the keys in the provided map. As with `useSetUnvalidatedAtomValues`, the validator for each atom will be called when it is next read, and setting an atom without a configured validator will result in an exception. 17 | 18 | ### Example 19 | 20 | ```jsx 21 | import {RecoilRoot} from 'recoil'; 22 | 23 | function AppRoot() { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/api-reference/core/useRecoilValue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRecoilValue() 3 | sidebar_label: useRecoilValue() 4 | --- 5 | 6 | Returns the value of the given Recoil state. 7 | 8 | This hook will implicitly subscribe the component to the given state. 9 | 10 | --- 11 | 12 | - `state`: an [`atom`](/docs/api-reference/core/atom) or [`selector`](/docs/api-reference/core/selector) 13 | 14 | This is the recommended hook to use when a component intends to read state without writing to it as this hook works with both **read-only state** and **writeable state**. Atoms are writeable state while selectors may be either read-only or writeable. See [`selector()`](/docs/api-reference/core/selector) for more info. 15 | 16 | ### Example 17 | 18 | ```jsx 19 | import {atom, selector, useRecoilValue} from 'recoil'; 20 | 21 | const namesState = atom({ 22 | key: 'namesState', 23 | default: ['', 'Ella', 'Chris', '', 'Paul'], 24 | }); 25 | 26 | const filteredNamesState = selector({ 27 | key: 'filteredNamesState', 28 | get: ({get}) => get(namesState).filter((str) => str !== ''), 29 | }); 30 | 31 | function NameDisplay() { 32 | const names = useRecoilValue(namesState); 33 | const filteredNames = useRecoilValue(filteredNamesState); 34 | 35 | return ( 36 | <> 37 | Original names: {names.join(',')} 38 |
39 | Filtered names: {filteredNames.join(',')} 40 | 41 | ); 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/api-reference/core/useSetRecoilState.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSetRecoilState() 3 | sidebar_label: useSetRecoilState() 4 | --- 5 | 6 | Returns a setter function for updating the value of writeable Recoil state. 7 | 8 | --- 9 | 10 | - `state`: writeable Recoil state (an [`atom`](/docs/api-reference/core/atom) or a _writeable_ [`selector`](/docs/api-reference/core/selector)) 11 | 12 | This is the recommended hook to use when a component intends to write to state without reading it. If a component used the `useRecoilState()` hook to get the setter, it would also subscribe to updates and re-render when the atom or selector updated. Using `useSetRecoilState()` allows a component to set the value without re-rendering when the value changes. 13 | 14 | ### Example 15 | 16 | ```jsx 17 | import {atom, useSetRecoilState} from 'recoil'; 18 | 19 | const namesState = atom({ 20 | key: 'namesState', 21 | default: ['Ella', 'Chris', 'Paul'], 22 | }); 23 | 24 | function NameInput() { 25 | const [name, setName] = useState(''); 26 | const setNamesState = useSetRecoilState(namesState); 27 | 28 | const addName = () => { 29 | setNamesState((existingNames) => [...existingNames, name]); 30 | }; 31 | 32 | const onChange = (e) => { 33 | setName(e.target.value); 34 | }; 35 | 36 | return ( 37 | <> 38 | 39 | 40 | 41 | ); 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recoil", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "docusaurus start", 7 | "build": "docusaurus build", 8 | "swizzle": "docusaurus swizzle", 9 | "deploy": "docusaurus deploy", 10 | "ci": "yarn lint && yarn prettier:diff", 11 | "lint": "eslint --cache \"**/*.js\"", 12 | "prettier": "prettier --config .prettierrc --write \"**/*.{js,md}\"", 13 | "prettier:diff": "prettier --config .prettierrc --list-different \"**/*.{js,md}\"" 14 | }, 15 | "dependencies": { 16 | "@docusaurus/core": "^2.0.0-alpha.54", 17 | "@docusaurus/preset-classic": "^2.0.0-alpha.54", 18 | "classnames": "^2.2.6", 19 | "react": "^16.8.4", 20 | "react-dom": "^16.8.4", 21 | "recoil": "0.0.7" 22 | }, 23 | "devDependencies": { 24 | "babel-eslint": "^10.0.3", 25 | "eslint": "^7.1.0", 26 | "eslint-config-airbnb": "^18.0.1", 27 | "eslint-config-prettier": "^6.7.0", 28 | "eslint-plugin-import": "^2.18.2", 29 | "eslint-plugin-jsx-a11y": "^6.2.3", 30 | "eslint-plugin-react": "^7.20.0", 31 | "eslint-plugin-react-hooks": "^4.0.4", 32 | "prettier": "^2.0.2" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recoil 中文文档 2 | 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#贡献者) 5 | 6 | 7 | 当前翻译版本 [e377043](https://github.com/facebookexperimental/Recoil/commit/e37704379e13c11c4ed4afed8da553157e3aae96)(2020-05-25T10:09:16Z) 8 | 9 | ## 安装 10 | 11 | ``` 12 | $ yarn 13 | ``` 14 | 15 | ## 本地开发 16 | 17 | ``` 18 | $ yarn start 19 | ``` 20 | 21 | 这个命令会开启一个服务,并自动打开浏览器。当有文件更改时页面会自动刷新。 22 | 23 | ## 构建 24 | 25 | ``` 26 | $ yarn build 27 | ``` 28 | 29 | 这个命令会生成静态文件到 `build` 目录。 30 | 31 | ## 部署 32 | 33 | ``` 34 | $ GIT_USER= USE_SSH=true yarn deploy 35 | ``` 36 | 37 | 如果需要部署到 GitHub pages,这个命令会先构建静态页面,然后推送到 `gh-pages` 分支。 38 | 39 | ## 贡献者 40 | 41 | 感谢所有的贡献者们 🎉 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |

迷渡(@justjavac)

八云酱(@bayunjiang)
52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/api-reference/core/atom.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: atom(options) 3 | sidebar_label: atom() 4 | --- 5 | 6 | Returns writeable Recoil state. 7 | 8 | --- 9 | 10 | - `options` 11 | - `key`: A unique string used to identify the atom internally. This string should be unique with respect to other atoms and selectors in the entire application. 12 | - `default`: The initial value of the atom. 13 | 14 | Most often, you'll use the following hooks to interact with atoms: 15 | 16 | - [`useRecoilState()`](/docs/api-reference/core/useRecoilState): use this hook when you intend on both reading and writing to the atom. This hook subscribes the component to the atom. 17 | - [`useRecoilValue()`](/docs/api-reference/core/useRecoilValue): use this hook when you intend on only reading the atom. This hook subscribes the component to the atom. 18 | - [`useSetRecoilState()`](/docs/api-reference/core/useSetRecoilState): use this hook when you intend on only writing to the atom. 19 | - [`useResetRecoilState()`](/docs/api-reference/core/useResetRecoilState): use this hook to reset an atom to its default value. 20 | 21 | For rare cases where you need to read an atom's value without subscribing to the component, see [`useRecoilCallback()`](/docs/api-reference/core/useRecoilCallback). 22 | 23 | ### Example 24 | 25 | ```jsx 26 | import {atom, useRecoilState} from 'recoil'; 27 | 28 | const counter = atom({ 29 | key: 'myCounter', 30 | default: 0, 31 | }); 32 | 33 | function Counter() { 34 | const [count, setCount] = useRecoilState(counter); 35 | const incrementByOne = () => setCount(count + 1); 36 | 37 | return ( 38 |
39 | Count: {count} 40 |
41 | 42 |
43 | ); 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/api-reference/core/useRecoilState.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRecoilState() 3 | sidebar_label: useRecoilState() 4 | --- 5 | 6 | Returns a tuple where the first element is the value of state and the second element is a setter function that will update the value of the given state when called. 7 | 8 | This hook will implicitly subscribe the component to the given state. 9 | 10 | --- 11 | 12 | - `state`: an [`atom`](/docs/api-reference/core/atom) or a _writeable_ [`selector`](/docs/api-reference/core/selector). Writeable selectors are selectors that were have both a `get` and `set` in their definition while read-only selectors only have a `get`. 13 | 14 | This is the recommended hook to use when a component intends to read and write state. 15 | 16 | ### Example 17 | 18 | ```jsx 19 | import {atom, selector, useRecoilState} from 'recoil'; 20 | 21 | const tempFahrenheit = atom({ 22 | key: 'tempFahrenheit', 23 | default: 32, 24 | }); 25 | 26 | const tempCelcius = selector({ 27 | key: 'tempCelcius', 28 | get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9, 29 | set: ({set}, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32), 30 | }); 31 | 32 | function TempCelcius() { 33 | const [tempF, setTempF] = useRecoilState(tempFahrenheit); 34 | const [tempC, setTempC] = useRecoilState(tempCelcius); 35 | 36 | const addTenCelcius = () => setTempC(tempC + 10); 37 | const addTenFahrenheit = () => setTempF(tempF + 10); 38 | 39 | return ( 40 |
41 | Temp (Celcius): {tempC} 42 |
43 | Temp (Fahrenheit): {tempF} 44 |
45 | 46 |
47 | 48 |
49 | ); 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/api-reference/core/useRecoilValueLoadable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRecoilValueLoadable() 3 | sidebar_label: useRecoilValueLoadable() 4 | --- 5 | 6 | ```jsx 7 | function useRecoilValueLoadable(state: RecoilValue): Loadable 8 | ``` 9 | 10 | Returns a `Loadable`. 11 | 12 | This hook is intended to be used for reading the value of asynchronous selectors. This hook will implicitly subscribe the component to the given state. 13 | 14 | Unlike `useRecoilValue()`, this hook will not throw a `Promise` when reading from a pending asynchronous selector (for the purpose of working alongside Suspense). Instead, this hook returns a `Loadable`, which is an object with the following interface: 15 | 16 | - `state`: indicates the status of the selector. Possible values are `'hasValue'`, `'hasError'`, `'loading'`. 17 | - `contents`: The value represented by this `Loadable`. If the state is `hasValue`, it is the actual value, if the state is `hasError` it is the `Error` object that was thrown, and if the state is `loading`, then it is a `Promise` of the value. 18 | - `getValue()`: if there is an error, this function throws the error. If selector is still loading, it throws a Promise. Otherwise it returns the value that the selector resolved to. 19 | - `toPromise()`: returns a `Promise` that will resolve when the selector has resolved. If the selector is synchronous or has already resolved, it returns a `Promise` that resolves immediately. 20 | 21 | --- 22 | 23 | - `state`: an [`atom`](/docs/api-reference/core/atom) or [`selector`](/docs/api-reference/core/selector) that _may_ have some asynchronous operations. The status of the returned loadable will depend on the status of the provided state selector. 24 | 25 | ### Example 26 | 27 | ```jsx 28 | function UserInfo({userID}) { 29 | const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID)); 30 | switch (userNameLoadable.state) { 31 | case 'hasValue': 32 | return
{userNameLoadable.contents}
; 33 | case 'loading': 34 | return
Loading...
; 35 | case 'hasError': 36 | throw userNameLoadable.contents; 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/api-reference/core/selector.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: selector(options) 3 | sidebar_label: selector() 4 | --- 5 | 6 | Returns writeable or read-only Recoil state, depending on the options passed to the function. 7 | 8 | Selectors represent **derived state**. You can think of derived state as the output of passing state to a pure function that modifies the given state in some way. 9 | 10 | --- 11 | 12 | - `options` 13 | - `key`: A unique string used to identify the atom internally. This string should be unique with respect to other atoms and selectors in the entire application. 14 | - `get`: A function that is passed an object as the first parameter containing the following properties: 15 | - `get`: a function used to retrieve values from other atoms/selectors. All atoms/selectors passed to this function will be implicitly added to a list of **dependencies** for the selector. If any of the selector's dependencies change, the selector will re-evaluate. 16 | - `set?`: If this property is set, the selector will return **writeable** state. A function that is passed an object as the first parameter containing the following properties: 17 | - `get`: a function used to retrieve values from other atoms/selectors. This function will not subscribe the selector to the given atoms/selectors. 18 | - `set`: a function used to set the values of Recoil state. The first parameter is the Recoil state and the second parameter is the new value. 19 | 20 | ### Example (Synchronous) 21 | 22 | ```jsx 23 | import {atom, selector, useRecoilState} from 'recoil'; 24 | 25 | const tempFahrenheit = atom({ 26 | key: 'tempFahrenheit', 27 | default: 32, 28 | }); 29 | 30 | const tempCelcius = selector({ 31 | key: 'tempCelcius', 32 | get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9, 33 | set: ({set}, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32), 34 | }); 35 | 36 | function TempCelcius() { 37 | const [tempF, setTempF] = useRecoilState(tempFahrenheit); 38 | const [tempC, setTempC] = useRecoilState(tempCelcius); 39 | 40 | const addTenCelcius = () => setTempC(tempC + 10); 41 | const addTenFahrenheit = () => setTempF(tempF + 10); 42 | 43 | return ( 44 |
45 | Temp (Celcius): {tempC} 46 |
47 | Temp (Fahrenheit): {tempF} 48 |
49 | 50 |
51 | 52 |
53 | ); 54 | } 55 | ``` 56 | 57 | ### Example (Asynchronous) 58 | -------------------------------------------------------------------------------- /sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | module.exports = { 11 | someSidebar: { 12 | 介绍: [ 13 | 'introduction/motivation', 14 | 'introduction/core-concepts', 15 | 'introduction/installation', 16 | 'introduction/getting-started', 17 | ], 18 | 基础: [ 19 | 'basic-tutorial/intro', 20 | 'basic-tutorial/atoms', 21 | 'basic-tutorial/selectors', 22 | // 'basic-tutorial/demo', 23 | // 'basic-tutorial/performance', 24 | ], 25 | 指南: [ 26 | // { 27 | // 'Migrating to Recoil': [ 28 | // 'guides/migrating/from-react-state', 29 | // 'guides/migrating/from-redux', 30 | // 'guides/migrating/from-mobx', 31 | // ], 32 | // }, 33 | // 'guides/usage-flow', 34 | // 'guides/usage-typescript', 35 | 'guides/asynchronous-data-queries', 36 | 'guides/asynchronous-state-sync', 37 | 'guides/persistence', 38 | // 'guides/writing-test', 39 | // 'guides/code-splitting', 40 | ], 41 | 42 | 'API 参考': [ 43 | { 44 | Core: [ 45 | 'api-reference/core/RecoilRoot', 46 | 'api-reference/core/atom', 47 | 'api-reference/core/selector', 48 | 'api-reference/core/isRecoilValue', 49 | // 'api-reference/core/DefaultValue', 50 | { 51 | Hooks: [ 52 | 'api-reference/core/useRecoilState', 53 | 'api-reference/core/useRecoilValue', 54 | 'api-reference/core/useSetRecoilState', 55 | 'api-reference/core/useResetRecoilState', 56 | 'api-reference/core/useRecoilValueLoadable', 57 | 'api-reference/core/useRecoilStateLoadable', 58 | 'api-reference/core/useRecoilCallback', 59 | ], 60 | }, 61 | ], 62 | }, 63 | { 64 | Utils: [ 65 | 'api-reference/utils/atomFamily', 66 | 'api-reference/utils/selectorFamily', 67 | 'api-reference/utils/constSelector', 68 | 'api-reference/utils/errorSelector', 69 | 'api-reference/utils/noWait', 70 | 'api-reference/utils/waitForAll', 71 | 'api-reference/utils/waitForAny', 72 | 'api-reference/utils/waitForNone', 73 | ], 74 | }, 75 | ], 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /docs/introduction/core-concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 核心概念 3 | --- 4 | 5 | ## 概述 6 | 7 | Recoil 允许你使用 _Selector_ (纯函数)创建一个从 _Atom_ (共享状态)下沉至组件的数据流转图谱。Atom 表示组件可以订阅的最小状态单元。Selector 可以同步或者异步更新。 8 | 9 | ## Atoms 10 | 11 | Atom 是最小状态单元。它们可以被订阅和更新:当它更新时,所有订阅它的组件都会使用新数据重绘;它可以在运行时创建;它也可以在局部状态使用;同一个 Atom 可以被多个组件使用与共享。 12 | 13 | 使用 `atom` 方法创建 Atom 实例。 14 | 15 | ```javascript 16 | const fontSizeState = atom({ 17 | key: 'fontSizeState', 18 | default: 14, 19 | }); 20 | ``` 21 | 22 | Atom 需要一个唯一标识用于调试、数据持久化和状态查询的高阶方法。必须保证这个唯一标识全局唯一否则将会报错,创建时可以设置默认值。 23 | 24 | 在组件中使用 Hook 方法 `useRecoilState` 对 Atom 进行读写操作,参考 `React.useState`,区别在于这个状态可以在组件之间共享。 25 | 26 | ```jsx 27 | function FontButton() { 28 | const [fontSize, setFontSize] = useRecoilState(fontSizeState); 29 | return ( 30 | 33 | ); 34 | } 35 | ``` 36 | 37 | 点击按钮会依次增加按钮字号,其他组件也会使用相同的字号: 38 | 39 | ```jsx 40 | function Text() { 41 | const [fontSize, setFontSize] = useRecoilState(fontSizeState); 42 | return

这里的字号会同步增大

; 43 | } 44 | ``` 45 | 46 | ## Selectors 47 | 48 | **Selector** 是一个入参为 Atom 或者其他 Selector 的纯函数。当它的上游 Atom 或者 Selector 更新时,它会进行重新计算。Selector 可以像 Atom 一样被组件订阅,当它更新时,订阅它的组件将会重新渲染。 49 | 50 | Selector 通常用于计算一些基于原始状态的派生数据。因为不需要使用 reducer 来保证数据的一致性和有效性,所以可以避免冗余数据。我们使用 Atom 保存一点原始状态,其他数据都是在其基础上计算得来的。因为 Selector 会追踪使用它们的组件以及它们依赖的数据状态,所以函数式编程会比较高效。 51 | 52 | 因为 Seletor 和 Atom 给组件提供相同的方法,所以它们可以相互替代。 53 | 54 | 使用 `selector` 方法创建 Selector 实例。 55 | 56 | ```javascript 57 | const fontSizeLabelState = selector({ 58 | key: 'fontSizeLabelState', 59 | get: ({get}) => { 60 | const fontSize = get(fontSizeState); 61 | const unit = 'px'; 62 | 63 | return `${fontSize}${unit}`; 64 | }, 65 | }); 66 | ``` 67 | 68 | `get` 属性是一个计算函数,它可以使用入参 `get` 字段来访问输入的 Atom 和 Selector。当它访问其他 Atom 和 Selector 时,这层依赖关系会保证更新状态的同步。 69 | 70 | 参考上述 `fontSizeLabelState` 示例,它依赖于 `fontSizeState`。根据之前的描述,`fontSizeLabelState` 使用 `fontSizeState` 作为入参,并返回格式化之后的字号文本。 71 | 72 | 我们可以通过在 `useRecoilValue()` 方法中输入 Atom 或者 Selector 来获取对应的数据。这里不用 `useRecoilState()` 是因为 `fontSizeLabelState` 是一个不可写 Selector,更多细节参考 [Selector](/docs/api-reference/core/selector)。 73 | 74 | ```jsx 75 | function FontButton() { 76 | const [fontSize, setFontSize] = useRecoilState(fontSizeState); 77 | const fontSizeLabel = useRecoilValue(fontSizeLabelState); 78 | 79 | return ( 80 | <> 81 |
当前字号: ${fontSizeLabel}
82 | 83 | 86 | 87 | ); 88 | } 89 | ``` 90 | 91 | 点击按钮可以看到按钮和文本的字号同时在更新。 92 | -------------------------------------------------------------------------------- /docs/introduction/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 起步 3 | --- 4 | 5 | ## 创建 React 应用 6 | 7 | Recoil 只是一个 React 状态管理库,所以使用它之前需要搭建 React 运行环境。推荐使用 [Create React App](https://github.com/facebook/create-react-app#creating-an-app) 创建一个 React 应用: 8 | 9 | ```shell 10 | npx create-react-app my-app 11 | ``` 12 | 13 | > [`npx`](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) 是一个运行工具(npm 5.2+),历史版本[参考这里](https://gist.github.com/gaearon/4064d3c23a77c74a3614c498a8bb1c5f)。 14 | 15 | 更多安装方法参考[官方文档](https://github.com/facebook/create-react-app#creating-an-app)。 16 | 17 | ## 安装 18 | 19 | Recoil 安装包已经发布于 npm。可以通过下面命令来安装 Recoil 的最新稳定版: 20 | 21 | ```shell 22 | npm install recoil 23 | ``` 24 | 25 | 或者使用 yarn: 26 | 27 | ```shell 28 | yarn add recoil 29 | ``` 30 | 31 | ## RecoilRoot 32 | 33 | 组件使用 Recoil 状态之前需要在它的外面包裹一层 `RecoilRoot` 组件。可以直接短平快地放在根组件外面: 34 | 35 | ```jsx 36 | import React from 'react'; 37 | import { 38 | RecoilRoot, 39 | atom, 40 | selector, 41 | useRecoilState, 42 | useRecoilValue, 43 | } from 'recoil'; 44 | 45 | function App() { 46 | return ( 47 | 48 | 49 | 50 | ); 51 | } 52 | ``` 53 | 54 | 之后我们会详细介绍 `CharacterCounter` 组件。 55 | 56 | ## Atom 57 | 58 | **Atom** 表示一小块状态。Atom 可以在任意组件中进行读写。组件读取 Atom 数据将会隐式订阅它,任何更新都会导致订阅它的组件进行重新渲染。 59 | 60 | ```javascript 61 | const textState = atom({ 62 | key: 'textState', // 唯一标识 63 | default: '', // 默认值 64 | }); 65 | ``` 66 | 67 | 在组件中使用 `useRecoilState()` 读写 Atom 数据: 68 | 69 | ```jsx 70 | function CharacterCounter() { 71 | return ( 72 |
73 | 74 | 75 |
76 | ); 77 | } 78 | 79 | function TextInput() { 80 | const [text, setText] = useRecoilState(textState); 81 | 82 | const onChange = (event) => { 83 | setText(event.target.value); 84 | }; 85 | 86 | return ( 87 |
88 | 89 |
90 | 输入文本: {text} 91 |
92 | ); 93 | } 94 | ``` 95 | 96 | ## Selector 97 | 98 | **Selector** 表示一小块派生状态。派生状态是状态通过纯函数计算得来。 99 | 100 | ```jsx 101 | const charCountState = selector({ 102 | key: 'charCountState', // 唯一标识 103 | get: ({get}) => { 104 | const text = get(textState); 105 | 106 | return text.length; 107 | }, 108 | }); 109 | ``` 110 | 111 | 我们可以使用 Hook `useRecoilValue()` 读取 `charCountState` 数据: 112 | 113 | ```jsx 114 | function CharacterCount() { 115 | const count = useRecoilValue(charCountState); 116 | 117 | return <>输入长度: {count}; 118 | } 119 | ``` 120 | 121 | ## 示例 122 | 123 | 上述内容的最终结果: 124 | 125 | import GettingStarted from '../../src/components/introduction/GettingStarted'; 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/components/basic-tutorial/TodoListAtoms.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { 3 | RecoilRoot, 4 | atom, 5 | selector, 6 | useRecoilState, 7 | useRecoilValue, 8 | useSetRecoilState, 9 | } from 'recoil'; 10 | 11 | const todoListState = atom({ 12 | key: 'todoListState', 13 | default: [], 14 | }); 15 | 16 | function App() { 17 | return ( 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | function TodoList() { 25 | const todoList = useRecoilValue(todoListState); 26 | 27 | return ( 28 | <> 29 | { /* */ } 30 | { /* */ } 31 | 32 | 33 | {todoList.map((todoItem) => ( 34 | 35 | ))} 36 | 37 | ); 38 | } 39 | 40 | function TodoItemCreator() { 41 | const [inputValue, setInputValue] = useState(''); 42 | const setTodoList = useSetRecoilState(todoListState); 43 | 44 | const addItem = () => { 45 | setTodoList((oldTodoList) => [ 46 | ...oldTodoList, 47 | { 48 | id: getId(), 49 | text: inputValue, 50 | isComplete: false, 51 | }, 52 | ]); 53 | }; 54 | 55 | const onChange = ({target: {value}}) => { 56 | setInputValue(value); 57 | } 58 | 59 | return ( 60 |
61 | 62 | 63 |
64 | ); 65 | } 66 | 67 | function TodoItem({item}) { 68 | const [todoList, setTodoList] = useRecoilState(todoListState); 69 | const index = todoList.findIndex((listItem) => listItem === item); 70 | 71 | const editItemText = ({target: {value}}) => { 72 | const newList = replaceItemAtIndex(todoList, index, { 73 | ...item, 74 | text: value, 75 | }); 76 | 77 | setTodoList(newList); 78 | }; 79 | 80 | const toggleItemCompletion = () => { 81 | const newList = replaceItemAtIndex(todoList, index, { 82 | ...item, 83 | isComplete: !item.isComplete, 84 | }); 85 | 86 | setTodoList(newList); 87 | }; 88 | 89 | const deleteItem = () => { 90 | const newList = removeItemAtIndex(todoList, index); 91 | 92 | setTodoList(newList); 93 | }; 94 | 95 | return ( 96 |
97 | 98 | 103 | 104 |
105 | ); 106 | } 107 | 108 | function replaceItemAtIndex(arr, index, newValue) { 109 | return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; 110 | } 111 | 112 | function removeItemAtIndex(arr, index) { 113 | return [...arr.slice(0, index), ...arr.slice(index + 1)]; 114 | } 115 | 116 | let id = 0; 117 | function getId() { 118 | return id++; 119 | } 120 | 121 | export default App; 122 | -------------------------------------------------------------------------------- /docs/basic-tutorial/atoms.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Atoms 3 | --- 4 | 5 | Atoms 包含我们应用状态的真实来源。在我们的待办事项列表中,真实来源将是一个对象数组,每个对象表示一个待办事项。 6 | 7 | 我们将称我们的 atom 列表为`todoListState`,并使用`atom()`函数创建它: 8 | 9 | ```javascript 10 | const todoListState = atom({ 11 | key: 'todoListState', 12 | default: [], 13 | }); 14 | ``` 15 | 16 | 我们为 atom 提供一个唯一的`key`,并设置`default`值为空数组。要读取此 atom 的内容,我们可以在`TodoList`组件中使用`useRecoilValue()`hook: 17 | 18 | ```jsx 19 | function TodoList() { 20 | const todoList = useRecoilValue(todoListState); 21 | 22 | return ( 23 | <> 24 | {/* */} 25 | {/* */} 26 | 27 | 28 | {todoList.map((todoItem) => ( 29 | 30 | ))} 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | 注释掉的组件将在以下各部分实现。 37 | 38 | 要创建新的待办事项,我们需要访问一个 setter 函数,该函数将更新`todoListState`的内容。我们可以使用`useSetRecoilState()` hook 在我们的`TodoItemCreator`组件中获取 setter 函数: 39 | 40 | ```jsx 41 | function TodoItemCreator() { 42 | const [inputValue, setInputValue] = useState(''); 43 | const setTodoList = useSetRecoilState(todoListState); 44 | 45 | const addItem = () => { 46 | setTodoList((oldTodoList) => [ 47 | ...oldTodoList, 48 | { 49 | id: getId(), 50 | text: inputValue, 51 | isComplete: false, 52 | }, 53 | ]); 54 | setInputValue(''); 55 | }; 56 | 57 | const onChange = ({target: {value}}) => { 58 | setInputValue(value); 59 | }; 60 | 61 | return ( 62 |
63 | 64 | 65 |
66 | ); 67 | } 68 | 69 | // 用于创建唯一 ID 的工具 70 | let id = 0; 71 | function getId() { 72 | return id++; 73 | } 74 | ``` 75 | 76 | 请注意,我们使用 setter 函数的 **updater** 形式,以便我们可以基于旧的待办事项列表创建新的待办事项列表。 77 | 78 | `TodoItem`组件将显示待办事项的值,同时允许您更改其文本并删除项目。我们使用`useRecoilState()`来读取`todoListState`,并获取一个 setter 函数,用于更新项目文本、将其标记为已完成并删除它: 79 | 80 | ```jsx 81 | function TodoItem({item}) { 82 | const [todoList, setTodoList] = useRecoilState(todoListState); 83 | const index = todoList.findIndex((listItem) => listItem === item); 84 | 85 | const editItemText = ({target: {value}}) => { 86 | const newList = replaceItemAtIndex(todoList, index, { 87 | ...item, 88 | text: value, 89 | }); 90 | 91 | setTodoList(newList); 92 | }; 93 | 94 | const toggleItemCompletion = () => { 95 | const newList = replaceItemAtIndex(todoList, index, { 96 | ...item, 97 | isComplete: !item.isComplete, 98 | }); 99 | 100 | setTodoList(newList); 101 | }; 102 | 103 | const deleteItem = () => { 104 | const newList = removeItemAtIndex(todoList, index); 105 | 106 | setTodoList(newList); 107 | }; 108 | 109 | return ( 110 |
111 | 112 | 117 | 118 |
119 | ); 120 | } 121 | 122 | function replaceItemAtIndex(arr, index, newValue) { 123 | return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; 124 | } 125 | 126 | function removeItemAtIndex(arr, index) { 127 | return [...arr.slice(0, index), ...arr.slice(index + 1)]; 128 | } 129 | ``` 130 | 131 | 有了它,我们有了一个功能齐全的待办事项列表!下一节我们将介绍如何使用 selectors 将列表更新到一个新的水平。 132 | -------------------------------------------------------------------------------- /docs/api-reference/utils/selectorFamily.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: selectorFamily() 3 | sidebar_label: selectorFamily() 4 | --- 5 | 6 | Returns a function that returns a read-only `RecoilValueReadOnly` or writeable `RecoilState` selector. 7 | 8 | A `selectorFamily` is a powerful pattern that is similar to a `selector`, but allows you to pass parameters to the `get` and `set` callbacks of a `selector`. 9 | 10 | ```jsx 11 | function selectorFamily({ 12 | key: string, 13 | 14 | get: Parameter => ({get: GetRecoilValue}) => Promise | RecoilValue | T, 15 | 16 | dangerouslyAllowMutability?: boolean, 17 | }): RecoilValueReadOnly 18 | ``` 19 | 20 | ```jsx 21 | function selectorFamily({ 22 | key: string, 23 | 24 | get: Parameter => ({get: GetRecoilValue}) => Promise | RecoilValue | T, 25 | 26 | set: Parameter => ( 27 | { 28 | get: GetRecoilValue, 29 | set: SetRecoilValue, 30 | reset: ResetRecoilValue, 31 | }, 32 | newValue: T | DefaultValue, 33 | ) => void, 34 | 35 | dangerouslyAllowMutability?: boolean, 36 | }): RecoilState 37 | ``` 38 | 39 | Where 40 | 41 | ```jsx 42 | type ValueOrUpdater = 43 | | T 44 | | DefaultValue 45 | | ((prevValue: T) => T | DefaultValue); 46 | type GetRecoilValue = (RecoilValue) => T; 47 | type SetRecoilValue = (RecoilState, ValueOrUpdater) => void; 48 | type ResetRecoilValue = (RecoilState) => void; 49 | ``` 50 | 51 | --- 52 | 53 | The `selectorFamily()` utility returns a function which can be called with user-defined parameters and returns a selector. Each unique parameter value will return the same memoized selector instance. 54 | 55 | - `options` 56 | - `key`: A unique string used to identify the atom internally. This string should be unique with respect to other atoms and selectors in the entire application. 57 | - `get`: A function that is passed an object of named callbacks that returns the value of the selector, the same as the `selector()` interface. This is wrapped by a function which is passed the parameter from calling the selector family function. 58 | - `set?`: An optional function that will produce writeable selectors when provided. It should be a function that takes an object of named callbacks, same as the `selector()` interface. This is again wrapped by another function with gets the parameters from calling the selector family function. 59 | 60 | ## Example 61 | 62 | ```jsx 63 | const myNumberState = atom({ 64 | key: 'MyNumber', 65 | default: 2, 66 | }); 67 | 68 | const myMultipliedState = selectorFamily({ 69 | key: 'MyMultipliedNumber', 70 | get: (multiplier) => ({get}) => { 71 | return get(myNumberState) * multiplier; 72 | }, 73 | 74 | // optional set 75 | set: (multiplier) => ({set}, newValue) => { 76 | set(myNumberState, newValue / multiplier); 77 | }, 78 | }); 79 | 80 | function MyComponent() { 81 | // defaults to 2 82 | const number = useRecoilValue(myNumberState); 83 | 84 | // defaults to 200 85 | const multipliedNumber = useRecoilValue(myMultipliedState(100)); 86 | 87 | return
...
; 88 | } 89 | ``` 90 | 91 | ## Query Example 92 | 93 | Selector Families are also useful to use for passing parameters to queries: 94 | 95 | ```jsx 96 | const myDataQuery = selectorFamily({ 97 | key: 'MyDataQuery', 98 | get: (queryParameters) => async ({get}) => { 99 | const response = await asyncDataRequest(queryParameters); 100 | if (response.error) { 101 | throw response.error; 102 | } 103 | return response.data; 104 | }, 105 | }); 106 | 107 | function MyComponent() { 108 | const data = useRecoilValue(myDataQuery({userID: 132})); 109 | return
...
; 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/guides/persistence.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: State Persistence 3 | sidebar_label: State Persistence 4 | --- 5 | 6 | Recoil allows you to persist application state using atoms. 7 | 8 | --- 9 | 10 | ## _IMPORTANT NOTE_ 11 | 12 | **_This API is currently under development and still evolving. Please stay tuned..._** 13 | 14 | --- 15 | 16 | ## Saving State 17 | 18 | To save state, subscribe to atom changes and record the new state. You could use React effects to subscribe to individual atoms (_See [Asynchronous State Sync](asynchronous-state-sync)_). However, Recoil provides a hook to allow you to subscribe to state changes for all atoms using **`useTransactionObservation_UNSTABLE()`**. (**_NOTE_**: _This API is currently under development_). 19 | 20 | The subscription callback provides all of the atom state and tells you which atoms changed. From this you can save the changes with the storage and serialization of your preference. Here is an example of a basic implementation using JSON: 21 | 22 | ```jsx 23 | function PersistenceObserver() { 24 | useTransactionObservation_UNSTABLE( 25 | ({atomValues, atomInfo, modifiedAtoms}) => { 26 | for (const modifiedAtom of modifiedAtoms) { 27 | Storage.setItem( 28 | modifiedAtom.key, 29 | JSON.stringify({value: atomValues.get(modifiedAtom)}), 30 | ); 31 | } 32 | }, 33 | ); 34 | } 35 | ``` 36 | 37 | _Storage_ could be the browser URL history, [_LocalStorage_](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), _[AsyncStorage](https://github.com/react-native-community/react-native-async-storage)_ or whichever storage you like. 38 | 39 | - _`atomValues`_ - A Map of the atom keys and values. 40 | - _`modifiedAtoms`_ - Gives you a map containing the modified atoms. 41 | - _`atomInfo`_ - Atom metadata. 42 | 43 | You may not wish to persist all atoms, or some atoms may have different persistence behaviors. You can read metadata (**_NOTE_**: _new API coming soon_) to get options from each atom. 44 | 45 | ## Reestoring State 46 | 47 | After you ensure that you're saving your state to your storage, you need to recover it when loading the app. This can be done using the **`initializeState`** prop on thee **``** component. (**_NOTE_**: _API changes coming soon_). 48 | 49 | `initializeState` is a function that provides a **`set`** method to provide the initial atom value for an atom key. Pass the key for the atom and the stored value to this callback and it will initialize the atom to the restored state. 50 | 51 | Note that it is important to use this prop instead of just manually setting atom values in an effect. Otherwise there will be an initial render without the restored state which can cause flicker or be invalid. 52 | 53 | Here is a basic example: 54 | 55 | ```jsx 56 | const initializeState = ({set}) => { 57 | Storage.getAllKeys((error, keys) => { 58 | const promises = keys.map((key) => Storage.getItem(key)); 59 | Promise.all(promises).then((values) => { 60 | for (let i = 0; i < promises.length; i++) { 61 | const key = keys[i]; 62 | const value = JSON.parse(values[i]).value; 63 | set({key}, value); 64 | } 65 | }); 66 | }); 67 | }; 68 | 69 | return ( 70 | 71 | 72 | 73 | 74 | ); 75 | ``` 76 | 77 | ## Syncing State 78 | 79 | You may also wish for asynchronous updates of the storage, such as the user pressing the browser back button with URL persistence, to sync with the current app state. You can use a React effect to subscribe to these changes and update the value of any modified atoms directly. 80 | 81 | _Example coming soon..._ 82 | 83 | ## Backward-Compatibility and Value Validation 84 | 85 | What if your state storage is not reliable? Or what if you change which atoms or types you are using and need to work with previously persisted state? More documentation and examples on how to handle this coming soon as the API is fianlized... 86 | -------------------------------------------------------------------------------- /docs/api-reference/utils/atomFamily.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: atomFamily() 3 | sidebar_label: atomFamily() 4 | --- 5 | 6 | Returns a function that returns a writeable `RecoilState` atom. 7 | 8 | ```jsx 9 | function atomFamily({ 10 | key: string, 11 | 12 | default: 13 | | RecoilValue 14 | | Promise 15 | | T 16 | | (Parameter => T | RecoilValue | Promise), 17 | 18 | dangerouslyAllowMutability?: boolean, 19 | }): RecoilState 20 | ``` 21 | 22 | --- 23 | 24 | - `options` 25 | - `key`: A unique string used to identify the atom internally. This string should be unique with respect to other atoms and selectors in the entire application. 26 | - `default`: The initial value of the atom. It may either be a value directly, a `RecoilValue` or `Promise` that represents the default value, or a function to get the default value. The callback function is passed a copy of the parameter used when the `atomFamily` function is called. 27 | 28 | An `atom` represents a piece of state with _Recoil_. An atom is created and registered per `` by your app. But, what if your state isn’t global? What if your state is associated with a particular instance of a control, or with a particular element? For example, maybe your app is a UI prototyping tool where the user can dynamically add elements and each element has state, such as its position. Idealy, each element would get its own atom of state. You could implement this yourself via a memoization pattern. But, _Recoil_ provides this pattern for you with the `atomFamily` utility. An Atom Family represents a collection of atoms. When you call `atomFamily` it will return a function which provides the `RecoilState` atom based on the parameters you pass in. 29 | 30 | ## Example 31 | 32 | ```jsx 33 | const elementPositionStateFamily = atomFamily({ 34 | key: 'ElementPosition', 35 | default: [0, 0], 36 | }); 37 | 38 | function ElementListItem({elementID}) { 39 | const position = useRecoilValue(elementPositionStateFamily(elementID)); 40 | return ( 41 |
42 | Element: {elementID} 43 | Position: {position} 44 |
45 | ); 46 | } 47 | ``` 48 | 49 | An `atomFamily()` takes almost the same options as a simple `atom()`. However, the default value can also be parameterized. That means you could provide a function which takes the parameter value and returns the actual default value. For example: 50 | 51 | ```jsx 52 | const myAtomFamily = atomFamily({ 53 | key: ‘MyAtom’, 54 | default: param => defaultBasedOnParam(param), 55 | }); 56 | ``` 57 | 58 | or using `selectorFamily` instead of `selector`, you can also access the parameter value in a `default` selector as well. 59 | 60 | ```jsx 61 | const myAtomFamily = atomFamily({ 62 | key: ‘MyAtom’, 63 | default: selectorFamily({ 64 | key: 'MyAtom/Default', 65 | get: param => ({get}) => { ... }, 66 | }), 67 | }); 68 | ``` 69 | 70 | ## Subscriptions 71 | 72 | One advantage of using this pattern for separate atoms for each element over trying to store a single atom with a map of state for all elements is that they all maintain their own individual subscriptions. So, updating the value for one element will only cause React components that have subscribed to just that atom to update. 73 | 74 | ## Persistence 75 | 76 | Persistence observers will persist the state for each parameter value as a distinct atom with a unique key based on serialization of the parameter value used. Therefore, it is important to only use parameters which are primitives or simple compound objects containing primitives. Custom classes or functions are not allowed. 77 | 78 | It is allowed to “upgrade” a simple `atom` to be an `atomFamily` in a newer version of your app, based on the same key. If you do this, then any persisted values with the old simple key can still be read and all parameter values of the new `atomFamily` will default to the persisted state of the simple atom. If you change the format of the parameter in an `atomFamily`, however, it will not automatically read the previous values that were persisted before the change. However, you can add logic in a default selector to lookup values based on previous parameter formats. We hope to help automate this pattern in the future. 79 | -------------------------------------------------------------------------------- /docusaurus.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | module.exports = { 11 | title: 'Recoil 中文文档', 12 | tagline: '用于 React 的状态管理库', 13 | url: 'https://recoil.js.cn', 14 | organizationName: 'justjavac', 15 | projectName: 'recoil', 16 | baseUrl: '/', 17 | favicon: 'img/favicon.ico', 18 | themeConfig: { 19 | algolia: { 20 | apiKey: '9c5a009951e793525603922b8ca66628', 21 | indexName: 'recoiljs', 22 | }, 23 | image: 'img/og-image.png', 24 | navbar: { 25 | title: 'Recoil', 26 | items: [ 27 | { 28 | to: 'docs/introduction/installation', 29 | activeBasePath: 'docs', 30 | label: '文档', 31 | position: 'left', 32 | }, 33 | // {to: 'blog', label: 'Blog', position: 'left'}, 34 | // Please keep GitHub link to the right for consistency. 35 | { 36 | href: 'https://github.com/justjavac/recoil', 37 | label: 'GitHub', 38 | position: 'right', 39 | }, 40 | ], 41 | }, 42 | footer: { 43 | style: 'dark', 44 | links: [ 45 | { 46 | title: '学习', 47 | items: [ 48 | { 49 | label: '快速入门', 50 | to: 'docs/introduction/getting-started', 51 | }, 52 | { 53 | label: '核心概念', 54 | to: 'docs/introduction/core-concepts', 55 | }, 56 | ], 57 | }, 58 | { 59 | title: '社区', 60 | items: [ 61 | // { 62 | // label: 'Stack Overflow', 63 | // href: 'https://stackoverflow.com/questions/tagged/recoil', 64 | // }, 65 | { 66 | label: 'Twitter', 67 | href: 'https://twitter.com/recoiljs', 68 | }, 69 | // { 70 | // label: 'Discord', 71 | // href: 'https://discordapp.com/invite/docusaurus', 72 | // }, 73 | ], 74 | }, 75 | { 76 | title: '更多', 77 | items: [ 78 | // { 79 | // label: 'Blog', 80 | // to: 'blog', 81 | // }, 82 | { 83 | label: 'GitHub', 84 | href: 'https://github.com/justjavac/recoil', 85 | }, 86 | ], 87 | }, 88 | { 89 | title: '法律条款', 90 | // Please do not remove the privacy and terms, it's a legal requirement. 91 | items: [ 92 | { 93 | label: '隐私', 94 | href: 'https://opensource.facebook.com/legal/privacy/', 95 | target: '_blank', 96 | rel: 'noreferrer noopener', 97 | }, 98 | { 99 | label: '条款', 100 | href: 'https://opensource.facebook.com/legal/terms/', 101 | target: '_blank', 102 | rel: 'noreferrer noopener', 103 | }, 104 | ], 105 | }, 106 | ], 107 | logo: { 108 | alt: 'Facebook Open Source Logo', 109 | src: 'img/oss_logo.png', 110 | href: 'https://opensource.facebook.com', 111 | }, 112 | // Please do not remove the credits, help to publicize Docusaurus :) 113 | copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, 114 | }, 115 | }, 116 | presets: [ 117 | [ 118 | '@docusaurus/preset-classic', 119 | { 120 | docs: { 121 | sidebarPath: require.resolve('./sidebars.js'), 122 | editUrl: 'https://github.com/justjavac/recoil/tree/master', 123 | }, 124 | blog: { 125 | showReadingTime: true, 126 | editUrl: 'https://github.com/justjavac/recoil/tree/master/blog/', 127 | }, 128 | theme: { 129 | customCss: require.resolve('./src/css/custom.css'), 130 | }, 131 | }, 132 | ], 133 | ], 134 | }; 135 | -------------------------------------------------------------------------------- /docs/basic-tutorial/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Bonus: Performance' 3 | --- 4 | 5 | Our existing implementation is perfectly valid, but there are some important performance implications to consider as our app evolves from being a small toy project to a million-line corporate program. 6 | 7 | Let's think about what will cause each of our components to re-render: 8 | 9 | ### `` 10 | 11 | This component is subscribed to `filteredTodoListState`, which is a selector that has a dependency on `todoListState` and `todoListFilterState`. This means `TodoList` will re-render when the following state changes: 12 | 13 | - `todoListState` 14 | - `todoListFilterState` 15 | 16 | ### `` 17 | 18 | This component is subscribed to `todoListState`, so it will re-render whenever `todoListState` changes and whenever its parent component, `TodoList`, re-renders. 19 | 20 | ### `` 21 | 22 | This component is not subscribed to Recoil state (`useSetRecoilState()` does not create a subscription), so it will only re-render when its parent component, `TodoList`, re-renders. 23 | 24 | ### `` 25 | 26 | This component is subcribed to `todoListFilterState`, so it will re-render when either that state changes or when its parent component, `TodoList`, re-renders. 27 | 28 | ### `` 29 | 30 | This component is subscribed to `filteredTodoListState`, so it will re-render whenever that state changes or when its parent component, `TodoList`, re-renders. 31 | 32 | ## Room for Improvement 33 | 34 | The existing implementation has a few drawbacks, mainly that fact that we are re-rendering the entire tree whenever we make any change to `todoListState` due to the fact that `` is the parent of all of our components, so when it re-renders so will all of its children. 35 | 36 | Ideally, components would re-render only when they absolutely have to (when the data that they display on the screen has changed). 37 | 38 | ## Optimization #1: `React.memo()` 39 | 40 | To mitigate the issue of child components re-rendering unnecessarily, we can make use of [`React.memo()`](https://reactjs.org/docs/react-api.html#reactmemo), which memoizes a component based on the **props** passed to that component: 41 | 42 | ```js 43 | const TodoItem = React.memo(({item}) => ...); 44 | 45 | const TodoItemCreator = React.memo(() => ...); 46 | 47 | const TodoListFilters = React.memo(() => ...); 48 | 49 | const TodoListStats = React.memo(() => ...); 50 | ``` 51 | 52 | That helps with the re-renders of `` and `` as they no longer re-render in response to re-renders of their parent component, ``, but we still have the problem of `` and `` re-rendering when individual todo items have their text changed as text changes will result in a new `todoListFilterState`, which both `` and `` are subscribed to. 53 | 54 | ## Optimization #2: `atomFamily()` 55 | 56 | ### Rethinking State Shape 57 | 58 | Thinking of a todo list as an array of objects is problematic because it forms a tight coupling between each individual todo item and the list of all todo items. 59 | 60 | To fix this issue, we need to rethink our state shape by thinking about **normalized state**. In the context of our todo-list app, this means storing the **list** of item ids separately from the **data** for each invididual item. 61 | 62 | > For a more detailed discussion on how to think about normalized state, see [this page from the Redux documentation](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape). 63 | 64 | This ultimately means that we will be splitting our `todoListState` into two: 65 | 66 | - An array of todo item IDs 67 | - A mapping of item ID to item data 68 | 69 | The array of todo item IDs can be implemented using an atom like so: 70 | 71 | ```javascript 72 | const todoListItemIdsState = atom({ 73 | key: 'todoListItemIdsState', 74 | default: [], 75 | }); 76 | ``` 77 | 78 | For implementing a mapping of item ID to item data, Recoil provides a utility method that allows us to dynamically create a mapping from ID to atom. This utlity is [`atomFamily()`](/docs/api-reference/utils/atomFamily). 79 | 80 | ### `atomFamily()` 81 | 82 | We use the `atomFamily()` function 83 | -------------------------------------------------------------------------------- /docs/basic-tutorial/selectors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Selectors 3 | --- 4 | 5 | **选择器(selector)**表示**派生状态**的一部分 。你可以将派生状态视为将状态传递到纯函数的输出,该函数会以某种方式修改给定状态。 6 | 7 | 派生状态是一个强大的概念,因为派生状态使我们能够构建依赖于其他数据的动态数据。在我们的待办事项列表应用程序的上下文中,以下被视为派生状态: 8 | 9 | - **筛选待办事项列表**:通过创建一个新列表,筛选待办事项列表从完整的待办事项列表派生,该列表根据某些条件(如筛选出已完成的项目)筛选出某些项目。 10 | 11 | - **待办事项列表统计信息**:通过计算列表中的一些有特定作用的的属性(如列表中的项目总数、已完成项目数和已完成项目的百分比),待办事项列表统计信息从完整待办事项列表派生。 12 | 13 | 要实现筛选待办事项列表,我们需要选择一组筛选器条件,其值可以保存在 atom 中。我们将使用的筛选器选项包括:"显示全部","显示已完成"和"显示未完成"。默认值为"显示全部": 14 | 15 | ```javascript 16 | const todoListFilterState = atom({ 17 | key: 'todoListFilterState', 18 | default: 'Show All', 19 | }); 20 | ``` 21 | 22 | 使用`todoListFilterState`和`todoListState`,我们可以构建一个 "筛选的待办事项状态" selector, 它派生一个筛选的列表: 23 | 24 | ```javascript 25 | const filteredTodoListState = selector({ 26 | key: 'filteredTodoListState', 27 | get: ({get}) => { 28 | const filter = get(todoListFilterState); 29 | const list = get(todoListState); 30 | 31 | switch (filter) { 32 | case 'Show Completed': 33 | return list.filter((item) => item.isComplete); 34 | case 'Show Uncompleted': 35 | return list.filter((item) => !item.isComplete); 36 | default: 37 | return list; 38 | } 39 | }, 40 | }); 41 | ``` 42 | 43 | `filteredTodoListState`内部跟踪两个依赖项:`todoListFilterState`和`todoListState`,以便如果其中任何一个依赖项发生更改,它将重新运行(re-runs)。 44 | 45 | > 从组件的角度来看,可以使用用于读取 atom 的相同 hook 来读取 selector。但是,需要注意的是,某些 hook 仅适用于 **可写状态**(即`useRecoilState()`)。所有 atom 都是可写状态,但只有某些 selector 被视为可写状态(同时具有`get`和`set`属性的 selector)。有关本主题的详细信息,请参阅[核心概念](/docs/introduction/core-concepts)页面。 46 | 47 | 显示我们筛选的 todoList 非常简单,只需更改`TodoList`组件中的一行: 48 | 49 | ```jsx 50 | function TodoList() { 51 | // changed from todoListState to filteredTodoListState 52 | const todoList = useRecoilValue(filteredTodoListState); 53 | 54 | return ( 55 | <> 56 | 57 | 58 | 59 | 60 | {todoList.map((todoItem) => ( 61 | 62 | ))} 63 | 64 | ); 65 | } 66 | ``` 67 | 68 | 请注意,UI 与`todoListFilterState`的默认值为`"显示全部"`相同。为了更改筛选器,我们需要实现`TodoListFilters`组件: 69 | 70 | ```jsx 71 | function TodoListFilters() { 72 | const [filter, setFilter] = useRecoilState(todoListFilterState); 73 | 74 | const updateFilter = ({target: {value}}) => { 75 | setFilter(value); 76 | }; 77 | 78 | return ( 79 | <> 80 | Filter: 81 | 86 | 87 | ); 88 | } 89 | ``` 90 | 91 | 通过几行代码,我们成功实现了筛选!我们将使用相同的概念来实现`TodoListStats`组件。 92 | 93 | 我们要显示以下统计数据: 94 | 95 | - 待办事项总数 96 | - 已完成项目总数 97 | - 未完成项目总数 98 | - 已完成项目的百分比 99 | 100 | 虽然我们可以为每个统计数据创建一个 selector ,但更简单的方法是创建一个可以返回包含我们需要的数据的对象的 selector。我们把此 selector 称为`todoListStatsState`: 101 | 102 | ```javascript 103 | const todoListStatsState = selector({ 104 | key: 'todoListStatsState', 105 | get: ({get}) => { 106 | const todoList = get(filteredTodoListState); 107 | const totalNum = todoList.length; 108 | const totalCompletedNum = todoList.filter((item) => item.isComplete).length; 109 | const totalUncompletedNum = totalNum - totalCompletedNum; 110 | const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; 111 | 112 | return { 113 | totalNum, 114 | totalCompletedNum, 115 | totalUncompletedNum, 116 | percentCompleted, 117 | }; 118 | }, 119 | }); 120 | ``` 121 | 122 | 要读取`todoListStatsState`的值,我们再次使用`useRecoilValue()`: 123 | 124 | ```jsx 125 | function TodoListStats() { 126 | const { 127 | totalNum, 128 | totalCompletedNum, 129 | totalUncompletedNum, 130 | percentCompleted, 131 | } = useRecoilValue(todoListStatsState); 132 | 133 | const formattedPercentCompleted = Math.round(percentCompleted * 100); 134 | 135 | return ( 136 |
    137 |
  • Total items: {totalNum}
  • 138 |
  • Items completed: {totalCompletedNum}
  • 139 |
  • Items not completed: {totalUncompletedNum}
  • 140 |
  • Percent completed: {formattedPercentCompleted}
  • 141 |
142 | ); 143 | } 144 | ``` 145 | 146 | 综上,我们创建了一个满足我们所有要求的待办事项列表应用: 147 | 148 | - 添加待办事项 149 | - 编辑待办事项 150 | - 删除待办事项 151 | - 筛选待办事项 152 | - 显示有用的统计数据 153 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | import React from 'react'; 11 | import classnames from 'classnames'; 12 | import Layout from '@theme/Layout'; 13 | import Link from '@docusaurus/Link'; 14 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 15 | import useBaseUrl from '@docusaurus/useBaseUrl'; 16 | import styles from './styles.module.css'; 17 | 18 | const features = [ 19 | { 20 | title: <>Minimal and Reactish, 21 | // imageUrl: 'img/undraw_docusaurus_mountain.svg', 22 | description: ( 23 | <> 24 | Recoil works and thinks like React. Add some to your app and get fast 25 | and flexible shared state. 26 | 27 | ), 28 | }, 29 | { 30 | title: <>Data-Flow Graph, 31 | // imageUrl: 'img/undraw_docusaurus_tree.svg', 32 | description: ( 33 | <> 34 | Derived data and asynchronous queries are tamed with pure functions and 35 | efficient subscriptions. 36 | 37 | ), 38 | }, 39 | { 40 | title: <>Cross-App Observation, 41 | // imageUrl: 'img/undraw_docusaurus_react.svg', 42 | description: ( 43 | <> 44 | Implement persistence, routing, time-travel debugging, or undo by 45 | observing all state changes across your app, without impairing 46 | code-splitting. 47 | 48 | ), 49 | }, 50 | ]; 51 | 52 | function Feature({imageUrl, title, description}) { 53 | const imgUrl = useBaseUrl(imageUrl); 54 | return ( 55 |
56 | {imgUrl && ( 57 |
58 | {title} 59 |
60 | )} 61 |

{title}

62 |

{description}

63 |
64 | ); 65 | } 66 | 67 | function Home() { 68 | const context = useDocusaurusContext(); 69 | const {siteConfig = {}} = context; 70 | return ( 71 | 72 |
73 |
74 |

{siteConfig.title}

75 |

{siteConfig.tagline}

76 |
77 | 83 | 快速入门 84 | 85 |
86 |
87 |
88 |
89 | {features && features.length && ( 90 |
91 |
92 |
93 | {features.map(({title, imageUrl, description}) => ( 94 | 100 | ))} 101 |
102 |
103 |
104 | )} 105 |
106 |
107 |
108 |
109 |
110 |