├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/logo-plain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/counter) 6 | 7 | - [Todos (Basic)](https://github.com/xoidlabs/xoid/blob/master/examples/todos-basic) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/todos-basic) 8 | 9 | - [Todos (Filtered)](https://github.com/xoidlabs/xoid/blob/master/examples/todos-filtered) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](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) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](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) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](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) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](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) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](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) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](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 |
46 | alpha: {state.alpha}, beta: {state.beta} 47 | 48 |
49 | ) 50 | } 51 | ``` 52 | 53 | Only the components that subscribe to the content of `MyContext` explicitly via `useAtom` will rerender. Also note that, any component now has the chance to subscribe to a subtree/leaf of the same context such that: `useAtom($atom.focus('alpha'))`. Context selectors are achieved in a single, expressive API! Voilà! -------------------------------------------------------------------------------- /docs/recipes/finite-state-machines.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: finite-state-machines 3 | title: Finite state machines 4 | --- 5 | 6 | With **xoid**, a wide range of finite state machines can be expressed. 7 | 8 | ```js 9 | const createMachine = () => { 10 | function melt() { 11 | machine.set(liquid) 12 | console.log('I melted') 13 | } 14 | 15 | function freeze() { 16 | machine.set(solid) 17 | console.log('I freezed') 18 | } 19 | 20 | function condense() { 21 | machine.set(liquid) 22 | console.log('I condensed') 23 | } 24 | 25 | function vaporize() { 26 | machine.set(gas) 27 | console.log('I vaporized') 28 | } 29 | 30 | const solid = { name: "ice", actions: { melt } }; 31 | const liquid = { name: "water", actions: { freeze, vaporize } }; 32 | const gas = { name: "vapor", actions: { condense } }; 33 | 34 | const machine = atom(solid) 35 | return machine; 36 | } 37 | 38 | const App = () => { 39 | const { name, actions } = useAtom(createMachine) 40 | return ( 41 |
42 | {name} 43 | {Object.keys(actions).map((key) => ( 44 | 47 | ))} 48 |
49 | ) 50 | } 51 | ``` -------------------------------------------------------------------------------- /docs/recipes/nested-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: nested-state 3 | title: Working with nested state 4 | --- 5 | 6 | Before **xoid**: 7 | ```js 8 | setState((state) => { 9 | ...state, 10 | deeply: { 11 | ...state.deeply, 12 | nested: { 13 | ...state.deeply.nested, 14 | value: state.deeply.nested.value + 1 15 | } 16 | } 17 | }) 18 | ``` 19 | 20 | After **xoid**: 21 | ```js 22 | atom.focus(s => s.deeply.nested.value).update(s => s + 1) 23 | ``` 24 | 25 | **xoid** makes it easier to work with nested state. Redux and React (and **xoid**) are based on immutable updates. Immutability is great, however it usually has a bad impact on code readability. 26 | 27 | To overcome this, there are other tools like **immutablejs** or **immer**. Even Redux Toolkit comes with **immer** by default. Note that using Redux toolkit means adding another ~11kB to your bundle size. This number is ~5kB for **immer** alone. **xoid** is ~1kB, yet it can be used to overcome the same problem. 28 | 29 | ### Related 30 | 31 | To see how **xoid** compares to a classical reducer, and a dedicated library that's using **immer** internally (`use-methods`), you can check the following example: 32 | 33 | - [xoid vs useReducer vs useMethods](https://github.com/xoidlabs/xoid/tree/master/examples/xoid-vs-usereducer-vs-usemethods) [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/xoid-vs-usereducer-vs-usemethods) -------------------------------------------------------------------------------- /docs/recipes/persist-localstorage.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: persist-localstorage 3 | title: Persisting data with localStorage 4 | --- 5 | 6 | If the data is serializable, it's fairly simple. 7 | 8 | ```js 9 | const getLocalStorage = (key) => 10 | JSON.parse(localStorage.getItem(key)) 11 | 12 | const setLocalStorage = (key) => (state) => 13 | localStorage.setItem(key, JSON.stringify(state)) 14 | 15 | // usage 16 | const atom = atom(getLocalStorage('foo') || initialState) 17 | atom.subscribe(setLocalStorage('foo')) 18 | ``` -------------------------------------------------------------------------------- /docs/recipes/redux-devtools-integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: redux-devtools-integration 3 | title: Redux Devtools integration 4 | --- 5 | 6 | Import `@xoid/devtools` and set a `debugValue` to your atom. It will send values to the Redux Devtools Extension. 7 | 8 | ```js 9 | import { devtools } from '@xoid/devtools' 10 | import { atom, use } from 'xoid' 11 | devtools() // run once 12 | 13 | const $atom = atom( 14 | { alpha: 5 }, 15 | (a) => { 16 | const $alpha = a.focus(s => s.alpha) 17 | return { 18 | inc: () => $alpha.update(s => s + 1), 19 | resetState: () => a.set({ alpha: 5 }) 20 | deeply: { 21 | nested: { 22 | action: () => $alpha.set(5) 23 | } 24 | } 25 | } 26 | } 27 | ) 28 | 29 | $atom.debugValue = '$atom' // enable watching it by the devtools 30 | 31 | const { deeply, incrementAlpha } = $atom.actions // destructuring is no problem 32 | incrementAlpha() // logs "($atom).incrementAlpha" 33 | deeply.nested.action() // logs "($atom).deeply.nested.action" 34 | $atom.focus(s => s.alpha).set(25) // logs "($atom) Update ([timestamp]) 35 | ``` -------------------------------------------------------------------------------- /docs/recipes/redux-interop.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: redux-interop 3 | title: Using in an existing Redux App 4 | --- 5 | 6 | **xoid** and Redux can coexist in a project without a problem. There's no requirement to get rid of Redux when **xoid** is added. If you're planning to gradually move away from Redux however, **xoid** is a good candidate to do so. For this, one thing you can do is to start managing some part of your Redux state via **xoid**. You can follow these steps: 7 | 8 | ### Step 1: create an "omnipotent" action that has the ability to replace the Redux state 9 | 10 | ```js 11 | const someExistingReducer = (state, action) => { 12 | switch(action.type) { 13 | case 'EXTERNAL_XOID_UPDATE': { 14 | return action.payload 15 | } 16 | ... // other `case` clauses 17 | } 18 | ``` 19 | 20 | ### Step 2: Create an "enhanced atom" 21 | This will forward subscriptions and state modifications directly to the Redux store. 22 | 23 | ```js 24 | import { store } from './store' 25 | 26 | const $mediatorAtom = atom((read) => read(store.getState, store.subscribe)) 27 | $mediatorAtom.set = (payload) => store.dispatch({ type: 'EXTERNAL_XOID_UPDATE', payload }) 28 | ``` 29 | 30 | > Usually, atoms are derived from other **atoms** (as `atom((read) => get($someAtom))`). Observe how `read` is used with two arguments in this example. This is an additional overload that is used to consume an external (non-**xoid**) source. As long as the external source implements some getState & subscribe pair, it can be consumed by **xoid** like this. (See [Deriving state from external sources](../advanced-concepts#deriving-state-from-external-sources)) 31 | > 32 | > Also, in the second line, you may see that the default `set` method is overriden. In **xoid**'s terminology, atoms like these are called [enhanced atoms](../advanced-concepts#enhanced-atoms). Overriding the default `set` method also will modify the `update` method's behavior. 33 | 34 | > Note: If a partial Redux state is desired, A selector instead of the `store.getState` can be used. Second argument remains same as `store.subscribe`. 35 | -------------------------------------------------------------------------------- /docs/recipes/using-immer.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: using-immer 3 | title: Using immer 4 | --- 5 | 6 | `atom` has a `.plugins` array that you can use to enable plugins globally. 7 | If you'd like to add a `.produce` method that uses **immer** internally, you can do it like the following. 8 | 9 | ```js 10 | import { atom } from 'xoid' 11 | import { produce } from 'immer' 12 | 13 | atom.plugins.push((a) => { 14 | a.produce = (fn) => a.update((s) => produce(s, fn)) 15 | }) 16 | ``` 17 | 18 | If you're using TypeScript, simply apply the following module augmentation: 19 | 20 | ```js 21 | declare module 'xoid' { 22 | interface Atom { 23 | produce: (fn: (draft: T) => void) => void 24 | } 25 | interface Stream { 26 | produce: (fn: (draft: T) => void) => void 27 | } 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/recipes/using-reducers.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: using-reducers 3 | title: Using reducers 4 | --- 5 | 6 | You can easily use your existing reducers with **xoid**. The following function can be used to create an atom with reducer. 7 | 8 | ```js 9 | const atomWithReducer = (reducer, initialState) => atom( 10 | initialState, 11 | (a) => ({ dispatch: (action) => a.update((s) => reducer(s, action)) }) 12 | ) 13 | ``` 14 | Let's take this simple reducer: 15 | 16 | ```js 17 | const types = { increase: "INCREASE", decrease: "DECREASE" } 18 | 19 | const counterReducer = (state, { type, by }) => { 20 | switch (type) { 21 | case types.increase: return { 22 | ...state, 23 | count: state.count + by 24 | } 25 | case types.decrease: return { 26 | ...state, 27 | count: state.count - by 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | Usage: 34 | 35 | ```js 36 | const countAtom = atomWithReducer({ count: 0 }, counterReducer) 37 | 38 | countAtom.actions.dispatch({ type: types.increase, by: 1 }) 39 | ``` 40 | 41 | Connecting existing reducers to **xoid** can be beneficial, especially if you're planning to gradually refactor your reducers. The above reducer can be simplified into to the following: 42 | 43 | ```js 44 | const CounterModel = (s) => atom(s, (a) => { 45 | const $count = a.focus('count') 46 | return { 47 | increment: (by) => $count.update(s => s + by), 48 | decrement: (by) => $count.update(s => s - by), 49 | } 50 | }) 51 | ``` 52 | 53 | To see another demonstration with a more dramatic refactor, you can check [Working with nested state](nested-state) 54 | 55 | Related: [Using in an existing Redux App](redux-interop) 56 | -------------------------------------------------------------------------------- /examples/celcius-fahrenheit/README.md: -------------------------------------------------------------------------------- 1 | # xoid / celcius-fahrenheit 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/celcius-fahrenheit) -------------------------------------------------------------------------------- /examples/celcius-fahrenheit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-celcius-fahrenheit-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "17.0.0", 9 | "react-dom": "17.0.0", 10 | "react-scripts": "3.4.3", 11 | "xoid": "^1.0.0-beta.12", 12 | "@xoid/react": "^1.0.0-beta.12" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.8.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/celcius-fahrenheit/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/celcius-fahrenheit/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { atom } from 'xoid' 3 | import { useAtom } from '@xoid/react' 4 | 5 | const $celcius = atom(20) 6 | const $fahrenheit = atom(68) 7 | 8 | const setC = (num: number) => { 9 | if (!num) num = 0 10 | $celcius.set(num) 11 | $fahrenheit.set(num * (9 / 5) + 32) 12 | } 13 | 14 | const setF = (num: number) => { 15 | if (!num) num = 0 16 | $fahrenheit.set(num) 17 | $celcius.set(num - 32 * (5 / 9)) 18 | } 19 | 20 | export default () => { 21 | const C = useAtom($celcius) 22 | const F = useAtom($fahrenheit) 23 | return ( 24 |
25 | setC(parseFloat(e.target.value))} /> 26 | ˚C = 27 | setF(parseFloat(e.target.value))} /> 28 | ˚F 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/celcius-fahrenheit/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # xoid / counter 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/counter) -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-counter-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "17.0.0", 9 | "react-dom": "17.0.0", 10 | "react-scripts": "3.4.3", 11 | "xoid": "^1.0.0-beta.12", 12 | "@xoid/react": "^1.0.0-beta.12" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.8.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/counter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { atom } from 'xoid' 3 | import { useAtom } from '@xoid/react' 4 | import './styles.css' 5 | 6 | const NumberModel = (payload: number) => 7 | atom(payload, (a) => ({ 8 | increment: () => a.update((state) => state + 1), 9 | decrement: () => a.update((state) => state - 1), 10 | })) 11 | type NumberType = ReturnType 12 | 13 | const $alpha = NumberModel(0) 14 | const $beta = NumberModel(5) 15 | const $sum = atom((get) => get($alpha) + get($beta)) 16 | 17 | const NumberCounter = (props: { atom: NumberType; color: string }) => { 18 | const [value, { increment, decrement }] = useAtom(props.atom, true) 19 | return ( 20 |
21 | {value} 22 |
23 | 24 | 25 |
26 |
27 | ) 28 | } 29 | 30 | const Sum = (props: { color: string }) => { 31 | const state = useAtom($sum) 32 | return ( 33 |
34 | {state} 35 |
36 | ) 37 | } 38 | 39 | const App = () => ( 40 |
41 | 42 | + 43 | 44 | = 45 | 46 |
47 | ) 48 | 49 | export default App 50 | -------------------------------------------------------------------------------- /examples/counter/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/counter/src/styles.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | width: fit-content; 4 | font-family: sans-serif; 5 | margin: 100px auto; 6 | font-size: 50px; 7 | text-align: center; 8 | line-height: 100px; 9 | user-select: none; 10 | } 11 | 12 | .container { 13 | flex: 1; 14 | width: 100px; 15 | height: 100px; 16 | margin: 0 10px; 17 | border-radius: 50%; 18 | color: white; 19 | } 20 | 21 | .actions { 22 | display: flex; 23 | } 24 | 25 | .actions button { 26 | -webkit-appearance: none; 27 | border: none; 28 | flex: 1; 29 | height: 50px; 30 | font-size: 25px; 31 | border-radius: 50%; 32 | outline: none; 33 | cursor: pointer; 34 | } 35 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-dots-and-arrows", 3 | "version": "1.0.0", 4 | "description": "React and TypeScript example starter project", 5 | "keywords": [ 6 | "typescript", 7 | "react", 8 | "starter" 9 | ], 10 | "main": "src/index.tsx", 11 | "dependencies": { 12 | "react": "18.0.0", 13 | "react-dom": "18.0.0", 14 | "react-scripts": "4.0.3", 15 | "xoid": "^1.0.0-beta.12", 16 | "@xoid/react": "^1.0.0-beta.12" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "17.0.20", 20 | "@types/react-dom": "17.0.9", 21 | "typescript": "4.4.2" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test --env=jsdom", 27 | "eject": "react-scripts eject" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ] 35 | } -------------------------------------------------------------------------------- /examples/dots-and-arrows/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import React from 'react' 3 | import { useSetup } from '@xoid/react' 4 | import { useAtom } from '@xoid/react' 5 | import { create } from 'xoid' 6 | import Dot from './components/Dot' 7 | import Arrow from './components/Arrow' 8 | import ModulateButton from './components/ModulateButton' 9 | import TemporaryArrow from './components/TemporaryArrow' 10 | import { DotType, ArrowType, TemporaryArrowModel } from './models' 11 | 12 | const $atom = create<{ 13 | dots: Record 14 | arrows: Record 15 | }>({ 16 | dots: { 17 | aaa: { x: 60, y: 10 }, 18 | bbb: { x: 110, y: 100 }, 19 | ccc: { x: 30, y: 100 }, 20 | }, 21 | arrows: { 22 | yyy: { from: 'aaa', to: 'bbb' }, 23 | }, 24 | }) 25 | 26 | const randomString = () => Math.random().toString() 27 | 28 | const createDot = (x: number, y: number) => 29 | $atom.focus('dots').update((s) => ({ ...s, [randomString()]: { x, y } })) 30 | 31 | export default function App() { 32 | const { dots, arrows } = useAtom($atom) 33 | const $temporaryArrow = useSetup(() => 34 | TemporaryArrowModel({ 35 | onEndArrow: (arrow) => 36 | $atom.focus('arrows').update((s) => ({ ...s, [randomString()]: arrow })), 37 | }) 38 | ) 39 | return ( 40 |
createDot(e.clientX, e.clientY)}> 41 | {Object.keys(dots).map((key) => ( 42 | s.dots[key])} 45 | onClick={(e) => { 46 | $temporaryArrow.actions.startOrEndArrow(key) 47 | e.stopPropagation() 48 | }} 49 | /> 50 | ))} 51 | {Object.keys(arrows).map((key) => ( 52 | s.arrows[key])} 55 | $dots={$atom.focus((s) => s.dots)} 56 | /> 57 | ))} 58 | 59 | 60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/components/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAtom } from "@xoid/react" 3 | import { Atom } from 'xoid' 4 | import { ArrowType, BaseArrowType, DotType } from '../models' 5 | 6 | export const ArrowBase = (props: BaseArrowType) => { 7 | const { from, to } = props 8 | return ( 9 | 10 | 17 | 18 | ) 19 | } 20 | 21 | const Arrow = (props: { $arrow: Atom; $dots: Atom> }) => { 22 | const arrow = useAtom(props.$arrow) 23 | const dots = useAtom(props.$dots) 24 | const from = dots[arrow.from] 25 | const to = dots[arrow.to] 26 | return 27 | } 28 | 29 | export default Arrow 30 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/components/Dot.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from '@xoid/react' 2 | import { Atom } from 'xoid' 3 | import React from 'react' 4 | import { DotType } from '../models' 5 | 6 | const Dot = (props: { $dot: Atom } & React.ComponentProps<'div'>) => { 7 | const { $dot, ...rest } = props 8 | const { x, y } = useAtom($dot) 9 | return ( 10 |
17 | ) 18 | } 19 | 20 | export default Dot 21 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/components/ModulateButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Atom } from 'xoid' 3 | import { useEffect, useState } from 'react' 4 | import { DotType } from '../models' 5 | 6 | const modulate = (dot: DotType) => ({ 7 | x: dot.x + 2 * (Math.random() - 0.5), 8 | y: dot.y + 2 * (Math.random() - 0.5), 9 | }) 10 | 11 | const ModulateButton = (props: { $dots: Atom> }) => { 12 | const [state, setState] = useState(false) 13 | useEffect(() => { 14 | const listener = () => { 15 | props.$dots.update((s) => { 16 | const nextS = {} as typeof s 17 | Object.keys(s).forEach((key) => { 18 | const nextDot = modulate(s[key]) 19 | nextS[key] = nextDot 20 | }) 21 | return nextS 22 | }) 23 | } 24 | if (state) window.addEventListener('mousemove', listener) 25 | return () => window.removeEventListener('mousemove', listener) 26 | }, [state, props.$dots]) 27 | 28 | return ( 29 | 37 | ) 38 | } 39 | 40 | export default ModulateButton 41 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/components/TemporaryArrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAtom } from '@xoid/react' 3 | import { Atom } from 'xoid' 4 | import { ArrowBase } from './Arrow' 5 | import { DotType, TemporaryArrowModel, $mousePositionDot } from '../models' 6 | 7 | const TemporaryArrow = (props: { 8 | $dots: Atom> 9 | $temporaryArrow: ReturnType 10 | }) => { 11 | const temporaryArrow = useAtom(props.$temporaryArrow) 12 | const mousePositionDot = useAtom($mousePositionDot) 13 | const dots = useAtom(props.$dots) 14 | 15 | if (!temporaryArrow) return null 16 | 17 | const from = dots[temporaryArrow.from] 18 | const to = temporaryArrow.to 19 | ? dots[temporaryArrow.to] 20 | : mousePositionDot || dots[temporaryArrow.from] 21 | 22 | return 23 | } 24 | 25 | export default TemporaryArrow 26 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StrictMode } from 'react' 3 | import * as ReactDOMClient from 'react-dom/client' 4 | 5 | import App from './App' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = ReactDOMClient.createRoot(rootElement) 9 | 10 | root.render( 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/models.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'xoid' 2 | 3 | export type DotType = { x: number; y: number } 4 | export type ArrowType = { from: string; to: string } 5 | export type BaseArrowType = { from: DotType; to: DotType } 6 | 7 | let isArrowPending = false 8 | export const $mousePositionDot = atom(undefined as DotType | undefined) 9 | window.addEventListener('mousemove', (e) => { 10 | if (!isArrowPending) $mousePositionDot.set(undefined) 11 | $mousePositionDot.set({ x: e.clientX, y: e.clientY }) 12 | }) 13 | 14 | export const TemporaryArrowModel = (props: { onEndArrow: (value: ArrowType) => void }) => 15 | atom(undefined as { from: string; to?: string } | undefined, (a) => { 16 | const startArrow = (id: string) => { 17 | isArrowPending = true 18 | a.set({ from: id }) 19 | } 20 | const endArrow = (id: string) => { 21 | isArrowPending = false 22 | props.onEndArrow({ from: a.value!.from, to: id }) 23 | a.set(undefined) 24 | } 25 | 26 | const startOrEndArrow = (id: string) => { 27 | if (!isArrowPending) startArrow(id) 28 | else endArrow(id) 29 | } 30 | 31 | const abortArrow = () => { 32 | isArrowPending = false 33 | a.set(undefined) 34 | } 35 | window.addEventListener('keydown', abortArrow) 36 | 37 | return { startOrEndArrow } 38 | }) 39 | -------------------------------------------------------------------------------- /examples/dots-and-arrows/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | height: 100vh; 5 | } 6 | 7 | .Dot { 8 | width: 20px; 9 | height: 20px; 10 | border-radius: 50%; 11 | background: red; 12 | position: absolute; 13 | top: -10px; 14 | left: -10px; 15 | } 16 | 17 | .Arrow { 18 | position: absolute; 19 | left: 0; 20 | top: 0; 21 | pointer-events: none; 22 | } 23 | -------------------------------------------------------------------------------- /examples/finite-state-stopwatch/README.md: -------------------------------------------------------------------------------- 1 | # xoid / stopwatch 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/finite-state-stopwatch) -------------------------------------------------------------------------------- /examples/finite-state-stopwatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-stopwatch-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "17.0.0", 9 | "react-dom": "17.0.0", 10 | "react-scripts": "3.4.3", 11 | "xoid": "^1.0.0-beta.12", 12 | "@xoid/react": "^1.0.0-beta.12" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.8.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/finite-state-stopwatch/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/finite-state-stopwatch/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { atom } from 'xoid' 3 | import { useSetup } from '@xoid/react' 4 | import { useAtom } from '@xoid/react' 5 | 6 | const TimerSetup = () => { 7 | let interval: ReturnType 8 | const $time = atom(0) 9 | const $state = atom(stopped) 10 | 11 | function stopped() { 12 | clearInterval(interval) 13 | $time.set(0) 14 | return { 15 | playPauseButton: 'play', 16 | handlePlayPause: () => $state.update(playing), 17 | handleStop: () => $state.update(stopped), 18 | } 19 | } 20 | function playing() { 21 | interval = setInterval(() => $time.update((i) => i + 1), 100) 22 | return { 23 | playPauseButton: 'pause', 24 | handlePlayPause: () => $state.update(paused), 25 | handleStop: () => $state.update(stopped), 26 | } 27 | } 28 | function paused() { 29 | clearInterval(interval) 30 | return { 31 | playPauseButton: 'play', 32 | handlePlayPause: () => $state.update(playing), 33 | handleStop: () => $state.update(stopped), 34 | } 35 | } 36 | 37 | return { $time, $state } 38 | } 39 | 40 | const Stopwatch = () => { 41 | const { $time, $state } = useSetup(TimerSetup) 42 | const time = useAtom($time) 43 | const { playPauseButton, handlePlayPause, handleStop } = useAtom($state) 44 | 45 | return ( 46 |
47 | {time} 48 | 49 | 50 |
51 | ) 52 | } 53 | 54 | const App = () => 55 | export default App 56 | -------------------------------------------------------------------------------- /examples/finite-state-stopwatch/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/framework-agnostic-library/core.tsx: -------------------------------------------------------------------------------- 1 | import { Atom, effect } from 'xoid' 2 | 3 | /* eslint-disable @typescript-eslint/no-namespace */ 4 | export namespace WindowEvent { 5 | export type Props = [ 6 | type: T, 7 | listener: (ev: WindowEventMap[T]) => any, 8 | options?: boolean | AddEventListenerOptions 9 | ] 10 | 11 | export const setup = ($props: Atom>) => 12 | effect(() => 13 | $props.watch((args) => { 14 | window.addEventListener(...args) 15 | return () => window.removeEventListener(...args) 16 | }) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/framework-agnostic-library/react.tsx: -------------------------------------------------------------------------------- 1 | import { useSetup } from '@xoid/react' 2 | import { WindowEvent } from './core' 3 | 4 | export const useWindowEvent = (...props: WindowEvent.Props) => 5 | useSetup(WindowEvent.setup, props) 6 | -------------------------------------------------------------------------------- /examples/framework-agnostic-library/vue.tsx: -------------------------------------------------------------------------------- 1 | import { useSetup } from '@xoid/vue' 2 | import { WindowEvent } from './core' 3 | 4 | export const useWindowEvent = (...props: WindowEvent.Props) => 5 | useSetup(WindowEvent.setup, props) 6 | -------------------------------------------------------------------------------- /examples/redux-devtools-extension/README.md: -------------------------------------------------------------------------------- 1 | # xoid / counter 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/counter) -------------------------------------------------------------------------------- /examples/redux-devtools-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-redux-devtools-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "@xoid/devtools": "0.5.0", 9 | "@xoid/react": "1.0.0-beta.12", 10 | "react": "17.0.0", 11 | "react-dom": "17.0.0", 12 | "react-scripts": "3.4.3", 13 | "xoid": "1.0.0-beta.12" 14 | }, 15 | "devDependencies": { 16 | "typescript": "3.8.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } -------------------------------------------------------------------------------- /examples/redux-devtools-extension/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/redux-devtools-extension/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { atom } from 'xoid' 3 | import devtools from '@xoid/devtools' 4 | import { useAtom } from '@xoid/react' 5 | import './styles.css' 6 | 7 | devtools('my-app') 8 | 9 | const NumberModel = (payload: number) => 10 | atom(payload, (a) => ({ 11 | increment: () => a.update((state) => state + 1), 12 | decrement: () => a.update((state) => state - 1), 13 | })) 14 | type NumberType = ReturnType 15 | 16 | const $alpha = NumberModel(9) 17 | $alpha.debugValue = 'a' 18 | const $beta = NumberModel(16) 19 | $beta.debugValue = 'b' 20 | 21 | const $sum = atom((get) => get($alpha) + get($beta)) 22 | 23 | const NumberCounter = (props: { atom: NumberType; color: string }) => { 24 | const [value, { increment, decrement }] = useAtom(props.atom, true) 25 | console.log(increment.length) 26 | return ( 27 |
28 | {value} 29 |
30 | 31 | 32 |
33 |
34 | ) 35 | } 36 | 37 | const Sum = (props: { color: string }) => { 38 | const state = useAtom($sum) 39 | return ( 40 |
41 | {state} 42 |
43 | ) 44 | } 45 | 46 | const App = () => ( 47 |
48 | 49 | + 50 | 51 | = 52 | 53 |
54 | ) 55 | 56 | export default App 57 | -------------------------------------------------------------------------------- /examples/redux-devtools-extension/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/redux-devtools-extension/src/styles.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | width: fit-content; 4 | font-family: sans-serif; 5 | margin: 100px auto; 6 | font-size: 50px; 7 | text-align: center; 8 | line-height: 100px; 9 | user-select: none; 10 | } 11 | 12 | .container { 13 | flex: 1; 14 | width: 100px; 15 | height: 100px; 16 | margin: 0 10px; 17 | border-radius: 50%; 18 | color: white; 19 | } 20 | 21 | .actions { 22 | display: flex; 23 | } 24 | 25 | .actions button { 26 | -webkit-appearance: none; 27 | border: none; 28 | flex: 1; 29 | height: 50px; 30 | font-size: 25px; 31 | border-radius: 50%; 32 | outline: none; 33 | cursor: pointer; 34 | } 35 | -------------------------------------------------------------------------------- /examples/todos-basic/README.md: -------------------------------------------------------------------------------- 1 | # xoid / todos-basic 2 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/todos-basic) -------------------------------------------------------------------------------- /examples/todos-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-todos-basic-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "17.0.0", 9 | "react-dom": "17.0.0", 10 | "react-scripts": "3.4.3", 11 | "xoid": "^1.0.0-beta.12", 12 | "@xoid/react": "^1.0.0-beta.12" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.8.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/todos-basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/todos-basic/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { atom } from 'xoid' 3 | import { useAtom } from '@xoid/react' 4 | 5 | type TodoType = { title: string; checked: boolean } 6 | type TodoActions = { toggle: () => void; rename: (name: string) => void } 7 | 8 | const $todos = atom( 9 | [ 10 | { title: 'groceries', checked: true }, 11 | { title: 'world invasion', checked: false }, 12 | ], 13 | (a) => ({ 14 | add: (todo: TodoType) => a.update((s) => [...s, todo]), 15 | getItem: (index: number) => { 16 | const $todo = a.focus(index) 17 | return { 18 | toggle: () => $todo.focus('checked').update((s) => !s), 19 | rename: $todo.focus('title').set, 20 | } 21 | }, 22 | }) 23 | ) 24 | 25 | const Todo = (props: { data: TodoType; actions: TodoActions }) => { 26 | const { title, checked } = props.data 27 | const { toggle, rename } = props.actions 28 | return ( 29 |
30 | 31 | rename(e.target.value)} 35 | /> 36 |
37 | ) 38 | } 39 | 40 | export const Todos = () => { 41 | const [todos, { add, getItem }] = useAtom($todos, true) 42 | return ( 43 | <> 44 | {todos.map((data, id) => ( 45 | 46 | ))} 47 | 48 | 49 | ) 50 | } 51 | 52 | const App = () => 53 | export default App 54 | -------------------------------------------------------------------------------- /examples/todos-basic/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/todos-filtered/README.md: -------------------------------------------------------------------------------- 1 | # xoid / todos-filtered 2 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/todos-filtered) -------------------------------------------------------------------------------- /examples/todos-filtered/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-todos-filtered-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "17.0.0", 9 | "react-dom": "17.0.0", 10 | "react-scripts": "3.4.3", 11 | "xoid": "^1.0.0-beta.12", 12 | "@xoid/react": "^1.0.0-beta.12" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.8.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/todos-filtered/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/todos-filtered/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { atom } from 'xoid' 3 | import { useAtom } from '@xoid/react' 4 | 5 | type TodoType = { title: string; checked: boolean } 6 | type TodoActions = { toggle: () => void; rename: (name: string) => void } 7 | 8 | const $todos = atom( 9 | [ 10 | { title: 'groceries', checked: true }, 11 | { title: 'world invasion', checked: false }, 12 | ], 13 | (a) => ({ 14 | add: (todo: TodoType) => a.update((s) => [...s, todo]), 15 | getItem: (index: number) => { 16 | const $todo = a.focus(index) 17 | return { 18 | toggle: () => $todo.focus('checked').update((s) => !s), 19 | rename: $todo.focus('title').set, 20 | } 21 | }, 22 | }) 23 | ) 24 | 25 | const $hideChecked = atom(false) 26 | 27 | const $filteredTodos = atom((get) => { 28 | const hideChecked = get($hideChecked) 29 | const todos = get($todos) 30 | if (hideChecked) return todos.filter((item) => !item.checked) 31 | return todos 32 | }) 33 | 34 | const Todo = (props: { data: TodoType; actions: TodoActions }) => { 35 | const { title, checked } = props.data 36 | const { toggle, rename } = props.actions 37 | return ( 38 |
39 | 40 | rename(e.target.value)} 44 | /> 45 |
46 | ) 47 | } 48 | 49 | export const Todos = () => { 50 | const hideChecked = useAtom($hideChecked) 51 | const filteredTodos = useAtom($filteredTodos) 52 | const { add, getItem } = $todos.actions 53 | return ( 54 | <> 55 |
56 | $hideChecked.update((s) => !s)} 60 | /> 61 | Hide checked items 62 |
63 | {filteredTodos.map((data, id) => ( 64 | 65 | ))} 66 | 67 | 68 | ) 69 | } 70 | 71 | const App = () => 72 | export default App 73 | -------------------------------------------------------------------------------- /examples/todos-filtered/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/transient-update-resize-observer/README.md: -------------------------------------------------------------------------------- 1 | # xoid / transient update example 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/transient-update-resize-observer) -------------------------------------------------------------------------------- /examples/transient-update-resize-observer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-transient-update-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "17.0.0", 9 | "react-dom": "17.0.0", 10 | "react-scripts": "3.4.3", 11 | "@juggle/resize-observer": "^3.3.1", 12 | "xoid": "^1.0.0-beta.12", 13 | "@xoid/react": "^1.0.0-beta.12" 14 | }, 15 | "devDependencies": { 16 | "typescript": "3.8.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } -------------------------------------------------------------------------------- /examples/transient-update-resize-observer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/transient-update-resize-observer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create, effect } from 'xoid' 3 | import { useSetup } from '@xoid/react' 4 | import { ResizeObserver } from '@juggle/resize-observer' 5 | 6 | const ResizeObserverSetup = () => { 7 | const $element = create() 8 | const $rect = create<{ width: number; height: number }>() 9 | const observer = new ResizeObserver(([entry]) => $rect.set(entry.contentRect)) 10 | 11 | effect(() => { 12 | const element = $element.value 13 | if (!element) return 14 | observer.observe(element) 15 | const unsub = $rect.subscribe((rect) => { 16 | element.innerHTML = `${rect.width} x ${rect.height}` 17 | }) 18 | return () => { 19 | unsub() 20 | observer.unobserve(element) 21 | } 22 | }) 23 | 24 | return $element.set 25 | } 26 | 27 | const App = () => { 28 | const ref = useSetup(ResizeObserverSetup) 29 | console.log('this component renders only once!') 30 | 31 | return ( 32 | <> 33 |

