├── .codesandbox └── ci.json ├── .eslintrc.json ├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── 01_basic_spec.ts ├── 02_derived_spec.tsx └── __snapshots__ │ └── 02_derived_spec.tsx.snap ├── examples ├── 01_minimal │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ └── index.js └── 02_typescript │ ├── package.json │ ├── public │ └── index.html │ └── src │ ├── App.tsx │ ├── Counter.tsx │ ├── index.ts │ └── state.ts ├── package.json ├── src ├── Provider.tsx ├── index.ts ├── useAtom.ts ├── useAtomValue.ts └── useSetAtom.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "compile", 3 | "sandboxes": ["new", "react-typescript-react-ts"], 4 | "node": "14" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint", 5 | "react-hooks" 6 | ], 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "airbnb" 10 | ], 11 | "env": { 12 | "browser": true 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "node": { 17 | "extensions": [".js", ".ts", ".tsx"] 18 | } 19 | } 20 | }, 21 | "rules": { 22 | "react-hooks/rules-of-hooks": "error", 23 | "react-hooks/exhaustive-deps": ["error", { "additionalHooks": "useIsomorphicLayoutEffect" }], 24 | "@typescript-eslint/explicit-function-return-type": "off", 25 | "@typescript-eslint/explicit-module-boundary-types": "off", 26 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }], 27 | "react/prop-types": "off", 28 | "react/jsx-one-expression-per-line": "off", 29 | "import/extensions": ["error", "never"], 30 | "import/prefer-default-export": "off", 31 | "import/no-unresolved": ["error", { "ignore": ["use-atom"] }], 32 | "symbol-description": "off", 33 | "no-use-before-define": "off", 34 | "no-unused-vars": "off", 35 | "no-redeclare": "off", 36 | "react/function-component-definition": ["error", { "namedComponents": "arrow-function" }] 37 | }, 38 | "overrides": [{ 39 | "files": ["__tests__/**/*"], 40 | "env": { 41 | "jest": true 42 | }, 43 | "rules": { 44 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 45 | } 46 | }] 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '14.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Get yarn cache 21 | id: yarn-cache 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ${{ steps.yarn-cache.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | 32 | - name: Install dependencies 33 | run: yarn install 34 | 35 | - name: Test 36 | run: yarn test 37 | 38 | - name: Compile 39 | run: yarn run compile 40 | 41 | - name: Publish 42 | run: npm publish 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: '14.x' 17 | 18 | - name: Get yarn cache 19 | id: yarn-cache 20 | run: echo "::set-output name=dir::$(yarn cache dir)" 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v1 24 | with: 25 | path: ${{ steps.yarn-cache.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - name: Install dependencies 31 | run: yarn install 32 | 33 | - name: Test 34 | run: yarn test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | node_modules 4 | /dist 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.9.0] - 2022-12-08 6 | ### Changed 7 | - Migrate to Jotai v2 API #33 8 | - Jotai is a peer dependency 9 | 10 | ## [0.8.0] - 2022-05-03 11 | ### Changed 12 | - update dependencies and fix types #26 13 | - eliminate React.FC types 14 | 15 | ## [0.7.0] - 2022-03-26 16 | ### Changed 17 | - fix: updating atom in concurrent rendering #25 18 | 19 | ## [0.6.1] - 2021-12-23 20 | ### Changed 21 | - Fix peerDependencies 22 | 23 | ## [0.6.0] - 2021-12-23 24 | ### Added 25 | - Jotai compatible API 26 | ### Removed 27 | - Recoil compatible API 28 | 29 | ## [0.5.7] - 2020-08-18 30 | ### Changed 31 | - Fix a bug in set dependents 32 | 33 | ## [0.5.6] - 2020-07-08 34 | ### Changed 35 | - Fix the order of function overloads again 36 | 37 | ## [0.5.5] - 2020-07-08 38 | ### Changed 39 | - Fix the order of function overloads again (incomplete) 40 | 41 | ## [0.5.4] - 2020-07-02 42 | ### Changed 43 | - Modern build 44 | 45 | ## [0.5.3] - 2020-06-25 46 | ### Changed 47 | - Fix the order of function overloads 48 | 49 | ## [0.5.2] - 2020-06-20 50 | ### Changed 51 | - deriveAtom has non-null default with sync get 52 | 53 | ## [0.5.0] - 2020-06-18 54 | ### Changed 55 | - Fix initializing atom on demand 56 | 57 | ## [0.4.0] - 2020-06-07 58 | ### Changed 59 | - Improve logic (With some limitations in async) 60 | 61 | ## [0.3.0] - 2020-05-30 62 | ### Changed 63 | - Improve logic (WIP) 64 | 65 | ## [0.2.0] - 2020-05-30 66 | ### Changed 67 | - Improve logic 68 | 69 | ## [0.1.0] - 2020-05-29 70 | ### Added 71 | - Initial release 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 Daishi Kato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-atom 2 | 3 | [![CI](https://img.shields.io/github/actions/workflow/status/dai-shi/use-atom/ci.yml?branch=main)](https://github.com/dai-shi/use-atom/actions?query=workflow%3ACI) 4 | [![npm](https://img.shields.io/npm/v/use-atom)](https://www.npmjs.com/package/use-atom) 5 | [![size](https://img.shields.io/bundlephobia/minzip/use-atom)](https://bundlephobia.com/result?p=use-atom) 6 | [![discord](https://img.shields.io/discord/627656437971288081)](https://discord.gg/MrQdmzd) 7 | 8 | Yet another implementation for Jotai atoms without side effects 9 | 10 | ## Introduction 11 | 12 | This library used to be a former library to [jotai](https://github.com/pmndrs/jotai). 13 | It's now rewritten to follow jotai API and depends on 14 | [use-context-selector](https://github.com/dai-shi/use-context-selector). 15 | The biggest difference is that side effects in `write` is not allowed. 16 | 17 | ## Install 18 | 19 | ```bash 20 | npm install use-atom jotai 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```javascript 26 | import React from 'react'; 27 | import ReactDOM from 'react-dom'; 28 | 29 | import { Provider, atom, useAtom } from 'use-atom'; 30 | 31 | const countAtom = atom(0); 32 | const textAtom = atom('hello'); 33 | 34 | const Counter = () => { 35 | const [count, setCount] = useAtom(countAtom); 36 | return ( 37 |
38 | {Math.random()} 39 |
40 | Count: {count} 41 | 42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | const TextBox = () => { 49 | const [text, setText] = useAtom(textAtom); 50 | return ( 51 |
52 | {Math.random()} 53 |
54 | Text: {text} 55 | setText(event.target.value)} /> 56 |
57 |
58 | ); 59 | }; 60 | 61 | const App = () => ( 62 | 63 |

Counter

64 | 65 | 66 |

TextBox

67 | 68 | 69 |
70 | ); 71 | ``` 72 | 73 | ## Examples 74 | 75 | The [examples](examples) folder contains working examples. 76 | You can run one of them with 77 | 78 | ```bash 79 | PORT=8080 npm run examples:01_minimal 80 | ``` 81 | 82 | and open in your web browser. 83 | 84 | You can also try them in codesandbox.io: 85 | [01](https://codesandbox.io/s/github/dai-shi/use-atom/tree/main/examples/01_minimal) 86 | [02](https://codesandbox.io/s/github/dai-shi/use-atom/tree/main/examples/02_typescript) 87 | -------------------------------------------------------------------------------- /__tests__/01_basic_spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Provider, 3 | atom, 4 | useAtom, 5 | useAtomValue, 6 | useSetAtom, 7 | } from '../src/index'; 8 | 9 | describe('basic spec', () => { 10 | it('exported function', () => { 11 | expect(Provider).toBeDefined(); 12 | expect(atom).toBeDefined(); 13 | expect(useAtom).toBeDefined(); 14 | expect(useAtomValue).toBeDefined(); 15 | expect(useSetAtom).toBeDefined(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/02_derived_spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, StrictMode } from 'react'; 2 | import { render, fireEvent, cleanup } from '@testing-library/react'; 3 | 4 | import { Provider, atom, useAtom } from '../src/index'; 5 | 6 | describe('derived spec', () => { 7 | afterEach(cleanup); 8 | 9 | const initialState = { 10 | count: 0, 11 | dummy: 0, 12 | }; 13 | 14 | type Action = { type: 'setCount'; value: number } | { type?: undefined }; 15 | const reducer = (state = initialState, action: Action = {}) => { 16 | switch (action.type) { 17 | case 'setCount': 18 | return { 19 | ...state, 20 | count: action.value, 21 | }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | it('counter', () => { 28 | const globalAtom = atom(initialState); 29 | 30 | const countAtom = atom( 31 | (get) => get(globalAtom).count, 32 | (get, set, update: (prev: number) => number) => { 33 | set(globalAtom, reducer(get(globalAtom), { 34 | type: 'setCount', 35 | value: update(get(globalAtom).count), 36 | })); 37 | }, 38 | ); 39 | 40 | const Counter = () => { 41 | const [count, setCount] = useAtom(countAtom); 42 | const increment = () => setCount((c) => c + 1); 43 | const commitCount = useRef(0); 44 | useEffect(() => { 45 | commitCount.current += 1; 46 | }); 47 | return ( 48 |
49 | {count} 50 | 51 | {commitCount.current} 52 |
53 | ); 54 | }; 55 | 56 | const App = () => ( 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | 64 | const { getAllByText, container } = render(); 65 | expect(container).toMatchSnapshot(); 66 | fireEvent.click(getAllByText('+1')[0]); 67 | expect(container).toMatchSnapshot(); 68 | }); 69 | 70 | it('double counter', () => { 71 | const countAtom = atom(0); 72 | const doubledAtom = atom((get) => get(countAtom) * 2); 73 | 74 | const Counter = () => { 75 | const [count, setCount] = useAtom(countAtom); 76 | const increment = () => setCount((c) => c + 1); 77 | return ( 78 |
79 | count: {count} 80 | 81 |
82 | ); 83 | }; 84 | 85 | const Doubled = () => { 86 | const [doubled] = useAtom(doubledAtom); 87 | return doubled: {doubled}; 88 | }; 89 | 90 | const App = () => ( 91 | 92 | 93 | 94 | 95 | 96 | 97 | ); 98 | 99 | const { getAllByText, container } = render(); 100 | expect(container).toMatchSnapshot(); 101 | fireEvent.click(getAllByText('+1')[0]); 102 | expect(container).toMatchSnapshot(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/02_derived_spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`derived spec counter 1`] = ` 4 |
5 |
6 | 7 | 0 8 | 9 | 14 | 15 | 4 16 | 17 |
18 |
19 | `; 20 | 21 | exports[`derived spec counter 2`] = ` 22 |
23 |
24 | 25 | 1 26 | 27 | 32 | 33 | 5 34 | 35 |
36 |
37 | `; 38 | 39 | exports[`derived spec double counter 1`] = ` 40 |
41 |
42 | 43 | count: 44 | 0 45 | 46 | 51 |
52 | 53 | doubled: 54 | 0 55 | 56 |
57 | `; 58 | 59 | exports[`derived spec double counter 2`] = ` 60 |
61 |
62 | 63 | count: 64 | 1 65 | 66 | 71 |
72 | 73 | doubled: 74 | 2 75 | 76 |
77 | `; 78 | -------------------------------------------------------------------------------- /examples/01_minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-atom-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "jotai": "latest", 7 | "react": "latest", 8 | "react-dom": "latest", 9 | "react-scripts": "latest", 10 | "use-atom": "latest" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "browserslist": [ 19 | ">0.2%", 20 | "not dead", 21 | "not ie <= 11", 22 | "not op_mini all" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/01_minimal/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | use-atom example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/01_minimal/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { Provider, atom, useAtom } from 'use-atom'; 5 | 6 | const countAtom = atom(0); 7 | const textAtom = atom('hello'); 8 | 9 | const Counter = () => { 10 | const [count, setCount] = useAtom(countAtom); 11 | return ( 12 |
13 | {Math.random()} 14 |
15 | Count: {count} 16 | 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | const TextBox = () => { 24 | const [text, setText] = useAtom(textAtom); 25 | return ( 26 |
27 | {Math.random()} 28 |
29 | Text: {text} 30 | setText(event.target.value)} /> 31 |
32 |
33 | ); 34 | }; 35 | 36 | const App = () => ( 37 | 38 |

Counter

39 | 40 | 41 |

TextBox

42 | 43 | 44 |
45 | ); 46 | 47 | createRoot(document.getElementById('app')).render(); 48 | -------------------------------------------------------------------------------- /examples/02_typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-atom-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "jotai": "latest", 9 | "react": "latest", 10 | "react-dom": "latest", 11 | "react-scripts": "latest", 12 | "use-atom": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/02_typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | use-atom example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/02_typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Provider } from 'use-atom'; 4 | 5 | import Counter from './Counter'; 6 | 7 | const App = () => ( 8 | 9 |

Counter

10 | 11 |
12 | 13 |
14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /examples/02_typescript/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useAtom } from 'use-atom'; 4 | 5 | import { counts, total } from './state'; 6 | 7 | const Item = ({ count }: { 8 | count: (typeof counts)[number]; 9 | }) => { 10 | const [value, setValue] = useAtom(count); 11 | return ( 12 |
  • 13 | Count: {value} 14 | 15 | {Math.random()} 16 |
  • 17 | ); 18 | }; 19 | 20 | const Total = () => { 21 | const [value] = useAtom(total); 22 | return ( 23 |
    24 | Total: {value} 25 |
    {Math.random()}
    26 |
    27 | ); 28 | }; 29 | 30 | const Counter = () => ( 31 |
    32 | {Math.random()} 33 |
      34 | {counts.map((count) => ( 35 | 36 | ))} 37 |
    38 | 39 |
    40 | ); 41 | 42 | export default Counter; 43 | -------------------------------------------------------------------------------- /examples/02_typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line spaced-comment 2 | /// 3 | 4 | import React from 'react'; 5 | import { createRoot } from 'react-dom/client'; 6 | 7 | import App from './App'; 8 | 9 | const ele = document.getElementById('app'); 10 | if (!ele) throw new Error('no app'); 11 | createRoot(ele).render(React.createElement(App)); 12 | -------------------------------------------------------------------------------- /examples/02_typescript/src/state.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'use-atom'; 2 | 3 | export const counts = [ 4 | atom(0), 5 | atom(0), 6 | atom(0), 7 | atom(0), 8 | atom(0), 9 | ]; 10 | 11 | export const total = atom((get) => counts.reduce((p, c) => p + get(c), 0)); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-atom", 3 | "description": "Yet another implementation for Jotai atoms without side effects", 4 | "version": "0.9.0", 5 | "author": "Daishi Kato", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dai-shi/use-atom.git" 9 | }, 10 | "source": "./src/index.ts", 11 | "main": "./dist/index.umd.js", 12 | "module": "./dist/index.modern.js", 13 | "types": "./dist/src/index.d.ts", 14 | "exports": { 15 | "./package.json": "./package.json", 16 | ".": { 17 | "types": "./dist/src/index.d.ts", 18 | "module": "./dist/index.modern.js", 19 | "import": "./dist/index.modern.mjs", 20 | "default": "./dist/index.umd.js" 21 | } 22 | }, 23 | "sideEffects": false, 24 | "files": [ 25 | "src", 26 | "dist" 27 | ], 28 | "scripts": { 29 | "compile": "microbundle build -f modern,umd --jsx React.createElement --globals react=React", 30 | "postcompile": "cp dist/index.modern.mjs dist/index.modern.js && cp dist/index.modern.mjs.map dist/index.modern.js.map", 31 | "test": "run-s eslint tsc-test jest", 32 | "eslint": "eslint --ext .js,.ts,.tsx --ignore-pattern dist .", 33 | "jest": "jest", 34 | "tsc-test": "tsc --project . --noEmit", 35 | "apidoc": "documentation readme --section API --markdown-toc false --parse-extension ts src/*.ts", 36 | "examples:01_minimal": "DIR=01_minimal EXT=js webpack serve", 37 | "examples:02_typescript": "DIR=02_typescript webpack serve" 38 | }, 39 | "jest": { 40 | "testEnvironment": "jsdom", 41 | "preset": "ts-jest/presets/js-with-ts" 42 | }, 43 | "keywords": [ 44 | "react", 45 | "state", 46 | "recoil" 47 | ], 48 | "license": "MIT", 49 | "dependencies": { 50 | "use-context-selector": "1.4.1" 51 | }, 52 | "devDependencies": { 53 | "@testing-library/react": "^13.4.0", 54 | "@types/jest": "^29.2.4", 55 | "@types/react": "^18.0.26", 56 | "@types/react-dom": "^18.0.9", 57 | "@typescript-eslint/eslint-plugin": "^5.45.1", 58 | "@typescript-eslint/parser": "^5.45.1", 59 | "documentation": "^14.0.0", 60 | "eslint": "^8.29.0", 61 | "eslint-config-airbnb": "^19.0.4", 62 | "eslint-plugin-import": "^2.26.0", 63 | "eslint-plugin-jsx-a11y": "^6.6.1", 64 | "eslint-plugin-react": "^7.31.11", 65 | "eslint-plugin-react-hooks": "^4.6.0", 66 | "html-webpack-plugin": "^5.5.0", 67 | "jest": "^29.3.1", 68 | "jest-environment-jsdom": "^29.3.1", 69 | "jotai": "^1.11.1", 70 | "microbundle": "^0.15.1", 71 | "npm-run-all": "^4.1.5", 72 | "react": "^18.2.0", 73 | "react-dom": "^18.2.0", 74 | "ts-jest": "^29.0.3", 75 | "ts-loader": "^9.4.2", 76 | "typescript": "^4.9.4", 77 | "webpack": "^5.75.0", 78 | "webpack-cli": "^5.0.1", 79 | "webpack-dev-server": "^4.11.1" 80 | }, 81 | "peerDependencies": { 82 | "jotai": ">=1.11.1", 83 | "react": ">=16.8.0", 84 | "react-dom": "*", 85 | "react-native": "*", 86 | "scheduler": ">=0.19.0" 87 | }, 88 | "peerDependenciesMeta": { 89 | "react-dom": { 90 | "optional": true 91 | }, 92 | "react-native": { 93 | "optional": true 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Dispatch, 3 | ReactNode, 4 | useCallback, 5 | useState, 6 | } from 'react'; 7 | import { createContext } from 'use-context-selector'; 8 | import type { Atom, WritableAtom } from 'jotai/vanilla'; 9 | 10 | const warningObject = new Proxy({}, { 11 | get() { throw new Error('Please use '); }, 12 | apply() { throw new Error('Please use '); }, 13 | }); 14 | 15 | type InitAction = { 16 | type: 'INIT_ATOM'; 17 | atom: Atom; 18 | id: symbol; 19 | }; 20 | 21 | type DisposeAction = { 22 | type: 'DISPOSE_ATOM'; 23 | atom: Atom; 24 | id: symbol; 25 | }; 26 | 27 | type CommitAction = { 28 | type: 'COMMIT_ATOM'; 29 | atom: Atom; 30 | atomState: AtomState; 31 | }; 32 | 33 | type SetAction = { 34 | type: 'SET_ATOM'; 35 | atom: WritableAtom; 36 | args: unknown[]; 37 | }; 38 | 39 | type Action = InitAction | DisposeAction | CommitAction | SetAction; 40 | 41 | type AtomState = { 42 | value: Value; 43 | dependencies: Set>; 44 | dependents?: Set | symbol>; // symbol is id from INIT_ATOM 45 | }; 46 | 47 | type State = Map, AtomState>; // immutable map 48 | 49 | export const getAtomState = (state: State, atom: Atom) => { 50 | const atomState = state.get(atom) as AtomState | undefined; 51 | if (atomState) return atomState; 52 | const dependencies = new Set>(); 53 | const options = { 54 | get signal(): AbortSignal { 55 | throw new Error('signal is not supported'); 56 | }, 57 | get setSelf(): never { 58 | throw new Error('setSelf is not supported'); 59 | }, 60 | }; 61 | const value = atom.read((a: Atom) => { 62 | if (a !== atom as unknown as Atom) { 63 | dependencies.add(a); 64 | const aState = getAtomState(state, a); 65 | return aState.value; 66 | } 67 | if ('init' in a) return a.init as V; 68 | throw new Error('no atom init'); 69 | }, options); 70 | const newAtomState: AtomState = { value, dependencies }; 71 | return newAtomState; 72 | }; 73 | 74 | const initAtom = ( 75 | prevState: State, 76 | atom: Atom, 77 | dependent: symbol, 78 | ) => { 79 | const atomState = prevState.get(atom); 80 | if (!atomState) { 81 | throw new Error('no atom state found to initialize'); 82 | } 83 | const nextAtomState = { 84 | ...atomState, 85 | dependents: new Set(atomState.dependents).add(dependent), 86 | }; 87 | return new Map(prevState).set(atom, nextAtomState); 88 | }; 89 | 90 | const disposeAtom = ( 91 | prevState: State, 92 | dependent: Atom | symbol, 93 | ) => { 94 | let nextState = new Map(prevState); 95 | const deleted: Atom[] = []; 96 | nextState.forEach((atomState, atom) => { 97 | if (atomState.dependents?.has(dependent)) { 98 | const nextDependents = new Set(atomState.dependents); 99 | nextDependents.delete(dependent); 100 | if (nextDependents.size) { 101 | nextState.set(atom, { 102 | ...atomState, 103 | dependents: nextDependents, 104 | }); 105 | } else { 106 | nextState.delete(atom); 107 | deleted.push(atom); 108 | } 109 | } 110 | }); 111 | nextState = deleted.reduce((p, c) => disposeAtom(p, c), nextState); 112 | return nextState; 113 | }; 114 | 115 | const computeDependents = ( 116 | prevState: State, 117 | atom: Atom, 118 | atomState: AtomState, 119 | ) => { 120 | const prevAtomState = prevState.get(atom); 121 | let nextState = new Map(prevState); 122 | if (prevAtomState?.dependents && !atomState.dependents) { 123 | // eslint-disable-next-line no-param-reassign 124 | atomState.dependents = prevAtomState.dependents; // copy dependents 125 | } 126 | nextState.set(atom, atomState); 127 | const deleted: Atom[] = []; 128 | const prevDependencies = new Set(prevAtomState?.dependencies); 129 | atomState.dependencies.forEach((dependency) => { 130 | if (prevDependencies.has(dependency)) { 131 | prevDependencies.delete(dependency); 132 | } else { 133 | const dependencyState = getAtomState(nextState, dependency); 134 | const nextDependencyState: AtomState = { 135 | ...dependencyState, 136 | dependents: new Set(dependencyState.dependents).add(atom), 137 | }; 138 | nextState.set(dependency, nextDependencyState); 139 | } 140 | }); 141 | prevDependencies.forEach((deletedDependency) => { 142 | const dependencyState = getAtomState(nextState, deletedDependency); 143 | const dependents = new Set(dependencyState.dependents); 144 | dependents.delete(atom); 145 | if (dependents.size) { 146 | const nextDependencyState: AtomState = { 147 | ...dependencyState, 148 | dependents, 149 | }; 150 | nextState.set(deletedDependency, nextDependencyState); 151 | } else { 152 | deleted.push(deletedDependency); 153 | } 154 | }); 155 | nextState = deleted.reduce((p, c) => disposeAtom(p, c), nextState); 156 | return nextState; 157 | }; 158 | 159 | const commitAtom = ( 160 | prevState: State, 161 | atom: Atom, 162 | atomState: AtomState, 163 | ) => { 164 | if (atomState.dependents) { 165 | return prevState; 166 | } 167 | return computeDependents(prevState, atom, atomState); 168 | }; 169 | 170 | const setAtom = ( 171 | prevState: State, 172 | updatingAtom: WritableAtom, 173 | args: Args, 174 | ) => { 175 | let nextState = new Map(prevState); 176 | 177 | const updateDependents = (atom: Atom) => { 178 | const atomState = nextState.get(atom); 179 | if (!atomState) return; 180 | atomState.dependents?.forEach((dependent) => { 181 | if (typeof dependent === 'symbol') return; 182 | const prevDependentState = nextState.get(dependent); 183 | nextState.delete(dependent); // clear to re-evaluate 184 | const dependentState = getAtomState(nextState, dependent); 185 | if (prevDependentState) { 186 | nextState.set(dependent, prevDependentState); // restore it 187 | } 188 | nextState = computeDependents(nextState, dependent, dependentState); 189 | updateDependents(dependent); 190 | }); 191 | }; 192 | 193 | const updateAtomValue = ( 194 | atom: WritableAtom, 195 | ...ags: Ags 196 | ): Res => atom.write( 197 | (a: Atom) => getAtomState(nextState, a).value, 198 | (a: WritableAtom, ...as: As) => { 199 | if ((a as unknown) === atom) { 200 | const atomState = nextState.get(atom); 201 | const nextAtomState: AtomState = { 202 | dependencies: new Set(), 203 | dependents: new Set(), 204 | ...atomState, 205 | value: as[0], 206 | }; 207 | nextState = computeDependents(nextState, atom, nextAtomState); 208 | updateDependents(atom); 209 | return undefined as R; 210 | } 211 | return updateAtomValue(a as WritableAtom, ...as) as R; 212 | }, 213 | ...ags, 214 | ); 215 | 216 | updateAtomValue(updatingAtom as WritableAtom, ...args); 217 | return nextState; 218 | }; 219 | 220 | export const DispatchContext = createContext(warningObject as Dispatch); 221 | export const StateContext = createContext(warningObject as State); 222 | 223 | export const Provider = ({ children }: { children: ReactNode }) => { 224 | const [state, setState] = useState(() => new Map()); 225 | const dispatch = useCallback((action: Action) => setState((prevState) => { 226 | if (action.type === 'INIT_ATOM') { 227 | return initAtom(prevState, action.atom, action.id); 228 | } 229 | if (action.type === 'DISPOSE_ATOM') { 230 | return disposeAtom(prevState, action.id); 231 | } 232 | if (action.type === 'COMMIT_ATOM') { 233 | return commitAtom(prevState, action.atom, action.atomState); 234 | } 235 | if (action.type === 'SET_ATOM') { 236 | return setAtom(prevState, action.atom, action.args); 237 | } 238 | throw new Error('unexpected action type'); 239 | }), []); 240 | return ( 241 | 242 | 243 | {children} 244 | 245 | 246 | ); 247 | }; 248 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { atom } from 'jotai/vanilla'; 2 | export { Provider } from './Provider'; 3 | export { useAtomValue } from './useAtomValue'; 4 | export { useSetAtom } from './useSetAtom'; 5 | export { useAtom } from './useAtom'; 6 | -------------------------------------------------------------------------------- /src/useAtom.ts: -------------------------------------------------------------------------------- 1 | import type { Atom, WritableAtom } from 'jotai/vanilla'; 2 | import { useAtomValue } from './useAtomValue'; 3 | import { useSetAtom } from './useSetAtom'; 4 | 5 | export function useAtom( 6 | atom: WritableAtom, 7 | ): [Value, (...args: Args) => void] 8 | 9 | export function useAtom(atom: Atom): [Value, never] 10 | 11 | export function useAtom( 12 | atom: Atom | WritableAtom, 13 | ) { 14 | return [useAtomValue(atom), useSetAtom(atom as WritableAtom)]; 15 | } 16 | -------------------------------------------------------------------------------- /src/useAtomValue.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { useContext, useContextSelector } from 'use-context-selector'; 3 | import type { Atom } from 'jotai/vanilla'; 4 | import { StateContext, DispatchContext, getAtomState } from './Provider'; 5 | 6 | export function useAtomValue(atom: Atom) { 7 | const dispatch = useContext(DispatchContext); 8 | const atomState = useContextSelector( 9 | StateContext, 10 | useCallback((state) => getAtomState(state, atom), [atom]), 11 | ); 12 | useEffect(() => { 13 | if (!atomState.dependents) { 14 | dispatch({ type: 'COMMIT_ATOM', atom, atomState }); 15 | } 16 | }); 17 | useEffect(() => { 18 | const id = Symbol(); 19 | dispatch({ type: 'INIT_ATOM', atom, id }); 20 | return () => { 21 | dispatch({ type: 'DISPOSE_ATOM', atom, id }); 22 | }; 23 | }, [dispatch, atom]); 24 | return atomState.value; 25 | } 26 | -------------------------------------------------------------------------------- /src/useSetAtom.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useContext, useContextUpdate } from 'use-context-selector'; 3 | import type { Atom, WritableAtom } from 'jotai/vanilla'; 4 | import { StateContext, DispatchContext } from './Provider'; 5 | 6 | const isWritable = ( 7 | atom: Atom | WritableAtom, 8 | ): atom is WritableAtom => !!(atom as WritableAtom).write; 9 | 10 | export function useSetAtom( 11 | atom: WritableAtom, 12 | ) { 13 | const dispatch = useContext(DispatchContext); 14 | const updateState = useContextUpdate(StateContext); 15 | const setAtom = useCallback((...args: Args) => { 16 | if (isWritable(atom)) { 17 | updateState(() => { 18 | dispatch({ 19 | type: 'SET_ATOM', 20 | atom: atom as WritableAtom, 21 | args, 22 | }); 23 | }); 24 | } else { 25 | throw new Error('not writable atom'); 26 | } 27 | }, [atom, dispatch, updateState]); 28 | return setAtom; 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es5", 5 | "esModuleInterop": true, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "use-atom": ["./src"] 16 | }, 17 | "outDir": "./dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const { DIR, EXT = 'ts' } = process.env; 5 | 6 | module.exports = { 7 | mode: 'development', 8 | devtool: 'cheap-module-source-map', 9 | entry: `./examples/${DIR}/src/index.${EXT}`, 10 | output: { 11 | publicPath: '/', 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: `./examples/${DIR}/public/index.html`, 16 | }), 17 | ], 18 | module: { 19 | rules: [{ 20 | test: /\.[jt]sx?$/, 21 | exclude: /node_modules/, 22 | loader: 'ts-loader', 23 | options: { 24 | transpileOnly: true, 25 | }, 26 | }], 27 | }, 28 | resolve: { 29 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 30 | alias: { 31 | 'use-atom': `${__dirname}/src`, 32 | }, 33 | }, 34 | devServer: { 35 | port: process.env.PORT || '8080', 36 | static: { 37 | directory: `./examples/${DIR}/public`, 38 | }, 39 | historyApiFallback: true, 40 | }, 41 | }; 42 | --------------------------------------------------------------------------------