├── .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 | [](https://github.com/dai-shi/use-atom/actions?query=workflow%3ACI)
4 | [](https://www.npmjs.com/package/use-atom)
5 | [](https://bundlephobia.com/result?p=use-atom)
6 | [](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 |
--------------------------------------------------------------------------------