This div won't rerender, try resizing it and look at the console!

34 |
49 | 50 | ) 51 | } 52 | 53 | export default App 54 | -------------------------------------------------------------------------------- /examples/transient-update-resize-observer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/use-items-abstraction/README.md: -------------------------------------------------------------------------------- 1 | # xoid / xoid-vs-usereducer-vs-usemethods 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/use-items-abstraction) -------------------------------------------------------------------------------- /examples/use-items-abstraction/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-useitems-abstraction", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.3", 9 | "react-dom": "16.8.3", 10 | "react-scripts": "2.1.8", 11 | "xoid": "^1.0.0-beta.12", 12 | "@xoid/react": "^1.0.0-beta.12" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.3.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/use-items-abstraction/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/use-items-abstraction/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import useItems from "./useItems"; 3 | import Counter from "./Counter"; 4 | 5 | export default function App() { 6 | const [value, onChange] = useState([]); 7 | const { add, getActions } = useItems({ 8 | value, 9 | onChange, 10 | getInitialState: (id) => ({ id, count: 0 }), 11 | getActions: (atom) => { 12 | const $count = atom.focus((s) => s.count); 13 | return { 14 | increment: () => $count((s) => s + 1), 15 | reset: () => $count(0) 16 | }; 17 | } 18 | }); 19 | 20 | return ( 21 | <> 22 | 23 | {value.map(({ id, count }) => { 24 | const { increment, reset } = getActions(id); 25 | return ( 26 | 33 | ); 34 | })} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /examples/use-items-abstraction/src/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | 3 | export default memo(Counter); 4 | 5 | function Counter({ id, count, onIncrement, onReset }) { 6 | return ( 7 |
8 | {count} 9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/use-items-abstraction/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/use-items-abstraction/src/useItems.js: -------------------------------------------------------------------------------- 1 | import { useSetup } from '@xoid/react' 2 | 3 | const ItemsModel = ($props) => { 4 | const { getActions, getInitialState } = $props.value 5 | const $value = $props.focus('value') 6 | $value.set = (s) => $props.value.onChange(s) 7 | 8 | let nextId = $value.value.length 9 | const getItem = (id) => { 10 | const index = $value.value.findIndex((item) => item.id === id) 11 | return $value.focus(index) 12 | } 13 | 14 | return { 15 | add: () => { 16 | $value.update((s) => [...s, getInitialState(nextId)]) 17 | nextId++ 18 | }, 19 | getActions: (id) => { 20 | const $item = getItem(id) 21 | return { 22 | remove: (id) => $value.update((s) => s.filter((item) => item.id !== id)), 23 | ...getActions($item), 24 | } 25 | }, 26 | } 27 | } 28 | 29 | const useItems = (props) => useSetup(ItemsModel, props) 30 | 31 | export default useItems 32 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/README.md: -------------------------------------------------------------------------------- 1 | # xoid / xoid-vs-usereducer-vs-usemethods 2 | 3 | [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat&colorA=4f2eb3&colorB=4f2eb3&logo=codesandbox)](https://githubbox.com/xoidlabs/xoid/tree/master/examples/xoid-vs-usereducer-vs-usemethods) -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-vs-usereducer-vs-usemethods-comparison", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.3", 9 | "react-dom": "16.8.3", 10 | "react-scripts": "2.1.8", 11 | "use-methods": "0.4.5", 12 | "xoid": "^1.0.0-beta.12", 13 | "@xoid/react": "^1.0.0-beta.12" 14 | }, 15 | "devDependencies": { 16 | "typescript": "3.3.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/src/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | 3 | export default memo(Counter); 4 | 5 | function Counter({ id, count, onIncrement, onReset }) { 6 | return ( 7 |
8 | {count} 9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/src/UseMethodsCounters.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useMethods from "use-methods"; 3 | import Counter from "./Counter"; 4 | 5 | export default function UseMethodsCounters() { 6 | const [ 7 | { counters }, 8 | { addCounter, incrementCounter, resetCounter } 9 | ] = useMethods(methods, initialState); 10 | 11 | return ( 12 | <> 13 | 14 | {counters.map(({ id, count }) => ( 15 | 22 | ))} 23 | 24 | ); 25 | } 26 | 27 | const initialState = { 28 | nextId: 0, 29 | counters: [] 30 | }; 31 | 32 | const methods = state => { 33 | const getCounter = id => state.counters.find(counter => counter.id === id); 34 | 35 | return { 36 | addCounter() { 37 | state.counters.push({ id: state.nextId++, count: 0 }); 38 | }, 39 | incrementCounter(id) { 40 | getCounter(id).count++; 41 | }, 42 | resetCounter(id) { 43 | getCounter(id).count = 0; 44 | } 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/src/UseReducerCounters.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from "react"; 2 | import Counter from "./Counter"; 3 | 4 | export default function UseReducerCounters() { 5 | const [{ counters }, dispatch] = useReducer(reducer, initialState); 6 | 7 | return ( 8 | <> 9 | 12 | {counters.map(({ id, count }) => ( 13 | dispatch({ type: "INCREMENT_COUNTER", id })} 17 | onReset={() => dispatch({ type: "RESET_COUNTER", id })} 18 | /> 19 | ))} 20 | 21 | ); 22 | } 23 | 24 | const initialState = { 25 | nextId: 0, 26 | counters: [] 27 | }; 28 | 29 | const reducer = (state, action) => { 30 | let { nextId, counters } = state; 31 | const replaceCount = (id, transform) => { 32 | const index = counters.findIndex(counter => counter.id === id); 33 | const counter = counters[index]; 34 | return { 35 | ...state, 36 | counters: [ 37 | ...counters.slice(0, index), 38 | { ...counter, count: transform(counter.count) }, 39 | ...counters.slice(index + 1) 40 | ] 41 | }; 42 | }; 43 | 44 | switch (action.type) { 45 | case "ADD_COUNTER": { 46 | nextId = nextId + 1; 47 | return { 48 | nextId, 49 | counters: [...counters, { id: nextId, count: 0 }] 50 | }; 51 | } 52 | case "INCREMENT_COUNTER": { 53 | return replaceCount(action.id, count => count + 1); 54 | } 55 | case "RESET_COUNTER": { 56 | return replaceCount(action.id, () => 0); 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/src/XoidCounters.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { atom } from "xoid"; 3 | import { useAtom, useSetup } from '@xoid/react' 4 | import Counter from "./Counter"; 5 | 6 | export default function XoidCounters() { 7 | const $atom = useSetup(() => CountersModel(initialState)); 8 | const counters = useAtom($atom); 9 | const { addCounter, incrementCounter, resetCounter } = $atom.actions; 10 | 11 | return ( 12 | <> 13 | 14 | {counters.map(({ id, count }) => ( 15 | 22 | ))} 23 | 24 | ); 25 | } 26 | 27 | const initialState = { 28 | nextId: 0, 29 | counters: [] 30 | }; 31 | 32 | const CountersModel = ({ counters, nextId }) => 33 | atom(counters, (a) => { 34 | const getCountAtomById = (id) => { 35 | const index = a.value.findIndex((counter) => counter.id === id); 36 | return a.focus((s) => s[index].count); 37 | }; 38 | 39 | return { 40 | addCounter: () => { 41 | a.focus((s) => s[nextId]).set({ id: nextId, count: 0 }); 42 | nextId++; 43 | }, 44 | incrementCounter: (id) => getCountAtomById(id).update((s) => s + 1), 45 | resetCounter: (id) => getCountAtomById(id).set(0) 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import UseReducerCounters from "./UseReducerCounters"; 4 | import UseMethodsCounters from "./UseMethodsCounters"; 5 | import XoidCounters from "./XoidCounters"; 6 | 7 | import "./styles.css"; 8 | 9 | function App() { 10 | return ( 11 |
12 |
13 |

xoid

14 | 15 |
16 |
17 |

useReducer

18 | 19 |
20 |
21 |

useMethods

22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | const rootElement = document.getElementById("root"); 29 | ReactDOM.render(, rootElement); 30 | -------------------------------------------------------------------------------- /examples/xoid-vs-usereducer-vs-usemethods/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | justify-content: space-around; 4 | font-family: sans-serif; 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | const { compilerOptions } = require(`${process.cwd()}/tsconfig.json`); 3 | 4 | module.exports = { 5 | "rootDir": ".", 6 | "transform": {"\\.tsx?$": ['ts-jest']}, 7 | "testEnvironment": "jsdom", 8 | "modulePathIgnorePatterns": [ 9 | "dist", 10 | "tree", 11 | ".backup" 12 | ], 13 | "moduleNameMapper": pathsToModuleNameMapper(compilerOptions.paths || {}, { 14 | prefix: '/', 15 | }), 16 | "testRegex": "test.(js|ts|tsx)$", 17 | "coverageDirectory": "./coverage/", 18 | "collectCoverage": false, 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid-root", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "onurkerimov", 6 | "homepage": "https://xoid.dev", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/xoidlabs/xoid" 10 | }, 11 | "workspaces": { 12 | "packages": [ 13 | "packages/*" 14 | ] 15 | }, 16 | "filesize": { 17 | "track": [ 18 | "./dist/xoid/index.js", 19 | "./dist/lite/index.js" 20 | ] 21 | }, 22 | "scripts": { 23 | "build": "rollup --config", 24 | "dev": "vite dev", 25 | "test": "jest", 26 | "filesize": "filesize" 27 | }, 28 | "devDependencies": { 29 | "@ampproject/filesize": "^4.3.0", 30 | "@rollup/plugin-terser": "^0.3.0", 31 | "@testing-library/react": "^12.1.2", 32 | "@testing-library/vue": "^7.0.0", 33 | "@types/jest": "^26.0.14", 34 | "@types/node": "^14.11.8", 35 | "@types/react": "^18.3.3", 36 | "@types/react-dom": "^18.3.0", 37 | "@types/use-sync-external-store": "^0.0.3", 38 | "@typescript-eslint/eslint-plugin": "^4.31.2", 39 | "@typescript-eslint/parser": "^4.31.2", 40 | "eslint": "^7.5.0", 41 | "eslint-config-prettier": "^6.11.0", 42 | "eslint-config-react-app": "^6.0.0", 43 | "eslint-plugin-flowtype": "^5.2.0", 44 | "eslint-plugin-import": "^2.22.0", 45 | "eslint-plugin-jsx-a11y": "^6.3.1", 46 | "eslint-plugin-prettier": "^3.1.4", 47 | "eslint-plugin-react": "^7.20.3", 48 | "eslint-plugin-react-hooks": "^4.0.8", 49 | "jest": "^26.5.2", 50 | "prettier": "^2.0.5", 51 | "react": "^18.3.1", 52 | "react-dom": "^18.3.1", 53 | "rollup": "^2.30.0", 54 | "rollup-plugin-copy": "^3.4.0", 55 | "rollup-plugin-dts": "^4.0.0", 56 | "rollup-plugin-typescript2": "^0.30.0", 57 | "svelte": "^4.2.8", 58 | "ts-jest": "26.5.2", 59 | "typescript": "^4.4.3", 60 | "use-sync-external-store": "^1.1.0", 61 | "vite": "^4.5.0", 62 | "vue": ">=3.2.30", 63 | "workspaces-run": "^1.0.1" 64 | }, 65 | "dependencies": { 66 | "vite-tsconfig-paths": "^4.2.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/deprecated/lite/README.md: -------------------------------------------------------------------------------- 1 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/deprecated/lite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/lite", 3 | "version": "1.0.0-beta.12", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "sideEffects": false, 8 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 9 | "author": "onurkerimov", 10 | "homepage": "https://xoid.dev", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/xoidlabs/xoid", 14 | "directory": "./packages/lite" 15 | }, 16 | "license": "MIT", 17 | "keywords": [ 18 | "framework-agnostic", 19 | "state", 20 | "management", 21 | "state-management", 22 | "react", 23 | "redux", 24 | "recoil", 25 | "xoid", 26 | "atom", 27 | "store", 28 | "stream", 29 | "observable" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/deprecated/lite/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Destructor } from '../../xoid/src' 2 | import { createInternal, subscribeInternal } from '../../xoid/src/internal/utils' 3 | 4 | // This is a lightweight version of xoid that doesn't have the following features: 5 | // selectors, actions, `focus` and `map` methods. 6 | 7 | export type LiteAtom = { 8 | value: T 9 | set(state: T): void 10 | update(fn: (state: T) => T): void 11 | subscribe(fn: (state: T, prevState: T) => void | Destructor): () => void 12 | watch(fn: (state: T, prevState: T) => void | Destructor): () => void 13 | } 14 | 15 | export const createBaseApi = (value: T): LiteAtom => { 16 | const { get, set, subscribe } = createInternal(value) 17 | // Don't delete recurring `api.set` calls from the following code. 18 | // It lets enhanced atoms work. 19 | const api: LiteAtom = { 20 | get value() { 21 | return get() 22 | }, 23 | set value(item) { 24 | api.set(item) 25 | }, 26 | set: (value: any) => set(value), 27 | update: (fn: any) => api.set(fn(get())), 28 | subscribe: subscribeInternal(subscribe, get), 29 | watch: subscribeInternal(subscribe, get, true), 30 | } 31 | return api 32 | } 33 | -------------------------------------------------------------------------------- /packages/deprecated/model/README.md: -------------------------------------------------------------------------------- 1 | Heavily experimental stuff. Deprecated. Might delete later. 2 | 3 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/deprecated/model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/model", 3 | "version": "0.9.5", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "sideEffects": false, 8 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 9 | "homepage": "https://xoid.dev", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/xoidlabs/xoid", 13 | "directory": "./packages/model" 14 | }, 15 | "license": "MIT", 16 | "keywords": [ 17 | "framework-agnostic", 18 | "state", 19 | "management", 20 | "state-management", 21 | "react", 22 | "redux", 23 | "recoil", 24 | "xoid", 25 | "atom", 26 | "store", 27 | "stream", 28 | "observable" 29 | ], 30 | "devDependencies": { 31 | }, 32 | "peerDependencies": { 33 | "@xoid/core": ">=0.9.5", 34 | "@xoid/engine": ">=0.9.5" 35 | } 36 | } -------------------------------------------------------------------------------- /packages/deprecated/tree/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/tree", 3 | "version": "0.9.5", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "sideEffects": false, 8 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 9 | "homepage": "https://xoid.dev", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/xoidlabs/xoid", 13 | "directory": "./packages/tree" 14 | }, 15 | "license": "MIT", 16 | "keywords": [ 17 | "framework-agnostic", 18 | "state", 19 | "management", 20 | "state-management", 21 | "react", 22 | "redux", 23 | "recoil", 24 | "xoid", 25 | "atom", 26 | "store", 27 | "stream", 28 | "observable" 29 | ], 30 | "dependencies": { 31 | "@xoid/engine": ">=0.9.5" 32 | } 33 | } -------------------------------------------------------------------------------- /packages/deprecated/tree/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createNotifier, createCell } from './utils' 2 | import { subscribe as _subscribe, effect as _effect } from '@xoid/engine' 3 | import type { Init, Atom } from '@xoid/engine' 4 | export type { Atom, Init, GetState, Listener, StateOf } from '@xoid/engine' 5 | 6 | export type AtomTree = Atom & (T extends object ? { [K in keyof T]: AtomTree } : {}) 7 | 8 | export type Create = { 9 | (init: Init): AtomTree 10 | } 11 | 12 | /** 13 | * Creates a store with the first argument as the initial state. 14 | * Configured for immutable updates by default. Mutable mode can be set by setting second argument to `true`. 15 | * @see [xoid.dev/docs/api/create](https://xoid.dev/docs/api/create) 16 | */ 17 | 18 | export const create: Create = (init) => { 19 | const root = createNotifier() 20 | const store = createCell( 21 | { 22 | node: { value: init }, 23 | cache: {}, 24 | root, 25 | }, 26 | 'value' 27 | ) 28 | return store 29 | } 30 | 31 | /** 32 | * Subscribes to an observable. 33 | * @see [xoid.dev/docs/api/subscribe](https://xoid.dev/docs/api/subscribe) 34 | */ 35 | 36 | export const subscribe = _subscribe 37 | 38 | /** 39 | * Subscribes to an observable. Same to `subscribe`, except it runs the callback immediately. 40 | * @see [xoid.dev/docs/api/effect](https://xoid.dev/docs/api/effect) 41 | */ 42 | 43 | export const effect = _effect 44 | -------------------------------------------------------------------------------- /packages/deprecated/tree/src/utils.tsx: -------------------------------------------------------------------------------- 1 | import { createTarget, META, Atom } from '@xoid/engine' 2 | 3 | type Meta = { 4 | parentMeta?: Meta 5 | node: any 6 | cache: Record 7 | key?: string 8 | address?: string[] 9 | shape?: any 10 | root: ReturnType 11 | } 12 | 13 | export const createCell = (meta: Meta, key: string) => { 14 | // Return the child cell if it already exists 15 | if (Object.prototype.hasOwnProperty.call(meta.cache, key)) return meta.cache[key] 16 | 17 | const address = meta.address ? meta.address.map((s) => s) : ([] as string[]) 18 | 19 | const nextMeta = { 20 | parentMeta: meta, 21 | root: meta.root, 22 | key, 23 | address, 24 | get node() { 25 | return meta.node[key] 26 | }, 27 | set node(value) { 28 | const copy = shallowClone(meta.node) 29 | copy[key] = value 30 | meta.node = copy 31 | }, 32 | cache: {}, 33 | } as Meta 34 | 35 | const target = createTarget( 36 | () => nextMeta.node, 37 | (value: any) => void (nextMeta.node = value) 38 | ) 39 | const proxy: any = new Proxy(target, { 40 | get(_, prop: string | symbol) { 41 | if (prop === META) return nextMeta 42 | // start: prototype stuff 43 | const node = nextMeta.node 44 | if ((prop as symbol) === Symbol.toPrimitive) return () => node 45 | 46 | if ( 47 | !Object.prototype.hasOwnProperty.call(node, prop) && 48 | Array.isArray(node) && 49 | Object.prototype.hasOwnProperty.call(Array.prototype, prop) 50 | ) { 51 | throw Error("Array prototype methods shouldn't be used with xoid stores") 52 | } 53 | // end: prototype stuff 54 | return createCell(nextMeta, prop as string) 55 | }, 56 | set() { 57 | return false 58 | }, 59 | has(_, key) { 60 | return key in nextMeta.node 61 | }, 62 | ownKeys(t) { 63 | let keys = Reflect.ownKeys(nextMeta.node) 64 | keys = keys.concat(Reflect.ownKeys(t)) 65 | return Array.from(new Set(keys)) 66 | }, 67 | getOwnPropertyDescriptor(t, k) { 68 | if (Reflect.ownKeys(t).includes(k)) return Reflect.getOwnPropertyDescriptor(t, k) 69 | return Reflect.getOwnPropertyDescriptor(nextMeta.node, k) 70 | }, 71 | }) 72 | meta.cache[key] = proxy 73 | return proxy 74 | } 75 | 76 | const shallowClone = (obj: any) => 77 | Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)) 78 | 79 | export const debug = (store: Atom): Meta => (store as any)[META] 80 | -------------------------------------------------------------------------------- /packages/devtools/README.md: -------------------------------------------------------------------------------- 1 | [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) integration for [xoid](https://xoid.dev) -------------------------------------------------------------------------------- /packages/devtools/copy/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const devtools: (instanceName?: string) => () => void 2 | 3 | export { devtools as default } 4 | -------------------------------------------------------------------------------- /packages/devtools/copy/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | module.exports = require('./devtools.js'); 5 | } else { 6 | module.exports = function () { return function () {}; }; 7 | } -------------------------------------------------------------------------------- /packages/devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/devtools", 3 | "version": "0.7.0", 4 | "main": "./index.js", 5 | "types": "./index.d.ts", 6 | "exports": { 7 | "./devtools": { 8 | "default": "./devtools.js" 9 | } 10 | }, 11 | "sideEffects": false, 12 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 13 | "author": "onurkerimov", 14 | "homepage": "https://xoid.dev", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/xoidlabs/xoid", 18 | "directory": "./packages/devtools" 19 | }, 20 | "license": "MIT", 21 | "keywords": [ 22 | "framework-agnostic", 23 | "state", 24 | "management", 25 | "state-management", 26 | "react", 27 | "redux", 28 | "recoil", 29 | "xoid", 30 | "atom", 31 | "store", 32 | "stream", 33 | "observable" 34 | ], 35 | "peerDependencies": { 36 | "xoid": ">=1.0.0-beta.12" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/devtools/src/devtools.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | INTERNAL, 3 | register, 4 | Atom, 5 | atomMap, 6 | $registry, 7 | createPathMembrane, 8 | current, 9 | plugins, 10 | internal, 11 | } from './utils' 12 | 13 | export { $registry } 14 | 15 | const devtools = (instanceName = 'xoid') => { 16 | let extension: any 17 | try { 18 | extension = (window as any).__REDUX_DEVTOOLS_EXTENSION__ 19 | } catch {} 20 | if (!extension) { 21 | if ( 22 | typeof process === 'object' && 23 | process.env.NODE_ENV === 'development' && 24 | typeof window !== 'undefined' 25 | ) { 26 | console.warn('[Warning] Please install/enable Redux devtools extension') 27 | } 28 | return () => void 0 29 | } 30 | 31 | plugins.push((atom: any) => { 32 | // devtools support 33 | Object.defineProperty(atom, 'debugValue', { 34 | configurable: true, 35 | set(debugValue: string) { 36 | const internal = atom[INTERNAL] 37 | internal.debugValue = debugValue 38 | register(debugValue, atom) 39 | }, 40 | }) 41 | }) 42 | 43 | internal.send = (internal: any) => { 44 | const debugValue = internal.debugValue 45 | if (debugValue) { 46 | const id = atomMap[debugValue].map.get(internal) 47 | const regKey = id ? `${debugValue}-${id}` : debugValue 48 | $registry.focus((s) => s[regKey]).set(internal.get()) 49 | } 50 | } 51 | 52 | internal.wrap = (item, atom) => { 53 | const { debugValue } = atom[INTERNAL] 54 | return debugValue ? createPathMembrane(item, [], atom) : item 55 | } 56 | 57 | const dt = extension.connect({ name: instanceName }) 58 | let unsub: Function 59 | // Suppress all sync updates initially, because $registry is being filled 60 | setTimeout(() => { 61 | dt.init($registry.value) 62 | unsub = $registry.subscribe((value) => { 63 | dt.send(current.action(), value) 64 | }) 65 | }) 66 | return () => unsub?.() 67 | } 68 | export default devtools 69 | -------------------------------------------------------------------------------- /packages/incubator/atom-with-location/index.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'xoid' 2 | 3 | type Location = { 4 | pathname?: string 5 | searchParams?: URLSearchParams 6 | hash?: string 7 | } 8 | 9 | const get = (): Location => { 10 | if (typeof window === 'undefined' || !window.location) { 11 | return {} 12 | } 13 | return { 14 | pathname: window.location.pathname, 15 | searchParams: new URLSearchParams(window.location.search), 16 | hash: window.location.hash, 17 | } 18 | } 19 | 20 | const set = (location: Location): void => { 21 | const url = new URL(window.location.href) 22 | if ('pathname' in location) url.pathname = location.pathname! 23 | if ('searchParams' in location) url.search = location.searchParams!.toString() 24 | if ('hash' in location) url.hash = location.hash! 25 | window.history.pushState(null, '', url) 26 | } 27 | 28 | const subscribe = (callback: () => void) => { 29 | window.addEventListener('popstate', callback) 30 | return () => window.removeEventListener('popstate', callback) 31 | } 32 | 33 | type Options = { 34 | get?: () => T 35 | set?: (location: T, options?: { replace?: boolean }) => void 36 | subscribe?: (callback: () => void) => () => void 37 | } 38 | 39 | export const atomWithLocation = (options: Options = { get, set, subscribe }) => 40 | atom.call(options) 41 | -------------------------------------------------------------------------------- /packages/incubator/dnd/index.tsx: -------------------------------------------------------------------------------- 1 | import { atom, effect, inject, feature, setup } from 'xoid' 2 | 3 | const $startFeature = feature(() => atom(['pointerdown'])) 4 | const $endFeature = feature(() => atom(['pointerup'])) 5 | const $moveFeature = feature(() => atom(['pointermove'])) 6 | 7 | const $isDraggingFeature = feature(() => { 8 | const [$start, $end, $move] = [$startFeature, $endFeature, $moveFeature].map(inject) 9 | const $isDragging = atom(false) 10 | let to 11 | 12 | effect(() => { 13 | $start.subscribe((e) => { 14 | to = setTimeout(() => { 15 | $isDragging.value = true 16 | to = null 17 | }, 400) 18 | }) 19 | 20 | $end.subscribe(() => { 21 | $isDragging.value = false 22 | clearTimeout(to) 23 | }) 24 | 25 | $move.subscribe(fn) 26 | }) 27 | 28 | return $isDragging 29 | }) 30 | 31 | const dispose = setup(() => { 32 | const $isDragging = inject($isDraggingFeature) 33 | return {} 34 | }) 35 | -------------------------------------------------------------------------------- /packages/incubator/produce/README.md: -------------------------------------------------------------------------------- 1 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/incubator/produce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/produce", 3 | "version": "0.0.1", 4 | "main": "./index.js", 5 | "module": "./esm/index.js", 6 | "types": "./index.d.ts", 7 | "sideEffects": false, 8 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 9 | "author": "onurkerimov", 10 | "homepage": "https://xoid.dev", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/xoidlabs/xoid", 14 | "directory": "./packages/reactive" 15 | }, 16 | "license": "MIT", 17 | "keywords": [ 18 | "framework-agnostic", 19 | "state", 20 | "management", 21 | "state-management", 22 | "react", 23 | "redux", 24 | "recoil", 25 | "xoid", 26 | "atom", 27 | "store", 28 | "stream", 29 | "observable" 30 | ], 31 | "peerDependencies": { 32 | "xoid": ">=1.0.0-beta.12" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/incubator/produce/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Atom } from 'xoid' 2 | 3 | export * from 'xoid' 4 | 5 | declare const reactivity: unique symbol 6 | type ReactiveValue = T extends object ? (T extends Function ? T : Reactive) : T 7 | export type Reactive = { [reactivity]: never } & (T extends object 8 | ? { [K in keyof T]: ReactiveValue } 9 | : T) 10 | 11 | const IS_PROXY = Symbol() 12 | 13 | const map = new WeakMap() 14 | 15 | const isPrimitive = (obj: any) => 16 | !(typeof obj === 'function' || typeof obj === 'object') || obj === null 17 | 18 | export const toReactive = (atom: Atom): Reactive => { 19 | const { value } = atom 20 | if (isPrimitive(value)) return value as Reactive 21 | if (map.has(atom)) return map.get(atom) 22 | 23 | const target = (Array.isArray(atom.value) ? [] : {}) as Extract 24 | const proxy = new Proxy(target, { 25 | get(t, key) { 26 | const nextTarget = atom.value[key] 27 | if (key === IS_PROXY) return atom 28 | if (isPrimitive(nextTarget)) return nextTarget 29 | if (typeof nextTarget === 'function') return nextTarget 30 | return t[key] || (t[key] = toReactive(atom.focus(key as keyof T))) 31 | }, 32 | deleteProperty(t, key) { 33 | delete t[key] 34 | return true 35 | }, 36 | }) 37 | map.set(atom, proxy) 38 | return proxy as Reactive 39 | } 40 | -------------------------------------------------------------------------------- /packages/incubator/react-extras/README.md: -------------------------------------------------------------------------------- 1 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/incubator/react-extras/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/react-extras", 3 | "version": "0.0.1", 4 | "main": "./index.js", 5 | "module": "./esm/index.js", 6 | "types": "./index.d.ts", 7 | "sideEffects": false, 8 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 9 | "homepage": "https://xoid.dev", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/xoidlabs/xoid", 13 | "directory": "./packages/react-extras" 14 | }, 15 | "license": "MIT", 16 | "keywords": [ 17 | "framework-agnostic", 18 | "state", 19 | "management", 20 | "state-management", 21 | "react", 22 | "redux", 23 | "recoil", 24 | "xoid", 25 | "atom", 26 | "store", 27 | "stream", 28 | "observable" 29 | ], 30 | "dependencies": { 31 | "xoid": ">=0.9.31", 32 | "@xoid/react": ">=0.9.31", 33 | "use-sync-external-store": "^1.1.0" 34 | }, 35 | "peerDependencies": { 36 | "react": "*" 37 | } 38 | } -------------------------------------------------------------------------------- /packages/incubator/react-extras/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSetup } from '@xoid/react' 2 | import { useAtom } from '@xoid/react' 3 | export { slice } from './slice' 4 | 5 | export type Pair = { value: T; onChange: (value: T) => void } 6 | 7 | export type ComponentType = keyof JSX.IntrinsicElements | React.JSXElementConstructor 8 | 9 | export type PropsOf = T extends ComponentType 10 | ? React.ComponentProps 11 | : T extends (...args: any) => any 12 | ? Parameters[0] 13 | : never 14 | 15 | /** 16 | * Can be used to consume a **value-onChange pair** as an atom inside React components. 17 | * 18 | * Third argument, when set to `true`, enables optimistic updates. This can be 19 | * used for keeping a synced local state, and debouncing the updates sent to the parent. 20 | * @see [xoid.dev/docs/api-react/use-thru](https://xoid.dev/docs/api-react/use-thru) 21 | */ 22 | export const useThru = (value: T, onChange?: (value: T) => void, optimistic = false) => { 23 | const atom = useSetup( 24 | ($props) => { 25 | const atom = $props.focus('value') 26 | const props = $props.value 27 | const prevSet = atom.set 28 | atom.set = (value: T) => { 29 | if (props.optimistic) prevSet(value) 30 | props.onChange?.(value) 31 | } 32 | return atom 33 | }, 34 | { value, onChange, optimistic } 35 | ) 36 | useAtom(atom) 37 | return atom 38 | } 39 | -------------------------------------------------------------------------------- /packages/incubator/react-extras/src/slice.tsx: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' 2 | import React from 'react' 3 | import { Atom } from 'xoid' 4 | 5 | const identity = (value: T) => value 6 | 7 | export const useSelector = ( 8 | atom: Atom, 9 | selector: (value: T) => U = identity as (value: T) => U, 10 | equals = Object.is 11 | ) => 12 | useSyncExternalStoreWithSelector( 13 | atom.subscribe, 14 | () => atom.value, 15 | () => atom.value, 16 | selector, 17 | equals 18 | ) 19 | 20 | const getKeys = (item: T) => (Array.isArray(item) ? item.map((_, i) => i) : Object.keys(item)) 21 | 22 | function shallowEqualArrays(a: T[], b: T[]) { 23 | const len = a.length 24 | if (b.length !== len) return false 25 | for (let i = 0; i < len; i++) { 26 | if (a[i] !== b[i]) { 27 | return false 28 | } 29 | } 30 | return true 31 | } 32 | 33 | export const slice = ( 34 | atom: Atom, 35 | fn: (item: Atom, key: string) => U 36 | ) => 37 | React.createElement(() => ( 38 | <> 39 | {useSelector(atom, getKeys, shallowEqualArrays).map((key) => 40 | fn(atom.focus(key as any), key as any) 41 | )} 42 | 43 | )) 44 | -------------------------------------------------------------------------------- /packages/incubator/resize-observer/index.tsx: -------------------------------------------------------------------------------- 1 | const resizeObserverFeature = feature(() => { 2 | const $resizeObserverEntry = atom() 3 | const resizeObserver = new ResizeObserver((entries) => { 4 | for (const entry of entries) $resizeObserverEntry.set(entry) 5 | }) 6 | return { resizeObserver, $resizeObserverEntry } 7 | }) 8 | 9 | const ResizeObserverModel = (element) => { 10 | const { resizeObserver, $resizeObserverEntry } = inject(resizeObserverFeature) 11 | return atom.call({ 12 | subscribe(listener) { 13 | resizeObserver.observe(element) 14 | const dispose = $resizeObserverEntry.subscribe((entry) => { 15 | if (entry.target === element) listener(entry) 16 | }) 17 | return () => { 18 | resizeObserver.unobserve(element) 19 | dispose() 20 | } 21 | }, 22 | }) 23 | } 24 | 25 | ResizeObserverModel(myDiv).subscribe(() => {}) 26 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/react", 3 | "version": "1.0.0-beta.12", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "module": "./index.esm.js", 11 | "default": "./index.js" 12 | }, 13 | "./useAtom": { 14 | "types": "./useAtom.d.ts", 15 | "module": "./useAtom.esm.js", 16 | "default": "./useAtom.js" 17 | }, 18 | "./useAdapter": { 19 | "types": "./useAdapter.d.ts", 20 | "module": "./useAdapter.esm.js", 21 | "default": "./useAdapter.js" 22 | }, 23 | "./useConstant": { 24 | "types": "./useConstant.d.ts", 25 | "module": "./useConstant.esm.js", 26 | "default": "./useConstant.js" 27 | } 28 | }, 29 | "sideEffects": false, 30 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 31 | "author": "onurkerimov", 32 | "homepage": "https://xoid.dev", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/xoidlabs/xoid", 36 | "directory": "./packages/react" 37 | }, 38 | "license": "MIT", 39 | "keywords": [ 40 | "framework-agnostic", 41 | "state", 42 | "management", 43 | "state-management", 44 | "react", 45 | "redux", 46 | "recoil", 47 | "xoid", 48 | "atom", 49 | "store", 50 | "stream", 51 | "observable" 52 | ], 53 | "dependencies": { 54 | "use-sync-external-store": "^1.1.0" 55 | }, 56 | "peerDependencies": { 57 | "xoid": ">=1.0.0-beta.12", 58 | "react": "*" 59 | } 60 | } -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useDebugValue } from 'react' 2 | import { atom, Atom } from 'xoid' 3 | import { useConstant } from './useConstant' 4 | import { useAdapter } from './useAdapter' 5 | 6 | export { useAtom } from './useAtom' 7 | export { useConstant } from './useConstant' 8 | export { createProvider } from './useAdapter' 9 | 10 | // For server-side rendering: https://github.com/react-spring/zustand/pull/34 11 | const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect 12 | 13 | /** 14 | * Can be used to create local state inside React components. Similar to `React.useMemo`, 15 | * but creates values **exactly once**. 16 | * @see [xoid.dev/docs/framework-integrations/use-setup](https://xoid.dev/docs/framework-integrations/use-setup) 17 | */ 18 | export function useSetup(fn: () => T): T 19 | export function useSetup(fn: ($props: Atom

) => T, props: P): T 20 | export function useSetup(fn: ($props?: any) => any, props?: any): any { 21 | // Calling hooks conditionally wouldn't be an issue here, because we rely on just 22 | // the Function.length, which will remain static. 23 | /* eslint-disable react-hooks/rules-of-hooks */ 24 | let result 25 | if (arguments.length > 1) { 26 | const $props = useConstant(() => atom(() => props)) 27 | useIsoLayoutEffect(() => ($props as Atom).set(props), [props]) 28 | result = useAdapter(() => fn($props)) 29 | } else { 30 | result = useAdapter(fn) 31 | } 32 | useDebugValue(result) 33 | return result 34 | /* eslint-enable react-hooks/rules-of-hooks */ 35 | } 36 | -------------------------------------------------------------------------------- /packages/react/src/useAdapter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, createContext } from 'react' 2 | import type { InjectionKey } from 'xoid' 3 | import { setup, createAdapter } from 'xoid/setup' 4 | import { useConstant } from './useConstant' 5 | 6 | const contextMap = new Map, React.Context>() 7 | // TODO: Consider this in the future 8 | // Instead of multiple kinds of context providers, we may use single provider wrapped, so that it manually 9 | // merges the overrides onto its parents context. so we keep consistency. 10 | // everytime a `context(() => {` opens up, we would run injectMeta maybe 11 | export const createProvider = (key: InjectionKey, defaultValue: T) => { 12 | const context = createContext(defaultValue) 13 | contextMap.set(key, context) 14 | return context.Provider 15 | } 16 | // The only experimental feature of this package is the `read` method in the following React adapter. 17 | // It relies on the fiber internal: `reactInternals.ReactCurrentDispatcher.current.readContext`. 18 | // This may change in the future, but luckily popular projects like `react-relay`, `preact/compat` also assume it. 19 | // https://github.com/preactjs/preact/blob/cef315a681aaaef67200564d9a33bd007422665b/compat/src/render.js#L230 20 | const reactInternals = (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 21 | 22 | const inject = (symbol: InjectionKey): T => { 23 | if (typeof symbol !== 'symbol') throw new TypeError('An injection key should be a symbol.') 24 | return reactInternals.ReactCurrentDispatcher.current.readContext(contextMap.get(symbol)) 25 | } 26 | 27 | export const useAdapter = (fn: () => T): T => { 28 | const adapter = useConstant(() => createAdapter({ inject })) 29 | useEffect(() => { 30 | adapter.mount() 31 | return () => adapter.unmount() 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, []) 34 | return useConstant(() => setup.call(adapter, fn)) 35 | } 36 | -------------------------------------------------------------------------------- /packages/react/src/useAtom.tsx: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from 'use-sync-external-store/shim' 2 | import { useDebugValue } from 'react' 3 | import { Atom, Actions } from 'xoid' 4 | import { useConstant } from './useConstant' 5 | 6 | /** 7 | * An atom, or a function returning an atom can be passed as the first argument. 8 | * When the second optional argument is set to `true`, it will also consume the actions of the atom. 9 | * @see [xoid.dev/docs/framework-integrations/use-atom](https://xoid.dev/docs/framework-integrations/use-atom) 10 | */ 11 | 12 | export function useAtom(atom: Atom): T 13 | export function useAtom(atom: () => Atom): T 14 | export function useAtom(atom: Atom & Actions, withActions: true): [T, U] 15 | export function useAtom(atom: () => Atom & Actions, withActions: true): [T, U] 16 | export function useAtom( 17 | maybeAtom: Atom | (() => Atom), 18 | withActions?: boolean 19 | ): [T, U] | T { 20 | const atom = 21 | useConstant(() => typeof maybeAtom === 'function' && maybeAtom()) || (maybeAtom as Atom) 22 | const value = useSyncExternalStore( 23 | atom.subscribe, 24 | () => atom.value, 25 | () => atom.value 26 | ) 27 | useDebugValue(value) 28 | return withActions ? ([value, (atom as any).actions] as [T, U]) : (value as T) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react/src/useConstant.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | export const useConstant = (fn: () => T): T => { 4 | const ref = useRef<{ c: T }>() 5 | if (!ref.current) ref.current = { c: fn() } 6 | return ref.current.c 7 | } 8 | -------------------------------------------------------------------------------- /packages/reactive/README.md: -------------------------------------------------------------------------------- 1 | # @xoid/reactive 2 | 3 | This library is a thin layer over [xoid](https://xoid.dev) (total bundlesize 1.39 kB gzipped). While **xoid** is an atomic state management library based on immutable updates and explicit subscriptions, this one is based on ES6 proxies. It has a similar experience to **Vue 3 composition API**, or libraries like **Valtio**, **MobX**. 4 | 5 | It has the following exports: 6 | 7 | | Export | Description | | | 8 | |---|---|---|---| 9 | | `create` | Creates atoms. (Re-export of the same `create` function from **xoid**) | 10 | | `reactive` | Creates reactive proxies | 11 | | `computed` | Creates derived atoms | 12 | | `watch` | Creates side-effect watchers | 13 | | `toAtom` | Gets the corresponding atom from a proxy | 14 | | `toReactive` | Gets the corresponding proxy from an atom | 15 | 16 | If you use Vue 3, the exports are basically analogous to the following: 17 | 18 | | Export | Similar to (Vue 3 Composition API) | | | 19 | |---|---|---|---| 20 | | `create` | `ref` | 21 | | `reactive` | `reactive` | 22 | | `computed` | `computed` | 23 | | `watch` | `watchEffect`| 24 | | `toAtom` | `toRef`| 25 | | `toReactive` | `toReactive`| 26 | 27 | 28 | ### Usage 29 | 30 | ```js 31 | import { reactive, computed, watch, toRef, toReactive } from '@xoid/reactive' 32 | 33 | // Create a reactive object 34 | const data = reactive({ 35 | count: 0, 36 | greeting: 'Hello World!' 37 | }) 38 | 39 | // Create a derived atom that automatically subscribe to data.count 40 | const $doubleCount = computed(() => data.count * 2) 41 | 42 | // There's one atom corresponding to a reactive proxy, and vice versa 43 | assert(data === toReactive(toAtom(data))) // ✅ 44 | 45 | // Watch for changes in data 46 | watch(() => { 47 | console.log(`Counter changed: ${data.count}`) 48 | }) 49 | 50 | // Mutate the data freely 51 | data.count++ 52 | ``` 53 | 54 | **xoid** and **@xoid/reactive** libraries are interoperable. They export the same `create` function. The `.value` getter of an atom created by **xoid** package would also auto-subscribe when using `watch`, or `computed` 55 | 56 | ```js 57 | import { atom } from 'xoid' 58 | import { computed } from '@xoid/reactive' 59 | 60 | const $count = atom(0) 61 | 62 | const $doubleCount = computed(() => data.count * 2) 63 | ``` -------------------------------------------------------------------------------- /packages/reactive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/reactive", 3 | "version": "0.0.2", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "sideEffects": false, 8 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 9 | "author": "onurkerimov", 10 | "homepage": "https://xoid.dev", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/xoidlabs/xoid", 14 | "directory": "./packages/reactive" 15 | }, 16 | "license": "MIT", 17 | "keywords": [ 18 | "framework-agnostic", 19 | "state", 20 | "management", 21 | "state-management", 22 | "react", 23 | "redux", 24 | "recoil", 25 | "xoid", 26 | "atom", 27 | "store", 28 | "stream", 29 | "observable" 30 | ], 31 | "peerDependencies": { 32 | "xoid": ">=1.0.0-beta.12" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/reactive/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { atom, Atom, Destructor } from 'xoid' 2 | 3 | // @ts-ignore 4 | const tools = atom.internal 5 | 6 | export * from 'xoid' 7 | 8 | declare const reactivity: unique symbol 9 | type ReactiveValue = T extends object ? (T extends Function ? T : Reactive) : T 10 | export type Reactive = { [reactivity]: never } & (T extends object 11 | ? { [K in keyof T]: ReactiveValue } 12 | : T) 13 | 14 | const IS_PROXY = Symbol() 15 | 16 | const map = new WeakMap() 17 | 18 | const isPrimitive = (obj: any) => 19 | !(typeof obj === 'function' || typeof obj === 'object') || obj === null 20 | 21 | export const toReactive = (a: Atom): Reactive => { 22 | const { value } = a 23 | if (isPrimitive(value)) return value as Reactive 24 | if (map.has(a)) return map.get(a) 25 | 26 | const target = (Array.isArray(a.value) ? [] : {}) as Extract 27 | const proxy = new Proxy(target, { 28 | get(t, key) { 29 | const nextTarget = a.value[key] 30 | if (key === IS_PROXY) return a 31 | if (isPrimitive(nextTarget)) return nextTarget 32 | if (typeof nextTarget === 'function') { 33 | // if (Object.prototype.hasOwnProperty.call(atom.value, key)) { 34 | // console.warn( 35 | // `[@xoid/reactive] Calling functions which are instance variables results in original instance to be mutated.` 36 | // ) 37 | // } 38 | return nextTarget 39 | } 40 | return t[key] || (t[key] = toReactive(a.focus(key as keyof T))) 41 | }, 42 | set(t, key, nextValue) { 43 | a.focus(key as keyof T).set(nextValue) 44 | return true 45 | }, 46 | deleteProperty(t, key) { 47 | const nextValue = { ...a.value } 48 | delete nextValue[key] 49 | a.set(nextValue) 50 | return true 51 | }, 52 | }) 53 | map.set(a, proxy) 54 | return proxy as Reactive 55 | } 56 | 57 | export const reactive = (initialValue: T): Reactive => toReactive(atom(initialValue)) 58 | 59 | export const toAtom = (proxy: T): Atom => proxy[IS_PROXY] 60 | 61 | export const watch = (fn: () => void | Destructor) => { 62 | let cleanup 63 | const clean = () => { 64 | if (cleanup && typeof cleanup === 'function') { 65 | cleanup() 66 | cleanup = undefined 67 | } 68 | } 69 | const atom = computed(() => { 70 | clean() 71 | cleanup = fn() 72 | }) 73 | const unsub = atom.subscribe(() => 0 as any) 74 | return () => { 75 | unsub() 76 | clean() 77 | } 78 | } 79 | 80 | // @ts-ignore 81 | const INTERNAL = tools.symbol 82 | export const computed = (fn: () => T): Atom => { 83 | const a = atom(fn) 84 | a[INTERNAL].track = true 85 | return a 86 | } 87 | -------------------------------------------------------------------------------- /packages/svelte/README.md: -------------------------------------------------------------------------------- 1 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/svelte", 3 | "version": "0.5.2", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "module": "./index.esm.js", 11 | "default": "./index.js" 12 | }, 13 | "./useAtom": { 14 | "types": "./useAtom.d.ts", 15 | "module": "./useAtom.esm.js", 16 | "default": "./useAtom.js" 17 | }, 18 | "./useAdapter": { 19 | "types": "./useAdapter.d.ts", 20 | "module": "./useAdapter.esm.js", 21 | "default": "./useAdapter.js" 22 | } 23 | }, 24 | "sideEffects": false, 25 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 26 | "author": "onurkerimov", 27 | "homepage": "https://xoid.dev", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/xoidlabs/xoid", 31 | "directory": "./packages/vue" 32 | }, 33 | "license": "MIT", 34 | "keywords": [ 35 | "framework-agnostic", 36 | "state", 37 | "management", 38 | "state-management", 39 | "react", 40 | "vue", 41 | "redux", 42 | "recoil", 43 | "xoid", 44 | "atom", 45 | "store", 46 | "stream", 47 | "observable" 48 | ], 49 | "peerDependencies": { 50 | "xoid": ">=1.0.0-beta.12", 51 | "svelte": "*" 52 | } 53 | } -------------------------------------------------------------------------------- /packages/svelte/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { atom, Atom } from 'xoid' 3 | import { onDestroy } from 'svelte' 4 | import type { Readable } from 'svelte/store' 5 | import { useAdapter } from './useAdapter' 6 | 7 | export { useAtom } from './useAtom' 8 | 9 | /** 10 | * @see [xoid.dev/docs/framework-integrations/use-setup](https://xoid.dev/docs/framework-integrations/use-setup) 11 | */ 12 | export function useSetup(fn: () => T): T 13 | export function useSetup(fn: ($props: Atom

) => T, props: Readable

): T 14 | export function useSetup(fn: ($props?: any) => any, props?: Readable): any { 15 | if (arguments.length > 1) { 16 | const $props = atom(() => props) 17 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 18 | onDestroy(props!.subscribe((value) => $props.set(value))) 19 | return useAdapter(() => fn($props)) 20 | } 21 | return useAdapter(fn) 22 | } 23 | -------------------------------------------------------------------------------- /packages/svelte/src/useAdapter.tsx: -------------------------------------------------------------------------------- 1 | import { setup, createAdapter } from 'xoid/setup' 2 | import { getContext, onDestroy, onMount } from 'svelte' 3 | 4 | export const useAdapter = (fn: () => T): T => { 5 | const adapter = createAdapter({ inject: getContext }) 6 | onMount(() => adapter.mount()) 7 | onDestroy(() => adapter.unmount()) 8 | return setup.call(adapter, fn) 9 | } 10 | -------------------------------------------------------------------------------- /packages/svelte/src/useAtom.tsx: -------------------------------------------------------------------------------- 1 | import { Atom, Actions } from 'xoid' 2 | import type { Readable } from 'svelte/store' 3 | 4 | /** 5 | * @see [xoid.dev/docs/framework-integrations/use-atom](https://xoid.dev/docs/framework-integrations/use-atom) 6 | */ 7 | 8 | export function useAtom(atom: Atom): Readable 9 | export function useAtom(atom: Atom & Actions, withActions: true): [Readable, U] 10 | export function useAtom(atom: (Atom & Actions) | Atom, withActions?: boolean): any { 11 | const store = { subscribe: atom.watch } 12 | return withActions ? [store, (atom as any).actions] : store 13 | } 14 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | [xoid.dev](https://xoid.dev) -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xoid/vue", 3 | "version": "0.5.2", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "module": "./index.esm.js", 11 | "default": "./index.js" 12 | }, 13 | "./useAtom": { 14 | "types": "./useAtom.d.ts", 15 | "module": "./useAtom.esm.js", 16 | "default": "./useAtom.js" 17 | }, 18 | "./useAdapter": { 19 | "types": "./useAdapter.d.ts", 20 | "module": "./useAdapter.esm.js", 21 | "default": "./useAdapter.js" 22 | } 23 | }, 24 | "sideEffects": false, 25 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 26 | "author": "onurkerimov", 27 | "homepage": "https://xoid.dev", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/xoidlabs/xoid", 31 | "directory": "./packages/vue" 32 | }, 33 | "license": "MIT", 34 | "keywords": [ 35 | "framework-agnostic", 36 | "state", 37 | "management", 38 | "state-management", 39 | "react", 40 | "vue", 41 | "redux", 42 | "recoil", 43 | "xoid", 44 | "atom", 45 | "store", 46 | "stream", 47 | "observable" 48 | ], 49 | "peerDependencies": { 50 | "xoid": ">=1.0.0-beta.12", 51 | "vue": "<=3.1" 52 | } 53 | } -------------------------------------------------------------------------------- /packages/vue/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { provide, watch, onUnmounted, renderSlot, defineComponent } from 'vue' 3 | import { atom, Atom } from 'xoid' 4 | import { useAdapter } from './useAdapter' 5 | import { InjectionKey } from 'xoid' 6 | export { useAtom } from './useAtom' 7 | 8 | export const createProvider = (key: InjectionKey, defaultValue: T) => { 9 | return defineComponent({ 10 | props: ['value'], 11 | setup(props) { 12 | provide(key, props.value ?? defaultValue) 13 | return (ctx: any) => renderSlot(ctx.$slots, 'default') 14 | }, 15 | }) 16 | } 17 | 18 | /** 19 | * @see [xoid.dev/docs/framework-integrations/use-setup](https://xoid.dev/docs/framework-integrations/use-setup) 20 | */ 21 | export function useSetup(fn: () => T): T 22 | export function useSetup(fn: ($props: Atom

) => T, props: P): T 23 | export function useSetup(fn: ($props?: any) => any, props?: any): any { 24 | if (arguments.length > 1) { 25 | const $props = atom(() => props) 26 | onUnmounted(watch(props, () => $props.set({ ...props }))) 27 | return useAdapter(() => fn($props)) 28 | } 29 | return useAdapter(fn) 30 | } 31 | -------------------------------------------------------------------------------- /packages/vue/src/useAdapter.tsx: -------------------------------------------------------------------------------- 1 | import { inject, onMounted, onUnmounted } from 'vue' 2 | import { setup, createAdapter } from 'xoid/setup' 3 | 4 | export const useAdapter = (fn: () => T): T => { 5 | const adapter = createAdapter({ inject }) 6 | onMounted(adapter.mount) 7 | onUnmounted(adapter.unmount) 8 | return setup.call(adapter, fn) 9 | } 10 | -------------------------------------------------------------------------------- /packages/vue/src/useAtom.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentScope, onScopeDispose, readonly, shallowRef, Ref, DeepReadonly } from 'vue' 2 | import { Atom, Actions } from 'xoid' 3 | 4 | /** 5 | * @see [xoid.dev/docs/framework-integrations/use-atom](https://xoid.dev/docs/framework-integrations/use-atom) 6 | */ 7 | export function useAtom(atom: Atom): Readonly>> 8 | export function useAtom( 9 | atom: Atom & Actions, 10 | withActions: true 11 | ): [Readonly>>, U] 12 | export function useAtom(atom: (Atom & Actions) | Atom, withActions?: boolean) { 13 | const state = shallowRef(atom.value) 14 | 15 | const unsubscribe = atom.subscribe((value) => { 16 | state.value = value 17 | }) 18 | 19 | getCurrentScope() && onScopeDispose(unsubscribe) 20 | const result = readonly(state) 21 | return withActions ? [result, (atom as any).actions] : result 22 | } 23 | -------------------------------------------------------------------------------- /packages/xoid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoid", 3 | "version": "1.0.0-beta.12", 4 | "main": "./index.js", 5 | "module": "./index.esm.js", 6 | "types": "./index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "module": "./index.esm.js", 11 | "default": "./index.js" 12 | }, 13 | "./setup": { 14 | "types": "./setup.d.ts", 15 | "module": "./setup.esm.js", 16 | "default": "./setup.js" 17 | } 18 | }, 19 | "sideEffects": false, 20 | "description": "Framework-agnostic state management library designed for simplicity and scalability", 21 | "author": "onurkerimov", 22 | "homepage": "https://xoid.dev", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/xoidlabs/xoid", 26 | "directory": "./packages/xoid" 27 | }, 28 | "license": "MIT", 29 | "keywords": [ 30 | "framework-agnostic", 31 | "state", 32 | "management", 33 | "state-management", 34 | "react", 35 | "redux", 36 | "recoil", 37 | "xoid", 38 | "atom", 39 | "store", 40 | "stream", 41 | "observable" 42 | ], 43 | "collective": { 44 | "type": "opencollective", 45 | "url": "https://opencollective.com/xoid" 46 | } 47 | } -------------------------------------------------------------------------------- /packages/xoid/src/atom.tsx: -------------------------------------------------------------------------------- 1 | import type { Atom, Stream, Init, Actions } from './internal/types' 2 | import { createAtom, createInternal, tools } from './internal/utils' 3 | import { createSelector } from './internal/createSelector' 4 | 5 | /** 6 | * Creates an atom with the first argument as the initial state. 7 | * Second argument can be used to attach actions to the atom. 8 | * @see [xoid.dev/docs/quick-tutorial](https://xoid.dev/docs/quick-tutorial) 9 | */ 10 | export function atom(init: Init): Atom 11 | export function atom(init: Init, getActions?: (a: Atom) => U): Atom & Actions 12 | export function atom(): Stream 13 | export function atom(init?: Init, getActions?: (a: Atom) => U) { 14 | // @ts-ignore 15 | const internal = (typeof init === 'function' ? createSelector : createInternal)(init) 16 | // @ts-ignore 17 | internal.isStream = !arguments.length 18 | 19 | // @ts-ignore 20 | return createAtom(internal, getActions) 21 | } 22 | 23 | atom.plugins = [] as ((a: Atom) => void)[] 24 | 25 | // intentionally untyped 26 | ;(atom as any).internal = tools 27 | -------------------------------------------------------------------------------- /packages/xoid/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from './atom' 2 | import { inject as injectDeprecated, effect as effectDeprecated } from './setup' 3 | 4 | /** 5 | * @deprecated since version 1.0.0beta-12 6 | * 7 | * `create` as a named export is deprecated. In the future versions, use this instead: 8 | * @example 9 | * // Recommended 10 | * import { atom } from 'xoid' 11 | */ 12 | export const create = atom 13 | 14 | /** 15 | * @deprecated since version 1.0.0beta-12 16 | * 17 | * Default export is deprecated. In the future versions, use this instead: 18 | * @example 19 | * // Recommended 20 | * import { atom } from 'xoid' 21 | */ 22 | export default create 23 | 24 | export { atom } 25 | export const inject = injectDeprecated 26 | export const effect = effectDeprecated 27 | 28 | export type { InjectionKey, EffectCallback } from './setup' 29 | export * from './internal/types' 30 | -------------------------------------------------------------------------------- /packages/xoid/src/internal/createEvent.tsx: -------------------------------------------------------------------------------- 1 | export const createEvent = () => { 2 | const fns = new Set() 3 | const add = (fn: Function) => { 4 | fns.add(fn) 5 | } 6 | const fire = () => { 7 | fns.forEach((fn) => fn()) 8 | fns.clear() 9 | } 10 | return { add, fire } 11 | } 12 | -------------------------------------------------------------------------------- /packages/xoid/src/internal/createFocus.tsx: -------------------------------------------------------------------------------- 1 | import { Atom } from './types' 2 | import { createAtom, Internal } from './utils' 3 | 4 | export const INTERNAL = Symbol() 5 | 6 | export const shallowCopy = (obj: unknown) => 7 | Array.isArray(obj) 8 | ? obj.slice() // avoid _spread polyfill 9 | : Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)) 10 | 11 | // A recursive function that retrieves a value from a nested object using a path of keys. 12 | // It has an optional caching mechanism. It traverses the object based on the provided path 13 | // and returns the value at the specified path. 14 | export function getIn(obj: any, path: string[], cache = false, index = 0): any { 15 | if (index === path.length) return obj 16 | const key = path[index] 17 | if (cache && !obj[key]) obj[key] = {} 18 | return getIn(obj[key], path, cache, index + 1) 19 | } 20 | 21 | // A recursive function that sets a value in a nested object using a path of keys. 22 | // It returns a new object with the updated value at the specified path. If the new value 23 | // is the same as the current value, it returns the original object without making changes. 24 | export function setIn(obj: T, path: string[], value: any, index = 0): T { 25 | if (index === path.length) return value 26 | const key = path[index] 27 | const currentValue = (obj as any)[key] 28 | const nextValue = setIn(currentValue, path, value, index + 1) 29 | // this check holds, because we can avoid recursively copying the parent objects 30 | if (nextValue === currentValue) return obj 31 | const nextObj = shallowCopy(obj) 32 | nextObj[key] = nextValue 33 | return nextObj 34 | } 35 | 36 | const handler = { 37 | get: (path, key) => { 38 | if (key === INTERNAL) return path 39 | return new Proxy([...path, key], handler) 40 | }, 41 | } 42 | 43 | const pathProxy = new Proxy([], handler) 44 | 45 | export const createFocus = 46 | (internal: Internal, basePath: string[]): Atom['focus'] => 47 | (key: any) => { 48 | const relativePath = typeof key === 'function' ? key(pathProxy)[INTERNAL] : [key] 49 | if (!internal.cache) internal.cache = {} 50 | const path = basePath.concat(relativePath) 51 | const { get } = internal 52 | const attempt = getIn(internal.cache, path, true) 53 | return ( 54 | attempt[INTERNAL] || 55 | (attempt[INTERNAL] = createAtom({ 56 | ...internal, 57 | path, 58 | get: () => { 59 | const obj = get() 60 | return obj ? getIn(obj, path) : undefined 61 | }, 62 | // `internal.atom.set` reference is used here instead of `internal.set`, 63 | // because enhanced atoms need to work with focused atoms as well. 64 | set: (value: T) => (internal.atom as Atom).set(setIn(get(), path, value)), 65 | })) 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /packages/xoid/src/internal/createSelector.tsx: -------------------------------------------------------------------------------- 1 | import { createInternal, tools } from './utils' 2 | import { GetState } from './types' 3 | import { createEvent } from './createEvent' 4 | import { INTERNAL } from './createFocus' 5 | 6 | export const createGetState = 7 | (updateState: () => void, add: (fn: Function) => void): GetState => 8 | // @ts-ignore 9 | (read, sub) => { 10 | if (sub) { 11 | add(sub(updateState)) 12 | return read() 13 | } 14 | // @ts-ignore 15 | add(read.subscribe(updateState)) 16 | return read[INTERNAL].get() 17 | } 18 | 19 | export const createSelector = (init: (get: GetState) => T) => { 20 | const internal = createInternal() 21 | const { get, set, listeners } = internal 22 | const e = createEvent() 23 | 24 | let isPending = true 25 | const getter = createGetState(() => { 26 | if (listeners.size) evaluate() 27 | else isPending = true 28 | }, e.add) 29 | 30 | const evaluate = () => { 31 | // cleanup previous subscriptions 32 | e.fire() 33 | isPending = false 34 | const prevTracker = tools.get 35 | // @ts-ignore 36 | tools.get = internal.track ? getter : null 37 | set(init(getter)) 38 | tools.get = prevTracker 39 | } 40 | 41 | internal.get = () => { 42 | if (isPending) evaluate() 43 | return get() 44 | } 45 | return internal 46 | } 47 | -------------------------------------------------------------------------------- /packages/xoid/src/internal/createStream.tsx: -------------------------------------------------------------------------------- 1 | import { createInternal, Internal } from './utils' 2 | import { Atom } from './types' 3 | import { createAtom } from './utils' 4 | 5 | export const createStream = 6 | (internal: Internal): Atom['map'] => 7 | // @ts-ignore 8 | (selector: any, isFilter: any) => { 9 | let prevValue: any 10 | // @ts-ignore 11 | const nextInternal = createInternal() 12 | 13 | let isPending = true 14 | const listener = () => { 15 | if (nextInternal.listeners.size) evaluate() 16 | else isPending = true 17 | } 18 | 19 | const evaluate = () => { 20 | const v = internal.get() 21 | const result = selector(v, prevValue) 22 | isPending = false 23 | if (!(isFilter && !result)) { 24 | nextInternal.set(result) 25 | prevValue = result 26 | } 27 | } 28 | 29 | return createAtom({ 30 | ...nextInternal, 31 | get: () => { 32 | if (!internal.isStream && isPending) evaluate() 33 | return nextInternal.get() 34 | }, 35 | isStream: isFilter || internal.isStream, 36 | subscribe: (fn) => { 37 | const unsub = internal.subscribe(listener) 38 | const unsub2 = nextInternal.subscribe(fn) 39 | return () => { 40 | unsub2() 41 | if (!nextInternal.listeners.size) unsub() 42 | } 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/xoid/src/internal/types.tsx: -------------------------------------------------------------------------------- 1 | export type Atom = { 2 | value: T 3 | set(state: T): void 4 | update(fn: (state: T) => T): void 5 | subscribe(fn: (state: T, prevState: T) => void | Destructor): () => void 6 | watch(fn: (state: T, prevState: T) => void | Destructor): () => void 7 | focus(fn: (state: T) => U): Atom 8 | focus(key: U): Atom 9 | map(fn: (state: T, prevState: T) => U): Atom 10 | map(fn: (state: T, prevState: T) => U, filterOutFalsyValues: true): Stream> 11 | } 12 | 13 | export type Stream = { 14 | value: T | undefined 15 | set(state: T): void 16 | update(fn: (state: T | undefined) => T): void 17 | subscribe(fn: (state: T, prevState: T | undefined) => void | Destructor): () => void 18 | watch(fn: (state: T | undefined, prevState: T | undefined) => void | Destructor): () => void 19 | focus(fn: (state: T) => U): Stream 20 | focus(key: U): Stream 21 | map(fn: (state: T, prevState: T | undefined) => U): Stream 22 | map( 23 | fn: (state: T, prevState: T | undefined) => U, 24 | filterOutFalsyValues: true 25 | ): Stream> 26 | } 27 | 28 | export type GetState = { 29 | (atom: Atom): T 30 | (getState: () => T, subscribe: (fn: () => void) => () => void): T 31 | } 32 | 33 | export type Init = T | ((get: GetState) => T) 34 | 35 | export type Actions = { actions: U; debugValue?: string } 36 | 37 | export type Truthy = Exclude 38 | 39 | declare const voidOnly: unique symbol 40 | export type Destructor = () => void | { [voidOnly]: never } 41 | -------------------------------------------------------------------------------- /packages/xoid/src/internal/utils.tsx: -------------------------------------------------------------------------------- 1 | import create from '..' 2 | import { createEvent } from './createEvent' 3 | import { createFocus, INTERNAL } from './createFocus' 4 | import { createStream } from './createStream' 5 | import { Atom } from './types' 6 | 7 | export type Internal = { 8 | get: () => T 9 | set: (value: T) => void 10 | listeners: Set<() => void> 11 | subscribe: (listener: () => void) => () => void 12 | isStream?: boolean 13 | atom?: Atom 14 | path?: string[] 15 | cache?: any 16 | } 17 | 18 | export const tools = { 19 | symbol: INTERNAL, 20 | send: () => void 0, 21 | wrap: (value) => value, 22 | } as { 23 | get?: Function 24 | send: (_atom: T) => void 25 | wrap: (value: T, _atom: Atom) => T 26 | } 27 | 28 | export const subscribeInternal = 29 | (subscribe: (listener: () => void) => () => void, getter: () => T, watch?: boolean) => 30 | (fn: (state: T, prevState: T) => any) => { 31 | const event = createEvent() 32 | let prevState = getter() 33 | 34 | const callback = (state: T) => { 35 | const result = fn(state, prevState) 36 | if (typeof result === 'function') event.add(result) 37 | } 38 | 39 | if (watch) callback(prevState) 40 | 41 | const unsubscribe = subscribe(() => { 42 | const state = getter() 43 | // this check holds, because sometimes even though root is updated, some branch might be intact. 44 | if (state !== prevState) { 45 | event.fire() 46 | callback(state) 47 | prevState = state 48 | } 49 | }) 50 | 51 | return () => { 52 | event.fire() 53 | unsubscribe() 54 | } 55 | } 56 | 57 | export const createInternal = (value?: T): Internal => { 58 | const listeners = new Set<() => void>() 59 | const self = { 60 | listeners, 61 | get: () => value as T, 62 | set: (nextValue: T) => { 63 | if (value === nextValue) return 64 | value = nextValue 65 | // Used by devtools 66 | tools.send(self) 67 | listeners.forEach((listener) => listener()) 68 | }, 69 | subscribe: (listener: () => void) => { 70 | listeners.add(listener) 71 | return () => void listeners.delete(listener) 72 | }, 73 | } 74 | return self 75 | } 76 | 77 | export function createAtom(internal: Internal, getActions?: any) { 78 | const { get, subscribe, atom } = internal 79 | // Don't delete recurring `api.set` calls from the following code. 80 | // It lets enhanced atoms work. 81 | const nextAtom = { 82 | get value() { 83 | // @ts-ignore 84 | tools.get && tools.get(nextAtom) 85 | return get() 86 | }, 87 | set value(item) { 88 | nextAtom.set(item) 89 | }, 90 | get actions() { 91 | return tools.wrap(actions, nextAtom) 92 | }, 93 | set: (value: any) => internal.set(value), 94 | update: (fn: any) => nextAtom.set(fn(get())), 95 | subscribe: subscribeInternal(subscribe, get), 96 | watch: subscribeInternal(subscribe, get, true), 97 | focus: createFocus(atom ? atom[INTERNAL] : internal, internal.path || []), 98 | map: createStream(internal), 99 | [INTERNAL]: internal, 100 | } as Atom 101 | // @ts-ignore 102 | internal.atom = nextAtom 103 | create.plugins.forEach((fn) => fn(nextAtom)) 104 | const actions = getActions && getActions(nextAtom) 105 | 106 | return nextAtom 107 | } 108 | -------------------------------------------------------------------------------- /packages/xoid/src/setup.tsx: -------------------------------------------------------------------------------- 1 | import { createEvent } from './internal/createEvent' 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-interface 4 | export interface InjectionKey extends Symbol {} 5 | 6 | declare const voidOnly: unique symbol 7 | type Destructor = () => void | { [voidOnly]: never } 8 | export type EffectCallback = () => void | Destructor 9 | 10 | const error = (text: string) => { 11 | throw new Error( 12 | `[xoid] \`${text}\` cannot be used outside the setup context. To create a setup context, use the \`useSetup\` hook from a framework integration package.` 13 | ) 14 | } 15 | 16 | let adapter = { 17 | effect: () => error('effect'), 18 | inject: () => error('inject'), 19 | } 20 | 21 | /** 22 | * @deprecated since version 1.0.0beta-12 23 | * 24 | * In the future versions, {@link inject} and {@link effect} will be exported from the root instead of the `xoid/setup` route. 25 | * @example 26 | * // Recommended 27 | * import { inject } from 'xoid' 28 | */ 29 | export const inject = (symbol: InjectionKey): T => (adapter.inject as any)(symbol) 30 | 31 | /** 32 | * @deprecated since version 1.0.0beta-12 33 | * 34 | * In the future versions, {@link inject} and {@link effect} will be exported from the root instead of the `xoid/setup` route. 35 | * @example 36 | * // Recommended 37 | * import { effect } from 'xoid' 38 | */ 39 | export const effect = (callback: EffectCallback): void => (adapter.effect as any)(callback) 40 | 41 | export function setup( 42 | this: { inject: typeof inject; effect: typeof effect } | void, 43 | fn: () => T 44 | ): T { 45 | const prevAdapter = adapter 46 | adapter = this as any 47 | const result = fn() 48 | adapter = prevAdapter 49 | return result 50 | } 51 | 52 | export const createAdapter = (rest: { inject: typeof inject }) => { 53 | const mount = createEvent() 54 | const unmount = createEvent() 55 | 56 | return { 57 | inject: rest.inject, 58 | mount: mount.fire, 59 | unmount: unmount.fire, 60 | effect: (fn: EffectCallback) => 61 | mount.add(() => { 62 | const result = fn() 63 | if (typeof result === 'function') unmount.add(result) 64 | }), 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import workspacesRun from 'workspaces-run'; 5 | import copy from 'rollup-plugin-copy'; 6 | import dts from 'rollup-plugin-dts'; 7 | 8 | async function main() { 9 | const copyTargets = [] 10 | const plugins = [ 11 | typescript({ 12 | useTsconfigDeclarationDir: true, 13 | }), 14 | copy({ targets: copyTargets }) 15 | ]; 16 | 17 | const results = []; 18 | let packages = []; 19 | 20 | await workspacesRun({ cwd: __dirname, orderByDeps: true }, async (pkg) => { 21 | if (!pkg.config.private) { 22 | packages.push(pkg); 23 | } 24 | }); 25 | 26 | if (!process.env.TARGET) { 27 | console.log('Found the following packages:') 28 | packages.forEach((pkg) => console.log('- ', pkg.name)) 29 | } else { 30 | packages = packages.filter((pkg) => pkg.name === process.env.TARGET) 31 | if (!packages.length) throw new Error(`No package with name "${process.env.TARGET}". `) 32 | } 33 | 34 | packages.forEach((pkg) => { 35 | const basePath = path.relative(__dirname, pkg.dir) 36 | const outputPath = basePath.replace('packages/', 'dist/'); 37 | let copyPath = path.join(basePath, 'copy'); 38 | 39 | if(fs.existsSync(copyPath)) { 40 | copyTargets.push({ src: `${copyPath}/*`, dest: outputPath }) 41 | } 42 | ['package.json', 'README.md'].forEach((fileName) => { 43 | const file = path.join(basePath, fileName) 44 | if(fs.existsSync(file)) { 45 | copyTargets.push({ src: file, dest: outputPath }) 46 | } else if (fileName === 'README.md') { 47 | copyTargets.push({ src: 'README.md', dest: outputPath }) 48 | } 49 | }) 50 | const configExports = pkg.config.exports || {'.': { 51 | types: pkg.config.types, 52 | module: pkg.config.module, 53 | default: pkg.config.main, 54 | }} 55 | 56 | const entries = Object.keys(configExports) 57 | 58 | entries.forEach((entry) => { 59 | const externalLookup = [ 60 | ...Object.keys(pkg.config.dependencies || []), 61 | ...Object.keys(pkg.config.peerDependencies || []), 62 | ...entries.filter(s => s !== '.' || s !== entry) 63 | ]; 64 | const external = (name) => externalLookup.includes(/^((?:\.\/)?(?:.*?))(?:\/|$)/.exec(name)[1]) 65 | 66 | const entryOutputs = configExports[entry] 67 | if(entry === '.') entry = 'index' 68 | 69 | const input = path.join(basePath, 'src', entry + '.tsx'); 70 | const output = [] 71 | 72 | 73 | if(entryOutputs.default) { 74 | output.push({ 75 | file: path.join(outputPath, entry + '.js'), 76 | format: 'cjs', 77 | }) 78 | } 79 | 80 | if(entryOutputs.module) { 81 | output.push({ 82 | file: path.join(outputPath, entry + '.esm.js'), 83 | format: 'esm', 84 | }) 85 | } 86 | 87 | results.push({ 88 | input, 89 | output, 90 | external, 91 | plugins, 92 | }); 93 | 94 | if(entryOutputs.types) { 95 | results.push({ 96 | input: path.join('dist/ts-out', basePath, `src/${entry}.d.ts`), 97 | output: { file: path.join(outputPath, `${entry}.d.ts`), format: 'es' }, 98 | external, 99 | plugins: [dts({})], 100 | }); 101 | } 102 | 103 | }) 104 | 105 | }); 106 | return results; 107 | } 108 | 109 | export default main(); 110 | -------------------------------------------------------------------------------- /tests/__snapshots__/actions.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`uses the actions in vanilla (interop) 1`] = ` 4 | Object { 5 | "actions": Object { 6 | "inc": [Function], 7 | }, 8 | "get": Object { 9 | "count": 1, 10 | }, 11 | "getSerialized": "{\\"count\\":1}", 12 | "self": Object { 13 | "actions": Object { 14 | "inc": [Function], 15 | }, 16 | "focus": [Function], 17 | "map": [Function], 18 | "set": [Function], 19 | "subscribe": [Function], 20 | "update": [Function], 21 | "value": Object { 22 | "count": 1, 23 | }, 24 | "watch": [Function], 25 | Symbol(): Object { 26 | "atom": [Circular], 27 | "get": [Function], 28 | "isStream": false, 29 | "listeners": Set {}, 30 | "set": [Function], 31 | "subscribe": [Function], 32 | }, 33 | }, 34 | "selfSerialized": "{\\"value\\":{\\"count\\":1},\\"actions\\":{}}", 35 | } 36 | `; 37 | 38 | exports[`uses the actions in vanilla 1`] = ` 39 | Object { 40 | "actions": Object { 41 | "inc": [Function], 42 | }, 43 | "get": Object { 44 | "count": 1, 45 | }, 46 | "getSerialized": "{\\"count\\":1}", 47 | "self": Object { 48 | "actions": Object { 49 | "inc": [Function], 50 | }, 51 | "focus": [Function], 52 | "map": [Function], 53 | "set": [Function], 54 | "subscribe": [Function], 55 | "update": [Function], 56 | "value": Object { 57 | "count": 1, 58 | }, 59 | "watch": [Function], 60 | Symbol(): Object { 61 | "atom": [Circular], 62 | "get": [Function], 63 | "isStream": false, 64 | "listeners": Set {}, 65 | "set": [Function], 66 | "subscribe": [Function], 67 | }, 68 | }, 69 | "selfSerialized": "{\\"value\\":{\\"count\\":1},\\"actions\\":{}}", 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /tests/__snapshots__/derived-atoms.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`creates a derived atom from multiple atoms 1`] = ` 4 | Object { 5 | "actions": undefined, 6 | "get": 8, 7 | "getSerialized": "8", 8 | "self": Object { 9 | "actions": undefined, 10 | "focus": [Function], 11 | "map": [Function], 12 | "set": [Function], 13 | "subscribe": [Function], 14 | "update": [Function], 15 | "value": 8, 16 | "watch": [Function], 17 | Symbol(): Object { 18 | "atom": [Circular], 19 | "get": [Function], 20 | "isStream": false, 21 | "listeners": Set {}, 22 | "set": [Function], 23 | "subscribe": [Function], 24 | }, 25 | }, 26 | "selfSerialized": "{\\"value\\":8}", 27 | } 28 | `; 29 | 30 | exports[`creates a derived atom using the same atom 1`] = ` 31 | Object { 32 | "actions": undefined, 33 | "get": 8, 34 | "getSerialized": "8", 35 | "self": Object { 36 | "actions": undefined, 37 | "focus": [Function], 38 | "map": [Function], 39 | "set": [Function], 40 | "subscribe": [Function], 41 | "update": [Function], 42 | "value": 8, 43 | "watch": [Function], 44 | Symbol(): Object { 45 | "atom": [Circular], 46 | "get": [Function], 47 | "isStream": false, 48 | "listeners": Set {}, 49 | "set": [Function], 50 | "subscribe": [Function], 51 | }, 52 | }, 53 | "selfSerialized": "{\\"value\\":8}", 54 | } 55 | `; 56 | 57 | exports[`creates a derived atom using the same atom using selectors 1`] = ` 58 | Object { 59 | "actions": undefined, 60 | "get": 8, 61 | "getSerialized": "8", 62 | "self": Object { 63 | "actions": undefined, 64 | "focus": [Function], 65 | "map": [Function], 66 | "set": [Function], 67 | "subscribe": [Function], 68 | "update": [Function], 69 | "value": 8, 70 | "watch": [Function], 71 | Symbol(): Object { 72 | "atom": [Circular], 73 | "get": [Function], 74 | "isStream": false, 75 | "listeners": Set {}, 76 | "set": [Function], 77 | "subscribe": [Function], 78 | }, 79 | }, 80 | "selfSerialized": "{\\"value\\":8}", 81 | } 82 | `; 83 | -------------------------------------------------------------------------------- /tests/__snapshots__/lazy-evaluation.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`lazily evaluates a state initializer function 1`] = ` 4 | Object { 5 | "actions": undefined, 6 | "get": 5, 7 | "getSerialized": "5", 8 | "self": Object { 9 | "actions": undefined, 10 | "focus": [Function], 11 | "map": [Function], 12 | "set": [Function], 13 | "subscribe": [Function], 14 | "update": [Function], 15 | "value": 5, 16 | "watch": [Function], 17 | Symbol(): Object { 18 | "atom": [Circular], 19 | "get": [Function], 20 | "isStream": false, 21 | "listeners": Set {}, 22 | "set": [Function], 23 | "subscribe": [Function], 24 | }, 25 | }, 26 | "selfSerialized": "{\\"value\\":5}", 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /tests/__snapshots__/reactive.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`creates a derived atom using the same atom using selectors 1`] = ` 4 | Object { 5 | "actions": undefined, 6 | "get": 8, 7 | "getSerialized": "8", 8 | "self": Object { 9 | "actions": undefined, 10 | "focus": [Function], 11 | "map": [Function], 12 | "set": [Function], 13 | "subscribe": [Function], 14 | "update": [Function], 15 | "value": 8, 16 | "watch": [Function], 17 | Symbol(): Object { 18 | "atom": [Circular], 19 | "get": [Function], 20 | "isStream": false, 21 | "listeners": Set {}, 22 | "set": [Function], 23 | "subscribe": [Function], 24 | "track": true, 25 | }, 26 | }, 27 | "selfSerialized": "{\\"value\\":8}", 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /tests/actions.test.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'xoid' 2 | import { debug } from './testHelpers' 3 | 4 | it('uses the actions in vanilla (interop)', async () => { 5 | const $atom = atom({ count: 0 }, (a) => ({ 6 | inc: () => a.update((state) => ({ count: state.count + 1 })), 7 | })) 8 | $atom.actions.inc() 9 | expect(debug($atom)).toMatchSnapshot() 10 | }) 11 | 12 | it('uses the actions in vanilla', async () => { 13 | const $atom = atom({ count: 0 }, (a) => ({ 14 | inc: () => a.update((state) => ({ count: state.count + 1 })), 15 | })) 16 | $atom.actions.inc() 17 | expect(debug($atom)).toMatchSnapshot() 18 | }) 19 | -------------------------------------------------------------------------------- /tests/basic.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from '@testing-library/react' 2 | import { create } from 'xoid' 3 | import { debug } from './testHelpers' 4 | 5 | const consoleError = console.error 6 | afterEach(() => { 7 | cleanup() 8 | console.error = consoleError 9 | }) 10 | 11 | it('creates a atom with a primitive value', () => { 12 | const atom = create(5) 13 | expect(debug(atom)).toMatchSnapshot() 14 | }) 15 | 16 | it('creates a atom with a record', () => { 17 | const atom = create({ alpha: 3, beta: 5 }) 18 | expect(debug(atom)).toMatchSnapshot() 19 | }) 20 | 21 | it('normalizes nested atoms in a record', () => { 22 | const atom = create({ alpha: create(3), beta: create(5) }) 23 | expect(debug(atom)).toMatchSnapshot() 24 | }) 25 | -------------------------------------------------------------------------------- /tests/derived-atoms.test.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'xoid' 2 | import { debug } from './testHelpers' 3 | 4 | it('creates a derived atom from multiple atoms', () => { 5 | const a = create(3) 6 | const b = create(5) 7 | const atom = create((get) => get(a) + get(b)) 8 | expect(debug(atom)).toMatchSnapshot() 9 | }) 10 | 11 | it('creates a derived atom using the same atom', () => { 12 | const firstAtom = create({ alpha: 3, beta: 5 }) 13 | const atom = create((get) => { 14 | const value = get(firstAtom) 15 | return value.alpha + value.beta 16 | }) 17 | expect(debug(atom)).toMatchSnapshot() 18 | }) 19 | 20 | it('creates a derived atom using the same atom using selectors', () => { 21 | const firstAtom = create({ alpha: 3, beta: 5 }) 22 | const atom = create((get) => get(firstAtom.focus('alpha')) + get(firstAtom.focus('beta'))) 23 | expect(debug(atom)).toMatchSnapshot() 24 | }) 25 | 26 | it('creates a derived atom from multiple atoms (keeps in sync)', () => { 27 | const a = create(3) 28 | const b = create(5) 29 | const atom = create((get) => get(a) + get(b)) 30 | expect(atom.value).toBe(8) 31 | 32 | a.update((s) => s + 1) 33 | expect(atom.value).toBe(9) 34 | }) 35 | 36 | it('creates a derived atom using the same atom (keeps in sync)', () => { 37 | const firstAtom = create({ alpha: 3, beta: 5 }) 38 | const atom = create((get) => { 39 | const value = get(firstAtom) 40 | return value.alpha + value.beta 41 | }) 42 | expect(atom.value).toBe(8) 43 | 44 | firstAtom.focus('alpha').update((s) => s + 1) 45 | expect(atom.value).toBe(9) 46 | }) 47 | 48 | it('creates a derived atom using the same atom using selectors (keeps in sync)', () => { 49 | const firstAtom = create({ alpha: 3, beta: 5 }) 50 | const atom = create((get) => get(firstAtom.focus('alpha')) + get(firstAtom.focus('beta'))) 51 | expect(atom.value).toBe(8) 52 | 53 | firstAtom.focus('alpha').update((s) => s + 1) 54 | expect(atom.value).toBe(9) 55 | }) 56 | 57 | test('can derive state from external sources', () => { 58 | const fakeReduxBase = create(8) 59 | const fakeRedux = { 60 | getState: () => fakeReduxBase.value, 61 | subscribe: (item: any) => fakeReduxBase.subscribe(item), 62 | } 63 | 64 | const reduxAtom = create((get) => get(fakeRedux.getState, fakeRedux.subscribe)) 65 | const listener = jest.fn() 66 | 67 | expect(reduxAtom.value).toBe(8) 68 | 69 | reduxAtom.subscribe(listener) 70 | expect(listener).not.toBeCalled() 71 | reduxAtom.update((s) => s + 1) 72 | expect(listener).toBeCalled() 73 | expect(listener).toBeCalledWith(9, 8) 74 | }) 75 | -------------------------------------------------------------------------------- /tests/devtools.test.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'xoid' 2 | import devtools, { $registry } from '@xoid/devtools' 3 | 4 | const dt = { 5 | init: jest.fn(), 6 | send: jest.fn(), 7 | } 8 | const extension = { 9 | connect: jest.fn(() => dt), 10 | } 11 | 12 | const NumberModel = (payload: number) => 13 | create(payload, (atom) => ({ 14 | increment: () => atom.update((state) => state + 1), 15 | incrementAsync: async () => { 16 | await new Promise((resolve) => setTimeout(resolve)) 17 | atom.update((state) => state + 1) 18 | }, 19 | decrement: () => atom.update((state) => state - 1), 20 | })) 21 | 22 | beforeAll(() => { 23 | ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extension 24 | devtools() 25 | expect(extension.connect).toBeCalled() 26 | }) 27 | 28 | afterAll(() => { 29 | ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = undefined 30 | }) 31 | 32 | it('Devtools works', async () => { 33 | const $alpha = NumberModel(0) 34 | $alpha.debugValue = 'same name' 35 | 36 | const $beta = NumberModel(0) 37 | $beta.debugValue = 'same name' 38 | 39 | // wait for 1 tick 40 | await new Promise((resolve) => setTimeout(resolve)) 41 | 42 | expect(dt.init).toBeCalled() 43 | 44 | $alpha.actions.increment() 45 | expect(dt.send).toBeCalledWith( 46 | { payload: [], type: '(same name).increment' }, 47 | { 'same name': 1, 'same name-1': 0 } 48 | ) 49 | 50 | expect($registry.value).toEqual({ 'same name': 1, 'same name-1': 0 }) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/enhanced-atoms.test.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'xoid' 2 | 3 | const consoleError = console.error 4 | afterEach(() => { 5 | console.error = consoleError 6 | }) 7 | 8 | it('enhanced atoms work', () => { 9 | const listener = jest.fn() 10 | const $source = create(3) 11 | 12 | const fakeRedux = { 13 | getValue: () => $source.value, 14 | subscribe: $source.subscribe, 15 | } 16 | 17 | const $enhanced = create((get) => get(fakeRedux.getValue, fakeRedux.subscribe)) 18 | $enhanced.set = $source.set 19 | 20 | const unsub = $enhanced.subscribe(listener) 21 | expect(listener).not.toBeCalled() 22 | 23 | $enhanced.set(3) 24 | expect(listener).not.toBeCalled() 25 | 26 | $enhanced.set(4) 27 | expect(listener).toBeCalledTimes(1) 28 | expect(listener).toBeCalledWith(4, 3) 29 | expect($source.value).toBe(4) 30 | 31 | unsub() 32 | expect(listener).toBeCalledTimes(1) 33 | }) 34 | 35 | it('enhanced atoms work with update function', () => { 36 | const listener = jest.fn() 37 | const $source = create(3) 38 | 39 | const fakeRedux = { 40 | getValue: () => $source.value, 41 | subscribe: $source.subscribe, 42 | } 43 | 44 | const $enhanced = create((get) => get(fakeRedux.getValue, fakeRedux.subscribe)) 45 | $enhanced.set = $source.set 46 | 47 | const unsub = $enhanced.subscribe(listener) 48 | expect(listener).not.toBeCalled() 49 | 50 | $enhanced.update(() => 3) 51 | expect(listener).not.toBeCalled() 52 | 53 | $enhanced.update(() => 4) 54 | expect(listener).toBeCalledTimes(1) 55 | expect(listener).toBeCalledWith(4, 3) 56 | expect($source.value).toBe(4) 57 | 58 | unsub() 59 | expect(listener).toBeCalledTimes(1) 60 | }) 61 | 62 | it('enhanced atoms also work when updates are nested', () => { 63 | const fn = jest.fn() 64 | const $source = create({ deep: { value: 24 } }) 65 | 66 | const $enhanced = create((get) => get($source)) 67 | $enhanced.set = (value: typeof $enhanced.value) => { 68 | fn() 69 | $source.set(value) 70 | } 71 | 72 | expect(fn).toBeCalledTimes(0) 73 | expect($source.value.deep.value).toBe(24) 74 | 75 | $enhanced.focus((s) => s.deep.value).update((s) => s + 1) 76 | 77 | expect(fn).toBeCalledTimes(1) 78 | expect($source.value.deep.value).toBe(25) 79 | }) 80 | 81 | it('enhanced atoms should not accidentally override internal set', () => { 82 | const fn = jest.fn() 83 | const $source = create({ deep: { value: 24 } }) 84 | 85 | const $enhanced = create((get) => get($source)) 86 | $enhanced.set = (value: typeof $enhanced.value) => { 87 | fn() 88 | $source.set(value) 89 | } 90 | 91 | expect(fn).toBeCalledTimes(0) 92 | expect($source.value.deep.value).toBe(24) 93 | 94 | $enhanced.focus((s) => s.deep.value).update((s) => s + 1) 95 | 96 | expect(fn).toBeCalledTimes(1) 97 | expect($source.value.deep.value).toBe(25) 98 | 99 | // this time editing source 100 | $source.focus((s) => s.deep.value).update((s) => s + 1) 101 | expect(fn).toBeCalledTimes(1) 102 | }) 103 | -------------------------------------------------------------------------------- /tests/isomorphism/CounterReact.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAtom, useSetup } from '@xoid/react' 3 | import { CounterSetup } from './CounterSetup' 4 | 5 | const CounterReact = (props: { initialValue: number }) => { 6 | const { $counter, increment, decrement } = useSetup(CounterSetup, props) 7 | const counter = useAtom($counter) 8 | 9 | return ( 10 |

11 | count: {counter} 12 | 13 | 14 |
15 | ) 16 | } 17 | 18 | export default CounterReact 19 | -------------------------------------------------------------------------------- /tests/isomorphism/CounterSetup.tsx: -------------------------------------------------------------------------------- 1 | import { Atom } from 'xoid' 2 | import { effect } from 'xoid' 3 | 4 | export const CounterSetup = ($props: Atom<{ initialValue: number }>) => { 5 | const $counter = $props.map((s) => s.initialValue) 6 | 7 | effect(() => { 8 | console.log('mounted') 9 | return () => console.log('unmounted') 10 | }) 11 | 12 | return { 13 | $counter, 14 | increment: () => $counter.update((s) => s + 1), 15 | decrement: () => $counter.update((s) => s - 1), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/isomorphism/CounterVue.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | /** @jsxImportSource vue */ 3 | import { defineComponent } from 'vue' 4 | import { useAtom, useSetup } from '@xoid/vue' 5 | import { CounterSetup } from './CounterSetup' 6 | 7 | const CounterVue = defineComponent({ 8 | props: { 9 | initialValue: { type: Number, required: true }, 10 | }, 11 | setup(props) { 12 | const { $counter, increment, decrement } = useSetup(CounterSetup, props) 13 | const counter = useAtom($counter) 14 | return () => ( 15 |
16 | count: {counter.value} 17 | 18 | 19 |
20 | ) 21 | }, 22 | }) 23 | 24 | export default CounterVue 25 | -------------------------------------------------------------------------------- /tests/isomorphism/dependency-injection.test.tsx: -------------------------------------------------------------------------------- 1 | import { inject, InjectionKey } from 'xoid' 2 | import { createProvider as createProviderReact, useSetup as useSetupReact } from '@xoid/react' 3 | import { createProvider as createProviderVue, useSetup as useSetupVue } from '@xoid/vue' 4 | import { render as renderReact } from '@testing-library/react' 5 | import { render as renderVue } from '@testing-library/vue' 6 | import React from 'react' 7 | import { defineComponent, h } from 'vue' 8 | 9 | export const StoreKey: InjectionKey = Symbol() 10 | export const StoreSetup = () => inject(StoreKey) 11 | 12 | describe('Same setup, using the same injection key can be used by React and Vue', () => { 13 | it('React', async () => { 14 | const ProviderReact = createProviderReact(StoreKey, 0) 15 | const AppReact = () => { 16 | const num = useSetupReact(StoreSetup) 17 | return
injected: {num}
18 | } 19 | const { findByText } = renderReact( 20 | // @ts-ignore 21 | 22 | 23 | 24 | ) 25 | await findByText('injected: 5') 26 | }) 27 | it('Vue', async () => { 28 | const ProviderVue = createProviderVue(StoreKey, 0) 29 | const AppVue = defineComponent(() => { 30 | const num = useSetupVue(StoreSetup) 31 | return () => h('div', ['injected: ', num]) 32 | }) 33 | const Wrapper = defineComponent(() => { 34 | return () => h(ProviderVue, { value: 5 }, () => [h(AppVue), ' ', h('div', {}, 'other slot')]) 35 | }) 36 | const { findByText } = renderVue(Wrapper) 37 | await findByText('injected: 5') 38 | await findByText('other slot') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/isomorphism/setup-only.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { fireEvent, render as renderReact } from '@testing-library/react' 3 | import { render as renderVue } from '@testing-library/vue' 4 | import { defineComponent, h } from 'vue' 5 | import CounterReact from './CounterReact' 6 | import CounterVue from './CounterVue' 7 | 8 | describe('Same isomorphic setup function works in React and Vue', () => { 9 | const loggerFn = jest.fn() 10 | let consoleLog: any 11 | 12 | beforeEach(() => { 13 | consoleLog = console.log 14 | console.log = loggerFn 15 | }) 16 | 17 | afterEach(() => { 18 | console.log = consoleLog 19 | jest.resetAllMocks() 20 | }) 21 | 22 | test('React', async () => { 23 | const { findByText, getByText, rerender, unmount } = renderReact( 24 | 25 | ) 26 | 27 | expect(loggerFn).toBeCalledTimes(1) 28 | expect(loggerFn).toBeCalledWith('mounted') 29 | 30 | await findByText('count: 5') 31 | fireEvent.click(getByText('+')) 32 | await findByText('count: 6') 33 | 34 | rerender() 35 | 36 | await findByText('count: 25') 37 | fireEvent.click(getByText('-')) 38 | await findByText('count: 24') 39 | 40 | unmount() 41 | expect(loggerFn).toBeCalledTimes(2) 42 | expect(loggerFn).toBeCalledWith('unmounted') 43 | }) 44 | 45 | test('Vue', async () => { 46 | const { findByText, getByText, rerender, unmount } = renderVue( 47 | h(CounterVue, { initialValue: 5 }) 48 | ) 49 | 50 | expect(loggerFn).toBeCalledTimes(1) 51 | expect(loggerFn).toBeCalledWith('mounted') 52 | 53 | await findByText('count: 5') 54 | fireEvent.click(getByText('+')) 55 | await findByText('count: 6') 56 | 57 | rerender({ initialValue: 25 }) 58 | 59 | await findByText('count: 25') 60 | fireEvent.click(getByText('-')) 61 | await findByText('count: 24') 62 | 63 | unmount() 64 | expect(loggerFn).toBeCalledTimes(2) 65 | expect(loggerFn).toBeCalledWith('unmounted') 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/lazy-evaluation.test.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'xoid' 2 | import { debug } from './testHelpers' 3 | 4 | const consoleError = console.error 5 | afterEach(() => { 6 | console.error = consoleError 7 | }) 8 | 9 | it('lazily evaluates a state initializer function', () => { 10 | const fn = jest.fn() 11 | const atom = create(() => { 12 | fn() 13 | return 5 14 | }) 15 | expect(fn).not.toBeCalled() 16 | expect(debug(atom)).toMatchSnapshot() 17 | expect(fn).toBeCalledTimes(1) 18 | }) 19 | 20 | it('lazily evaluates a state initializer function 2', () => { 21 | const fn = jest.fn() 22 | const atom = create(() => { 23 | fn() 24 | return 5 25 | }) 26 | expect(fn).not.toBeCalled() 27 | atom.subscribe(console.log) 28 | expect(fn).toBeCalledTimes(1) 29 | }) 30 | 31 | it('lazily evaluate only when a sub atom is read/written', () => { 32 | const fn = jest.fn() 33 | 34 | const atom = create(() => { 35 | fn() 36 | return { deep: { value: 5 } } 37 | }) 38 | expect(fn).not.toBeCalled() 39 | 40 | const subAtom = atom.focus((s) => s.deep.value) 41 | expect(fn).not.toBeCalled() 42 | 43 | subAtom.set(25) 44 | expect(fn).toBeCalledTimes(1) 45 | }) 46 | 47 | it('lazily evaluate when a mapped atom is read', () => { 48 | const fn = jest.fn() 49 | 50 | const sourceAtom = create({ deep: { value: 5 } }) 51 | const derivedAatom = sourceAtom.map((state) => { 52 | fn() 53 | return state.deep.value 54 | }) 55 | 56 | expect(fn).not.toBeCalled() 57 | 58 | console.log(derivedAatom.value) 59 | 60 | expect(fn).toBeCalledTimes(1) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/manual-testing/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Atom, effect } from 'xoid' 2 | import { useSetup } from '@xoid/react' 3 | 4 | /* eslint-disable @typescript-eslint/no-namespace */ 5 | export namespace WindowEvent { 6 | export type Props = [ 7 | type: T, 8 | listener: (ev: WindowEventMap[T]) => any, 9 | options?: boolean | AddEventListenerOptions 10 | ] 11 | 12 | export const setup = ($props: Atom>) => 13 | effect(() => 14 | $props.watch((args) => { 15 | window.addEventListener(...args) 16 | return () => window.removeEventListener(...args) 17 | }) 18 | ) 19 | } 20 | 21 | export const useWindowEvent = (...props: WindowEvent.Props) => 22 | useSetup(WindowEvent.setup, props) 23 | 24 | function App() { 25 | useSetup(() => { 26 | const callback = () => console.log('event') 27 | 28 | effect(() => { 29 | window.addEventListener('click', callback) 30 | return () => window.removeEventListener('click', callback) 31 | }) 32 | }) 33 | 34 | return ( 35 |
36 |

Hello CodeSandbox

37 |

Start editing to see some magic happen!

38 |
39 | ) 40 | } 41 | 42 | export default App 43 | -------------------------------------------------------------------------------- /tests/manual-testing/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Testing 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/manual-testing/src/index.tsx: -------------------------------------------------------------------------------- 1 | // import { reactive } from '@xoid/reactive' 2 | 3 | // class Anonymous {} 4 | 5 | // const Reactive = new Proxy(Anonymous, { 6 | // construct(a, b) { 7 | // const obj = Object.create(reactive({})) 8 | // return reactive(obj) 9 | // }, 10 | // apply() { 11 | // return 4 12 | // }, 13 | // }) as { 14 | // (): number 15 | // new (): {} 16 | // } 17 | 18 | // class System extends Reactive { 19 | // count = 0 20 | // inc() { 21 | // this.count++ 22 | // } 23 | // } 24 | // const instance = new System() 25 | // console.log(instance) 26 | // console.log(instance.count) 27 | // instance.inc() 28 | // console.log(instance.count) 29 | 30 | import React from 'react' 31 | import ReactDOM from 'react-dom/client' 32 | import App from './App' 33 | 34 | const reactRoot = ReactDOM.createRoot(document.getElementById('root')) 35 | reactRoot.render() 36 | -------------------------------------------------------------------------------- /tests/proxy.test.tsx: -------------------------------------------------------------------------------- 1 | import { toReactive, toAtom } from '@xoid/reactive' 2 | import { create } from 'xoid' 3 | 4 | const consoleError = console.error 5 | afterEach(() => { 6 | console.error = consoleError 7 | }) 8 | const fn = jest.fn() 9 | 10 | const initialState = { deep: { alpha: 5 } } 11 | const $state = create(() => { 12 | fn() 13 | return initialState 14 | }) 15 | const state = toReactive($state) 16 | 17 | it('`toReactive` is able to make immutable updates', () => { 18 | expect(fn).toBeCalledTimes(1) 19 | expect($state.value === initialState) 20 | expect($state.focus('deep').value === initialState.deep) 21 | expect(state.deep.alpha).toBe(5) 22 | state.deep.alpha = 6 23 | expect(state.deep.alpha).toBe(6) 24 | expect($state.value !== initialState) 25 | expect($state.focus('deep').value !== initialState.deep) 26 | }) 27 | 28 | it('`toReactive` is able to make immutable updates in classes', () => { 29 | class System { 30 | alpha = 3 31 | deep = { beta: 3 } 32 | arr = [] 33 | increment() { 34 | this.alpha++ 35 | this.deep.beta++ 36 | } 37 | incOwn = () => { 38 | this.alpha++ 39 | this.deep.beta++ 40 | } 41 | } 42 | const initialInstance = new System() 43 | const $instance = create(initialInstance) 44 | const rootFn = jest.fn() 45 | const fn = jest.fn() 46 | $instance.subscribe(rootFn) 47 | $instance.focus('alpha').subscribe(fn) 48 | const proxy = toReactive($instance) 49 | 50 | expect(fn).toBeCalledTimes(0) 51 | 52 | const expectAll = (num: number) => { 53 | expect(proxy.alpha).toBe(num) 54 | expect(initialInstance.alpha).toBe(3) 55 | expect($instance.value.alpha).toBe(num) 56 | 57 | expect(proxy.deep.beta).toBe(num) 58 | expect(initialInstance.deep.beta).toBe(3) 59 | expect($instance.value.deep.beta).toBe(num) 60 | } 61 | 62 | proxy.increment() 63 | expectAll(4) 64 | expect(fn).toBeCalledTimes(1) 65 | 66 | proxy.increment() 67 | expectAll(5) 68 | expect(fn).toBeCalledTimes(2) 69 | 70 | proxy.arr.push('hi') 71 | expectAll(5) 72 | expect(fn).toBeCalledTimes(2) 73 | 74 | expect(proxy.arr).toEqual(['hi']) 75 | expect(initialInstance.arr).toEqual([]) 76 | expect($instance.value.arr).toEqual(['hi']) 77 | 78 | proxy.arr.push('hello') 79 | expectAll(5) 80 | expect(proxy.arr).toEqual(['hi', 'hello']) 81 | expect(initialInstance.arr).toEqual([]) 82 | expect($instance.value.arr).toEqual(['hi', 'hello']) 83 | }) 84 | 85 | it('`toReactive` caches subproxies properly', () => { 86 | const { deep } = state 87 | expect(state.deep === deep).toBe(true) 88 | }) 89 | 90 | it('`toAtom` retrieves the original atom', () => { 91 | const $deep = toAtom(state.deep) 92 | expect($deep === $state.focus('deep')).toBe(true) 93 | }) 94 | -------------------------------------------------------------------------------- /tests/reactive.test.tsx: -------------------------------------------------------------------------------- 1 | import { reactive, watch, computed } from '@xoid/reactive' 2 | import create from 'xoid' 3 | import { debug } from './testHelpers' 4 | 5 | it('creates a derived atom using the same atom using selectors', () => { 6 | const proxy = reactive({ alpha: 3, beta: 5 }) 7 | const atom = computed(() => proxy.alpha + proxy.beta) 8 | expect(debug(atom)).toMatchSnapshot() 9 | }) 10 | 11 | it('creates a derived atom using the same atom (keeps in sync)', () => { 12 | const proxy = reactive({ alpha: 3, beta: 5 }) 13 | const atom = computed(() => { 14 | return proxy.alpha + proxy.beta 15 | }) 16 | expect(atom.value).toBe(8) 17 | 18 | proxy.alpha++ 19 | expect(atom.value).toBe(9) 20 | }) 21 | 22 | it('creates a derived atom using the same atom using selectors (keeps in sync)', () => { 23 | const proxy = reactive({ alpha: 3, beta: 5 }) 24 | const atom = computed(() => proxy.alpha + proxy.beta) 25 | expect(atom.value).toBe(8) 26 | 27 | proxy.alpha++ 28 | expect(atom.value).toBe(9) 29 | }) 30 | 31 | it('watches a proxy using the same atom using selectors (keeps in sync)', () => { 32 | const proxy = reactive({ alpha: 3, beta: 5 }) 33 | const fn = jest.fn() 34 | const unsub = watch(() => fn(proxy.alpha + proxy.beta)) 35 | expect(fn).toBeCalledWith(8) 36 | 37 | proxy.alpha++ 38 | expect(fn).toBeCalledWith(9) 39 | 40 | expect(fn).toBeCalledTimes(2) 41 | unsub() 42 | 43 | proxy.alpha++ 44 | expect(fn).toBeCalledTimes(2) 45 | }) 46 | 47 | it('is able to watch atoms', () => { 48 | const fn = jest.fn() 49 | const $count = create(0) 50 | 51 | watch(() => fn($count.value)) 52 | expect(fn).toBeCalledTimes(1) 53 | $count.value++ 54 | expect(fn).toBeCalledTimes(2) 55 | expect(fn).toBeCalledWith(1) 56 | }) 57 | 58 | it('is able to create derived state from atoms', () => { 59 | const $count = create(0) 60 | const fn = jest.fn() 61 | 62 | const $doubleCount = computed(() => { 63 | fn() 64 | return $count.value * 2 65 | }) 66 | expect(fn).not.toBeCalled() 67 | expect($doubleCount.value).toBe(0) 68 | $count.value++ 69 | expect($doubleCount.value).toBe(2) 70 | }) 71 | 72 | it("Doesn't accidentally subscribe to dependencies of dependencies", () => { 73 | const $alpha = create(0) 74 | const $beta = create(() => $alpha.value) 75 | const fn = jest.fn() 76 | 77 | const $doubleCount = computed(() => { 78 | fn() 79 | return $beta.value * 2 80 | }) 81 | expect(fn).not.toBeCalled() 82 | expect($doubleCount.value).toBe(0) 83 | $beta.value++ 84 | expect($doubleCount.value).toBe(2) 85 | $alpha.value++ 86 | expect($doubleCount.value).toBe(2) 87 | }) 88 | 89 | it("Doesn't accidentally subscribe to dependencies of dependencies (both)", () => { 90 | const $alpha = create(0) 91 | const $beta = computed(() => $alpha.value) 92 | const $gamma = computed(() => $beta.value) 93 | 94 | expect($beta.value).toBe(0) 95 | expect($gamma.value).toBe(0) 96 | 97 | $alpha.value++ 98 | expect($beta.value).toBe(1) 99 | expect($gamma.value).toBe(1) 100 | 101 | $alpha.value++ 102 | expect($beta.value).toBe(2) 103 | expect($gamma.value).toBe(2) 104 | }) 105 | -------------------------------------------------------------------------------- /tests/stream.test.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'xoid' 2 | 3 | it("don't lazily evaluate when a mapped stream is read", () => { 4 | const fn = jest.fn() 5 | 6 | const sourceAtom = create<{ deep: { value: 5 } }>() 7 | const derivedAatom = sourceAtom.map((state) => { 8 | fn() 9 | return state.deep.value 10 | }) 11 | 12 | expect(fn).not.toBeCalled() 13 | 14 | console.log(derivedAatom.value) 15 | 16 | expect(fn).not.toBeCalled() 17 | }) 18 | 19 | it("don't lazily evaluate when a mapped (twice) stream is read", () => { 20 | const fn = jest.fn() 21 | 22 | const sourceAtom = create<{ deep: { value: 5 } }>().map((s) => s) 23 | const derivedAatom = sourceAtom.map((state) => { 24 | fn() 25 | return state.deep.value 26 | }) 27 | 28 | expect(fn).not.toBeCalled() 29 | 30 | console.log(derivedAatom.value) 31 | 32 | expect(fn).not.toBeCalled() 33 | }) 34 | 35 | it("don't lazily evaluate when a focused stream is read", () => { 36 | const fn = jest.fn() 37 | 38 | const sourceAtom = create<{ deep: { value: 5 } }>().focus('deep') 39 | const derivedAatom = sourceAtom.map((state) => { 40 | fn() 41 | return state.value 42 | }) 43 | 44 | expect(fn).not.toBeCalled() 45 | 46 | console.log(derivedAatom.value) 47 | 48 | expect(fn).not.toBeCalled() 49 | }) 50 | 51 | it('Lazily evaluate only when one of the dependents are read', () => { 52 | const fn = jest.fn() 53 | const fn1 = jest.fn() 54 | 55 | const $alpha = create(() => { 56 | fn() 57 | return { deep: { value: 5 } } 58 | }) 59 | 60 | const $deep = $alpha.map((s) => { 61 | fn1() 62 | return s.deep 63 | }) 64 | const $value = $deep.map((s) => s.value) 65 | 66 | expect(fn).not.toBeCalled() 67 | expect(fn1).not.toBeCalled() 68 | 69 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 70 | $value.value 71 | 72 | expect(fn).toBeCalledTimes(1) 73 | }) 74 | 75 | it('Never evaluate dependents of streams unless the stream value is satisfied', () => { 76 | const fn = jest.fn() 77 | 78 | const $alpha = create<{ deep: { value: number } }>() 79 | 80 | const $deep = $alpha.map((s) => { 81 | fn() 82 | return s.deep 83 | }) 84 | const $value = $deep.map((s) => s.value) 85 | 86 | expect(fn).not.toBeCalled() 87 | 88 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 89 | $value.value 90 | 91 | expect(fn).not.toBeCalled() 92 | 93 | $alpha.set({ deep: { value: 12 } }) 94 | 95 | expect(fn).not.toBeCalled() 96 | 97 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 98 | $value.value 99 | 100 | expect(fn).not.toBeCalled() 101 | 102 | $deep.subscribe(console.log) 103 | 104 | expect(fn).not.toBeCalled() 105 | 106 | $alpha.set({ deep: { value: 15 } }) 107 | 108 | expect(fn).toBeCalledTimes(1) 109 | }) 110 | 111 | it('Continue notifying dependent subscriber after another one is unsubscribed', () => { 112 | const fn = jest.fn() 113 | const fn1 = jest.fn() 114 | 115 | const sourceAtom = create<{ deep: { value: number } }>() 116 | const derivedAtom = sourceAtom.map((state) => state.deep.value) 117 | 118 | derivedAtom.subscribe(fn) 119 | const unsub = derivedAtom.subscribe(fn1) 120 | 121 | sourceAtom.set({ deep: { value: 5 } }) 122 | 123 | expect(fn).toBeCalledTimes(1) 124 | expect(fn1).toBeCalledTimes(1) 125 | 126 | unsub() 127 | 128 | sourceAtom.set({ deep: { value: 6 } }) 129 | 130 | expect(fn).toBeCalledTimes(2) 131 | expect(fn1).toBeCalledTimes(1) 132 | }) 133 | -------------------------------------------------------------------------------- /tests/testHelpers.tsx: -------------------------------------------------------------------------------- 1 | import { Atom } from 'xoid' 2 | 3 | export const debug = (atom: Atom) => { 4 | return { 5 | self: atom, 6 | selfSerialized: JSON.stringify(atom), 7 | get: atom.value, 8 | getSerialized: JSON.stringify(atom.value), 9 | // @ts-expect-error 10 | actions: atom.actions, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "skipLibCheck": true 7 | }, 8 | "include": ["packages"], 9 | "exclude": ["packages/deprecated", "packages/incubator"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "baseUrl": ".", 5 | "paths": { 6 | "xoid": ["./packages/xoid/src"], 7 | "@xoid/devtools": ["./packages/devtools/src/devtools"], 8 | "xoid/*": ["./packages/xoid/src/*"], 9 | "@xoid/*": ["./packages/*/src"] 10 | }, 11 | "target": "ES6", 12 | "jsx": "react-jsx", 13 | "downlevelIteration": true, 14 | "importHelpers": true, 15 | "isolatedModules": true, 16 | "declaration": true, 17 | "declarationDir": "dist/ts-out", 18 | "esModuleInterop": true, 19 | "moduleResolution": "Node" 20 | }, 21 | "include": ["packages", "tests", "examples"], 22 | "exclude": ["packages/deprecated"] 23 | } 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tsconfigPaths from 'vite-tsconfig-paths' 3 | // import macrosPlugin from "vite-plugin-babel-macros" 4 | 5 | export default defineConfig({ 6 | root: './tests/manual-testing/src', 7 | plugins: [ 8 | tsconfigPaths(), 9 | // macrosPlugin(), 10 | ], 11 | }) 12 | --------------------------------------------------------------------------------