├── .eslintrc.js
├── .github
└── FUNDING.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── diagram.png
├── integrations
│ ├── gear.png
│ ├── js.png
│ ├── react.ico
│ ├── redux.svg
│ ├── robot.png
│ ├── svelte.png
│ └── vue.png
├── logo-base.svg
├── logo-full.svg
├── logo-plain.svg
├── logo-text.svg
└── logo.svg
├── docs
├── advanced-concepts.md
├── examples.md
├── framework-integrations
│ ├── indtroduction.md
│ ├── use-atom.md
│ └── use-setup.md
├── introduction.md
├── performance-optimizations.md
├── quick-tutorial.md
├── recipes-react
│ ├── creating-react-custom-hooks.md
│ ├── dynamic-functions-with-fixed-references.md
│ ├── grabbing-refs.md
│ ├── refactoring-react-classes.md
│ └── using-context-correctly.md
├── recipes
│ ├── finite-state-machines.md
│ ├── nested-state.md
│ ├── persist-localstorage.md
│ ├── redux-devtools-integration.md
│ ├── redux-interop.md
│ ├── using-immer.md
│ └── using-reducers.md
└── streams.md
├── examples
├── celcius-fahrenheit
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ └── index.tsx
├── counter
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── index.tsx
│ │ └── styles.css
├── dots-and-arrows
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── components
│ │ ├── Arrow.tsx
│ │ ├── Dot.tsx
│ │ ├── ModulateButton.tsx
│ │ └── TemporaryArrow.tsx
│ │ ├── index.tsx
│ │ ├── models.tsx
│ │ └── styles.css
├── finite-state-stopwatch
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ └── index.tsx
├── framework-agnostic-library
│ ├── core.tsx
│ ├── react.tsx
│ └── vue.tsx
├── redux-devtools-extension
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── index.tsx
│ │ └── styles.css
├── todos-basic
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ └── index.tsx
├── todos-filtered
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ └── index.tsx
├── transient-update-resize-observer
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ └── index.tsx
├── use-items-abstraction
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.js
│ │ ├── Counter.js
│ │ ├── index.js
│ │ └── useItems.js
└── xoid-vs-usereducer-vs-usemethods
│ ├── README.md
│ ├── package.json
│ ├── public
│ └── index.html
│ └── src
│ ├── Counter.js
│ ├── UseMethodsCounters.js
│ ├── UseReducerCounters.js
│ ├── XoidCounters.js
│ ├── index.js
│ └── styles.css
├── jest.config.js
├── package.json
├── packages
├── deprecated
│ ├── lite
│ │ ├── README.md
│ │ ├── package.json
│ │ └── src
│ │ │ └── index.tsx
│ ├── model
│ │ ├── README.md
│ │ ├── package.json
│ │ └── src
│ │ │ └── index.tsx
│ └── tree
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ └── utils.tsx
├── devtools
│ ├── README.md
│ ├── copy
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── package.json
│ └── src
│ │ ├── devtools.tsx
│ │ └── utils.tsx
├── incubator
│ ├── atom-with-location
│ │ └── index.tsx
│ ├── dnd
│ │ └── index.tsx
│ ├── plugins-draft
│ │ └── plugins.ts
│ ├── produce
│ │ ├── README.md
│ │ ├── package.json
│ │ └── src
│ │ │ └── index.tsx
│ ├── react-extras
│ │ ├── README.md
│ │ ├── package.json
│ │ └── src
│ │ │ ├── index.tsx
│ │ │ └── slice.tsx
│ └── resize-observer
│ │ └── index.tsx
├── react
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── index.tsx
│ │ ├── useAdapter.tsx
│ │ ├── useAtom.tsx
│ │ └── useConstant.tsx
├── reactive
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.tsx
├── svelte
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── index.tsx
│ │ ├── useAdapter.tsx
│ │ └── useAtom.tsx
├── vue
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── index.tsx
│ │ ├── useAdapter.tsx
│ │ └── useAtom.tsx
└── xoid
│ ├── package.json
│ └── src
│ ├── atom.tsx
│ ├── index.tsx
│ ├── internal
│ ├── createEvent.tsx
│ ├── createFocus.tsx
│ ├── createSelector.tsx
│ ├── createStream.tsx
│ ├── types.tsx
│ └── utils.tsx
│ └── setup.tsx
├── rollup.config.js
├── tests
├── __snapshots__
│ ├── actions.test.tsx.snap
│ ├── basic.test.tsx.snap
│ ├── derived-atoms.test.tsx.snap
│ ├── lazy-evaluation.test.tsx.snap
│ ├── react.test.tsx.snap
│ └── reactive.test.tsx.snap
├── actions.test.tsx
├── basic.test.tsx
├── derived-atoms.test.tsx
├── devtools.test.tsx
├── enhanced-atoms.test.tsx
├── focus.test.tsx
├── isomorphism
│ ├── CounterReact.tsx
│ ├── CounterSetup.tsx
│ ├── CounterVue.tsx
│ ├── dependency-injection.test.tsx
│ └── setup-only.test.tsx
├── lazy-evaluation.test.tsx
├── manual-testing
│ └── src
│ │ ├── App.tsx
│ │ ├── index.html
│ │ └── index.tsx
├── proxy.test.tsx
├── react.test.tsx
├── reactive.test.tsx
├── stream.test.tsx
├── subscribe.test.tsx
└── testHelpers.tsx
├── tsconfig.build.json
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "react-app",
4 | "plugin:prettier/recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | ],
7 | plugins: [],
8 | rules: {
9 | "import/no-extraneous-dependencies": ["error"],
10 | "@typescript-eslint/explicit-function-return-type": ["off"],
11 | "@typescript-eslint/explicit-module-boundary-types": ["off"],
12 | "@typescript-eslint/member-delimiter-style": ["off"],
13 | '@typescript-eslint/no-explicit-any': ['off'],
14 | '@typescript-eslint/ban-ts-ignore': ['off'],
15 | '@typescript-eslint/ban-ts-comment': ['off'],
16 | '@typescript-eslint/ban-types': ['off'],
17 | "prettier/prettier": [
18 | "warn",
19 | { singleQuote: true, semi: false, printWidth: 100, endOfLine: 'auto' }
20 | ],
21 | '@typescript-eslint/no-extra-semi': ['off'],
22 | "spaced-comment": ["error", "always", { "markers": ["/"] }]
23 | },
24 | settings: {},
25 | ignorePatterns: ["*.js", "dist/**"],
26 | };
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: xoid
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | dist/
4 | development/
5 | .backup/
6 | notes.md
7 | Thumbs.db
8 | ehthumbs.db
9 | Desktop.ini
10 | $RECYCLE.BIN/
11 | .DS_Store
12 | .vscode
13 | .docz/
14 | package-lock.json
15 | coverage/
16 | .idea
17 | .rpt2_cache/
18 | .docusaurus
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Removed
11 | - `create` (both as a named export and the default export) is removed. Instead use `atom`.
12 |
13 |
14 | ## [1.0.0beta-12] - 2015-02-16
15 |
16 | This version, compared to the previous one only adds deprecation notices that can be fixed by slight modifications. Underlying implementations are not changed.
17 |
18 | ### Deprecated
19 |
20 | - `create` (both as a named export and the default export) is marked as deprecated, and it has been renamed to `atom`.
21 | > Before:
22 | > ```js
23 | > import create from 'xoid'
24 | >// or
25 | > import { create } from 'xoid'
26 | > ```
27 | > After:
28 | > ```js
29 | > import { atom } from 'xoid'
30 | > ```
31 | - `inject` and `effect` exports are now exported from the root. They used to be exported from the `xoid/setup` route.
32 | > Before:
33 | > ```js
34 | > import { effect, inject } from 'xoid/setup'
35 | > ```
36 | > After:
37 | > ```js
38 | > import { effect, inject } from 'xoid'
39 | > ```
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Onur Kerimov
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/assets/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/diagram.png
--------------------------------------------------------------------------------
/assets/integrations/gear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/integrations/gear.png
--------------------------------------------------------------------------------
/assets/integrations/js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/integrations/js.png
--------------------------------------------------------------------------------
/assets/integrations/react.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/integrations/react.ico
--------------------------------------------------------------------------------
/assets/integrations/redux.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/integrations/robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/integrations/robot.png
--------------------------------------------------------------------------------
/assets/integrations/svelte.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/integrations/svelte.png
--------------------------------------------------------------------------------
/assets/integrations/vue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xoidlabs/xoid/d4679b6e96d49f4777757766281962df50a7cccf/assets/integrations/vue.png
--------------------------------------------------------------------------------
/assets/logo-base.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/logo-plain.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/logo-text.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/advanced-concepts.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: advanced-concepts
3 | title: Advanced concepts
4 | ---
5 |
6 | ## Deriving state from external sources
7 |
8 | With an additional overload of the `get` function, you can consume external (non-**xoid**) sources. This can be a Redux store, an RxJS observable, or anything that implements getState & subscribe pair. Here is an atom that derives its state from a Redux store:
9 |
10 | ```js
11 | import store from './reduxStore'
12 |
13 | const $derivedAtom = atom((get) => get(store.getState, store.subscribe))
14 | ```
15 | As long as the external source implements a getState & subscribe, pair, it can be consumed by **xoid**.
16 |
17 | ## Enhanced atoms
18 |
19 | An enhanced atom is an atom whose default `.set` method is swapped with something else. This technique can be used to create "pass through atoms" that act as a mediators. Most people using **xoid** will not need to write enhanced atoms.
20 | This naming is inspired by Redux's concept of enhancers. For a real-life scenario, see [Using in an existing Redux App](recipes/redux-interop).
21 |
22 | ```js
23 | import store from './reduxStore'
24 |
25 | const $mediator = atom((get) => get(store.getState, store.subscribe))
26 |
27 | // we swap the default`.set` method
28 | $mediator.set = (value: number) => store.dispatch({ type: 'ACTION', payload: value })
29 |
30 | $mediator.update(s => s + 1) // modifications to `$mediator` will be directly forwarded to Redux dispatch.
31 | ```
32 | > Swapping `.set` also modifies the behavior of `.update`, because it uses `.set` internally. This is an intentional feature.
33 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | ---
4 |
5 | - [Counter](https://github.com/xoidlabs/xoid/blob/master/examples/counter) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/counter)
6 |
7 | - [Todos (Basic)](https://github.com/xoidlabs/xoid/blob/master/examples/todos-basic) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/todos-basic)
8 |
9 | - [Todos (Filtered)](https://github.com/xoidlabs/xoid/blob/master/examples/todos-filtered) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/todos-filtered)
10 |
11 | - [Celcius-Fahrenheit conversion](https://github.com/xoidlabs/xoid/blob/master/examples/celcius-fahrenheit) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/celcius-fahrenheit)
12 |
13 | - [Finite state stopwatch](https://github.com/xoidlabs/xoid/blob/master/examples/finite-state-stopwatch) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/finite-state-stopwatch)
14 |
15 | - [Dots and arrows](https://githubbox.com/xoidlabs/xoid/tree/master/examples/dots-and-arrows) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/dots-and-arrows)
16 |
17 | - [Transient update resize observer](https://github.com/xoidlabs/xoid/blob/master/examples/transient-update-resize-observer) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/transient-update-resize-observer)
18 |
19 | - [xoid vs useReducer vs useMethods](https://github.com/xoidlabs/xoid/tree/master/examples/xoid-vs-usereducer-vs-usemethods) [](https://githubbox.com/xoidlabs/xoid/tree/master/examples/xoid-vs-usereducer-vs-usemethods)
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/framework-integrations/indtroduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: introduction
3 | title: Introduction
4 | ---
5 |
6 | **xoid** provides an isomorphic API for **React**, **Vue**, and **Svelte**. If you're using **xoid** with one of these frameworks, simply install one of the following packages:
7 |
8 | import Tabs from '@theme/Tabs';
9 | import TabItem from '@theme/TabItem';
10 |
11 |
18 |
19 |
20 | ```bash
21 | npm install @xoid/react
22 | ```
23 |
24 |
25 |
26 |
27 | ```bash
28 | npm install @xoid/vue
29 | ```
30 |
31 |
32 |
33 |
34 | ```bash
35 | npm install @xoid/svelte
36 | ```
37 |
38 |
39 |
40 |
41 |
42 | > All these framework integration packages have `useAtom` and `useSetup` functions that have the same interface.
43 |
44 | ## Isomorphic component logic
45 |
46 | This might be the most unique feature of **xoid**. With **xoid**, you can write component logic (including lifecycle) ONCE, and run it across multiple frameworks. This feature is for you especially if:
47 | - You're a design system, or a headless UI library maintainer
48 | - You're using multiple frameworks in your project, or refactoring your code from one framework to another
49 | - You dislike React's render cycle and want a simpler, real closure for managing complex state
50 |
51 | The following is called a "setup" function:
52 |
53 | ```js
54 | import { atom, Atom, effect, inject } from 'xoid'
55 | import { ThemeSymbol } from './theme'
56 |
57 | export const CounterSetup = ($props: Atom<{ initialValue: number }>) => {
58 | const { initialValue } = $props.value
59 |
60 | const $counter = atom(initialValue)
61 | const increment = () => $counter.update((s) => s + 1)
62 | const decrement = () => $counter.update((s) => s - 1)
63 |
64 | effect(() => {
65 | console.log('mounted')
66 | return () => console.log('unmounted')
67 | })
68 |
69 | const theme = inject(ThemeSymbol)
70 | console.log("theme is obtained using context:", theme)
71 |
72 | return { $counter, increment, decrement }
73 | }
74 | ```
75 | All `@xoid/react`, `@xoid/vue`, and `@xoid/svelte` modules have an isomorphic `useSetup` function that can consume functions like this.
76 |
77 | > We're aware that not all users need this feature, so we've built it tree-shakable. If `useAtom` is all you need, you may choose to import it from `'@xoid/[FRAMEWORK]/useAtom'`.
78 |
79 |
80 | With this feature, you can effectively replace the following framework-specific APIs:
81 |
82 | | | xoid | React | Vue | Svelte |
83 | |---|---|---|---|---|
84 | | State | `atom` | `useState` / `useReducer` | `reactive` / `ref` | `readable` / `writable` |
85 | | Derived state | `atom` | `useMemo` | `computed` | `derived` |
86 | | Lifecycle | `effect` | `useEffect` | `onMounted`, `onUnmounted` | `onMount`, `onDestroy` |
87 | | Dependency injection | `inject` | `useContext` | `inject` | `getContext` |
88 |
--------------------------------------------------------------------------------
/docs/framework-integrations/use-atom.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: use-atom
3 | title: useAtom
4 | ---
5 |
6 | `import { useAtom } from '@xoid/react'`
7 |
8 | `import { useAtom } from '@xoid/svelte'`
9 |
10 | `import { useAtom } from '@xoid/vue'`
11 |
12 |
13 | Used for subscribing a component to an atom.
14 |
15 | ```js
16 | import { atom } from 'xoid';
17 | import { useAtom } from '@xoid/react'; // or '@xoid/vue' or '@xoid/svelte'
18 |
19 | const $number = atom(5);
20 | const $person = atom({ name: 'John', surname: 'Doe' });
21 |
22 | // inside a component
23 | const number = useAtom($number);
24 |
25 | // inside a component
26 | const name = useAtom($person.focus('name'));
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: getting-started
3 | title: Getting Started
4 | ---
5 |
6 | > **xoid** is a scalable state management library with a small API surface.
7 | > While learning it takes ~5 minutes, you can still manage great complexity with it.
8 |
9 |
10 | ## Installation
11 |
12 | The **xoid** package lives in npm. To install, you can run one of the the following commands:
13 |
14 | import Tabs from '@theme/Tabs';
15 | import TabItem from '@theme/TabItem';
16 |
17 |
24 |
25 |
26 | ```bash
27 | npm install xoid
28 | ```
29 |
30 |
31 |
32 |
33 | ```bash
34 | yarn add xoid
35 | ```
36 |
37 |
38 |
39 |
40 | ```js
41 | import { atom } from 'https://unpkg.com/xoid/index.js'
42 | ```
43 |
44 |
45 |
46 |
47 |
48 | If you're using **xoid** with one of these frameworks, simply install one of the following packages:
49 |
50 |
57 |
58 |
59 | ```bash
60 | yarn add @xoid/react
61 | ```
62 |
63 |
64 |
65 |
66 | ```bash
67 | yarn add @xoid/vue
68 | ```
69 |
70 |
71 |
72 |
73 | ```bash
74 | yarn add @xoid/svelte
75 | ```
76 |
77 |
78 |
79 |
80 |
81 | ## Resources
82 |
83 | - If you're new to **xoid**, we recommend starting with the [quick tutorial in the next section](quick-tutorial).
84 | - In [Examples](examples) section, you'll find examples to run on Codesandbox.
85 | - You can refer to [Recipes](./recipes-react/using-context-correctly) section for more.
86 |
--------------------------------------------------------------------------------
/docs/performance-optimizations.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: performance-optimizations
3 | title: Performance optimizations
4 | ---
5 |
6 | ## Lazy evaluation
7 |
8 | Atoms are lazily evaluated. If an atom is created using a *state initializer function*, this function won't run until the `.value` getter is read, or the atom is subscribed for the first time.
9 |
10 | ```js
11 | const $atom = atom(() => {
12 | console.log('I am lazily evaluated!')
13 | return expensiveComputation(25)
14 | })
15 | // nothing's logged on the console yet
16 |
17 | console.log($atom.value)
18 | // Console: "I am lazily evaluated!"
19 | // Console: 25
20 |
21 | console.log($atom.value)
22 | // Console: 25
23 | ```
24 | You can make use of this feature to avoid expensive computations where possible.
25 |
26 | ## Lazy evaluation in derived atoms
27 |
28 | A derived atom is not much different than a classical atom. Still, its state initializer function will wait for the atom's value to be requested in order to run.
29 |
30 | ```js
31 | const $alpha = atom(3)
32 | const $beta = atom(5)
33 |
34 | const $sum = atom((read) => {
35 | console.log('Evaluation occured')
36 | return read($alpha) + read($beta)
37 | })
38 | // nothing's logged on the console yet
39 | ```
40 |
41 | Later, when it's consumed for the first time:
42 |
43 | ```js
44 | console.log($sum.value)
45 | // Console: "Evaluation occured"
46 | // Console: 8
47 |
48 | console.log($sum.value)
49 | // Console: 8
50 | ```
51 |
52 | ## Dependency collection in derived atoms
53 |
54 | **Dependency collection** is another performance optimization that makes lazy evaluation much more advanced.
55 | When an atom is evaluated, it collects its latest dependencies. Since the `$sum` is evaluated at least once in our previous example, it's now "aware" that it's dependencies are `$alpha` and `$beta`. Let's observe what will happen when those dependencies are updated:
56 |
57 | ```js
58 | $alpha.set(30)
59 | $alpha.update((s) => s + 1)
60 | $beta.set(1000)
61 | // nothing's logged on the console yet
62 |
63 | console.log($sum.value)
64 | // Console: "Evaluation occured"
65 | // Console: 1031
66 |
67 | console.log($sum.value)
68 | // Console: 1031
69 | ```
70 |
71 | Observe that `$sum` knew that it needs to rerun its state initializer when it's `.value` is requested after the dependencies are changed. This can happen thanks to **dependency collection**. `$sum` knows that its internal state is invalid without causing evaluation. It can avoid evaluation until it's essential.
72 |
73 |
74 | ## Lazy evaluation in atoms created with `.map` method
75 |
76 | Same kind of performance optimizations apply to the atoms that are created using the `.map` method.
77 |
78 | ```js
79 | const $count = atom(() => {
80 | console.log('Ancestor atom evaluated')
81 | return 100
82 | })
83 |
84 | const $doubleCount = $count.map((value) => {
85 | console.log('Evaluation occured')
86 | return value * 2
87 | })
88 | // nothing's logged on the console yet
89 |
90 | $count.update(s => s + 1)
91 | // Console: "Ancestor atom evaluated"
92 |
93 | console.log($doubleCount.value)
94 | // Console: "Evaluation occured"
95 | // Console: 202
96 |
97 | console.log($doubleCount.value)
98 | // Console: 202
99 | ```
100 |
101 |
102 |
103 | > **xoid** supports special kind of atoms called "stream"s.
104 | > A stream is "an atom that may or may not have an immediate value". Lazy evaluation works slightly different in a "stream". See the [next section](streams) for more.
--------------------------------------------------------------------------------
/docs/quick-tutorial.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: quick-tutorial
3 | title: Quick Tutorial
4 | ---
5 |
6 | > You can skip this part if you've already read the Github README.
7 |
8 | ### Atom
9 |
10 | Atoms are holders of state.
11 |
12 | ```js
13 | import { atom } from 'xoid'
14 |
15 | const $count = atom(3)
16 | console.log($count.value) // 3
17 | $count.set(5)
18 | $count.update((state) => state + 1)
19 | console.log($count.value) // 6
20 | ```
21 |
22 | Atoms can have actions.
23 |
24 | ```js
25 | import { atom } from 'xoid'
26 |
27 | const $count = atom(5, (a) => ({
28 | increment: () => a.update(s => s + 1),
29 | decrement: () => a.value-- // `.value` setter is supported too
30 | }))
31 |
32 | $count.actions.increment()
33 | ```
34 |
35 | There's the `.focus` method, which can be used as a selector/lens. **xoid** is based on immutable updates, so if you "surgically" set state of a focused branch, changes will propagate to the root.
36 |
37 | ```js
38 | import create from 'xoid'
39 |
40 | const $atom = atom({ deeply: { nested: { alpha: 5 } } })
41 | const previousValue = $atom.value
42 |
43 | // select `.deeply.nested.alpha`
44 | const $alpha = $atom.focus(s => s.deeply.nested.alpha)
45 | $alpha.set(6)
46 |
47 | // root state is replaced with new immutable state
48 | assert($atom.value !== previousValue) // ✅
49 | assert($atom.value.deeply.nested.alpha === 6) // ✅
50 | ```
51 |
52 |
53 | ### Derived state
54 |
55 | State can be derived from other atoms. This API was heavily inspired by **Recoil**.
56 |
57 | ```js
58 | const $alpha = atom(3)
59 | const $beta = atom(5)
60 | // derived atom
61 | const $sum = atom((read) => read($alpha) + read($beta))
62 | ```
63 |
64 | Alternatively, `.map` method can be used to quickly derive the state from a single atom.
65 |
66 | ```js
67 | const $alpha = atom(3)
68 | // derived atom
69 | const $doubleAlpha = $alpha.map((s) => s * 2)
70 | ```
71 | > Atoms are lazily evaluated. This means that the callback functions of `$sum` and `$doubleAlpha` in this example won't execute until the first subscription to these atoms. This is a performance optimization.
72 |
73 | ### Subscriptions
74 |
75 | For subscriptions, `subscribe` and `watch` are used. They are the same, except `watch` runs the callback immediately, while `subscribe` waits for the first update after subscription.
76 |
77 | ```js
78 | const unsub = $atom.subscribe((state, previousState) => {
79 | console.log(state, previousState)
80 | })
81 |
82 | // later
83 | unsub()
84 | ```
85 | > This concludes the basic usage! 🎉
86 |
--------------------------------------------------------------------------------
/docs/recipes-react/creating-react-custom-hooks.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: creating-react-custom-hooks
3 | title: Creating React custom hooks
4 | ---
5 |
6 | ```js
7 | const CounterModel = (value: number) =>
8 | atom(value, (a) => ({
9 | increment: () => a.update((s) => s + 1),
10 | decrement: () => a.update((s) => s - 1),
11 | }))
12 |
13 | const useCounter = (value: number) => useAtom(() => CounterModel(value), true)
14 | ```
15 | > With the second argument set to `true`, `useAtom` returns a 2-item tuple.
16 |
17 | ```js
18 | const [state, { increment, decrement }] = useCounter(0)
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/recipes-react/dynamic-functions-with-fixed-references.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: dynamic-functions-with-fixed-references
3 | title: Dynamic functions with fixed references
4 | ---
5 |
6 | Inside a React function component, in some cases **a function with a fixed reference, but a dynamic content** may be needed. While this is not as straightforward with React*, it is with **xoid**.
7 |
8 | > *: Since this recipe was written, `useEvent` "the missing hook" has been added to React to solve the same problem. However ergonomicity claims of **xoid** still hold.
9 |
10 | ### Quick Example
11 |
12 | Let's imagine, we have the following `React.useEffect`. Inside it, an event listener is attached and removed everytime when `props.number` changes.
13 |
14 | ```js
15 | useEffect(() => {
16 | const callback = () => console.log(props.number)
17 | window.addEventListener('click', callback)
18 | return () => window.removeEventListener('click', callback)
19 | }, [props.number])
20 | ```
21 |
22 | Let's assume that, due to changed app requirements, we want to attach the listener only once, and remove it once the component is unmounted. This can be achieved in React way as the following:
23 |
24 | ```js
25 | // a ref to keep the value
26 | const numberRef = useRef(props.number)
27 | // an effect to update ref's current value when the `props.number` is changed
28 | useEffect(() => (numberRef.current = props.number), [props.number])
29 |
30 | // This time useEffect is with an empty dependency array, and it references the ref.
31 | useEffect(() => {
32 | const callback = () => console.log(numberRef.current)
33 | window.addEventListener('click', callback)
34 | return () => window.removeEventListener('click', callback)
35 | }, [])
36 | ```
37 |
38 | With **xoid**, the equivalent optimization is simply the following:
39 |
40 | ```js
41 | import { effect } from 'xoid'
42 | import { useSetup } from '@xoid/react'
43 |
44 | useSetup(($props) => {
45 | effect(() => {
46 | const callback = () => console.log($props.value.number)
47 | window.addEventListener('click', callback)
48 | return () => window.removeEventListener('click', callback)
49 | })
50 | }, props)
51 | ```
52 |
53 | After getting used to, **xoid** can feel more intuitive than React hooks in a lot of cases.
54 |
55 | ### Another Example
56 |
57 | Let's propose another problem, this time let's examine it in a more concrete scenario.
58 |
59 | Let's imagine, inside a React component, we're supposed to initialize a class called `DragDropLibrary` **only once** as `new DragDropLibrary({ onDrop })`. Let's assume we have only one chance to supply `onDrop` to the class instance, and this function cannot be replaced afterwards.
60 |
61 | Imagine that `props.func` is our dynamic function that changes in every render, and we're supposed to feed it to `onDrop`.
62 |
63 | With **xoid**:
64 | ```js
65 | useSetup(($props) => {
66 | const onDrop = (...args) => $props.value.func(...args)
67 | new DragDropLibrary({ onDrop })
68 | }, props)
69 | ```
70 |
71 | > Think of `useSetup` as not a hook, but as something unchanging, some closure that does not ever rerender. **@xoid/react**, in some sense, is a React without hooks.
72 |
73 | Without **xoid**:
74 | ```js
75 | const funcRef = useRef((...args) => props.func(...args))
76 | useEffect(() => { funcRef.current = (...args) => props.func(...args) }, [props.func])
77 | useMemo(() => {
78 | new DragDropLibrary({ onDrop: funcRef.current })
79 | }, [])
80 | ```
81 |
--------------------------------------------------------------------------------
/docs/recipes-react/grabbing-refs.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: grabbing-refs
3 | title: Grabbing refs
4 | ---
5 |
6 | A **xoid** atom can be used to grab element refs (as in React's terminology) in a typesafe manner.
7 |
8 | ```js
9 | const $ref = atom() // Stream
10 |
11 | $ref.set(document.body)
12 | ```
13 |
14 | It's completely safe to feed `atom.set` calls as refs to React components as `ref` prop.
15 |
16 | ```js
17 | import { atom } from 'xoid'
18 | import { useSetup } from '@xoid/react'
19 | // inside React
20 | const { $ref } = useSetup(() => {
21 | const $ref = atom()
22 | $ref.subscribe((element) => console.log(element))
23 | return { $ref }
24 | })
25 | return
26 | ```
27 | > This usage won't result in Typescript complaints. **xoid**'s `set` method in this example, would be compatible with `React.RefCallback`.
28 |
--------------------------------------------------------------------------------
/docs/recipes-react/refactoring-react-classes.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: refactoring-react-classes
3 | title: Refactoring React classes
4 | ---
5 |
6 | **xoid** can provide a scaffolding system for refactoring React class components into function components. During refactoring, intermediate version of the component keeps working.
7 |
8 | Let's imagine that the following class component is going to be refactored:
9 | ```js
10 | class App extends React.Component {
11 | // state
12 | state = { alpha: 5 }
13 | // methods
14 | incrementAlpha = () => {
15 | this.setState({ alpha: this.state.alpha + 1 })
16 | }
17 | render() {
18 | // render
19 | return
{this.state.alpha}
20 | }
21 | }
22 | ```
23 |
24 | Here's a basic React-like class component runtime prepared with **xoid**.
25 |
26 | ```js
27 | import { atom, Atom } from 'xoid'
28 |
29 | class Runtime {
30 | $props: Atom;
31 | $state!: Atom;
32 | constructor($props: Atom) {
33 | this.$props = $props;
34 | }
35 | get props() {
36 | return this.$props.value;
37 | }
38 | get state() {
39 | return this.$state.value;
40 | }
41 | setState(partial: Partial) {
42 | this.$state.update((s) => ({ ...s, ...partial }));
43 | }
44 | }
45 | ```
46 | We can then easily evolve into the following, working structure without too much refactor:
47 | ```js
48 | class AppRuntime extends Runtime<{}, { alpha: number }> {
49 | $state = atom({ alpha: 5 });
50 | incrementAlpha = () => {
51 | this.setState({ alpha: this.state.alpha + 1 });
52 | };
53 | }
54 |
55 | const App = (props: Props) => {
56 | const self = useSetup(($props) => new AppRuntime($props), props)
57 | useAtom(self.$state)
58 |
59 | return
{self.state.alpha}
60 | }
61 | ```
62 | Observe that the only big differece is replacing `this` in the render function with `self`.
63 |
64 | After getting rid of `this.setState` usages, we can get rid of the `Runtime` class too.
65 | ```js
66 | const AppSetup = ($props: Atom) => {
67 | const $state = atom({ alpha: 5 })
68 | const incrementAlpha = () => $state.focus('alpha').update((s) => s + 1)
69 | return { $state, incrementAlpha }
70 | }
71 |
72 | const App = (props: Props) => {
73 | const { $state, incrementAlpha } = useSetup(AppSetup, props)
74 | const { alpha } = useAtom(self.$state)
75 |
76 | return
{alpha}
77 | }
78 | ```
79 |
--------------------------------------------------------------------------------
/docs/recipes-react/using-context-correctly.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: using-context-correctly
3 | title: Using context correctly
4 | ---
5 |
6 | Using React context for rarely-occuring changes such as theme providers, or internationalization is harmless. However, when context starts to be used for other things, it can affect performance badly.
7 |
8 | A context provider, whenever its state changes, causes its whole subtree to rerender. This can result in noticable slowdowns. Even Redux has used React Context in v6, then reverted it in v7 due to [some recurring complaints](https://github.com/reduxjs/react-redux/issues/1164). This was also mentioned in [The History and Implementation of React-Redux by Mark Erikson](https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/#v7-0).
9 |
10 | Ideally, a context provider should cause zero rerenders.
11 |
12 | There's [an article by Michel Weststrate](https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076) on this topic. In the article, he summarizes as **"we should not store state directly in our context. Instead, we should use context as a dependency injection system"**. **xoid** couldn't agree more, and it can be used to do exactly that.
13 |
14 | Let our state to be shared via context be `{alpha: number, beta: number}`. Instead of feeding it directly as a context value, we can wrap it inside an atom. We can create that atom only once, inside a `useSetup` hook.
15 |
16 | ```js title="./App.tsx"
17 | import { atom } from 'xoid'
18 | import { useSetup } from '@xoid/react'
19 | import { MyContext } from './MyContext'
20 | import { ConsumerComponent } from './ConsumerComponent'
21 |
22 | export const App = () => {
23 | const contextValue = useSetup(() => atom({ alpha: 3, beta: 5 }))
24 |
25 | return (
26 |
27 |
28 |
29 | )
30 | }
31 | ```
32 | > useSetup's callback function will run exactly once, and the context's value reference will remain static, however an atom's internal state is dynamic.
33 |
34 | ```js title="./MyComponent.tsx"
35 | import { useContext } from 'react'
36 | import { use } from 'xoid'
37 | import { useAtom } from '@xoid/react'
38 | import { MyContext } from './MyContext'
39 |
40 | export const MyComponent = () => {
41 | const $atom = useContext(MyContext)
42 | const { alpha, beta } = useAtom($atom)
43 |
44 | return (
45 |