├── .nvmrc ├── packages ├── reactive-core │ ├── .npmignore │ ├── src │ │ ├── index.ts │ │ ├── untracked.ts │ │ ├── action.ts │ │ ├── atom.ts │ │ ├── autorun.ts │ │ ├── observer.ts │ │ ├── reaction.ts │ │ ├── reporting.ts │ │ └── observable.ts │ ├── package.json │ └── test │ │ ├── observable.test.ts │ │ └── autorun.test.ts └── reactive-react │ ├── .npmignore │ ├── src │ ├── index.ts │ └── useReactive.ts │ ├── package.json │ └── test │ ├── sectionMap.test.tsx │ └── useReactive.test.tsx ├── lerna.json ├── prettier.config.js ├── jest.config.js ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── package.json ├── .github └── workflows │ └── build.yaml ├── tsconfig.json ├── LICENSE └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 -------------------------------------------------------------------------------- /packages/reactive-core/.npmignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /packages/reactive-react/.npmignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /packages/reactive-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useReactive"; 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.2.2" 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | $schema: "http://json.schemastore.org/prettierrc", 3 | printWidth: 120, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/reactive-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./autorun"; 2 | export * from "./observable"; 3 | export * from "./observer"; 4 | export * from "./reaction"; 5 | export * from "./untracked"; 6 | export * from "./action"; 7 | export * from "./atom"; 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | roots: [""], 5 | modulePaths: [""], 6 | moduleNameMapper: { 7 | reactive: "/packages/reactive-core/src", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, 5 | "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, 6 | "jest.pathToConfig": "jest.config.js" 7 | } 8 | -------------------------------------------------------------------------------- /packages/reactive-core/src/untracked.ts: -------------------------------------------------------------------------------- 1 | let disableTracking = false; 2 | 3 | export function isTrackingDisabled() { 4 | return disableTracking; 5 | } 6 | 7 | export function untracked(fn: () => void) { 8 | disableTracking = true; 9 | try { 10 | fn(); 11 | } finally { 12 | disableTracking = false; 13 | } 14 | } 15 | 16 | export function untrackedCB(fn: () => void) { 17 | return () => untracked(fn); 18 | } 19 | -------------------------------------------------------------------------------- /packages/reactive-core/src/action.ts: -------------------------------------------------------------------------------- 1 | import { clearBatch } from "./reporting"; 2 | 3 | let runningActionCount = 0; 4 | 5 | export function isActionRunning() { 6 | return runningActionCount > 0; 7 | } 8 | 9 | export function runInAction(func: () => T) { 10 | runningActionCount++; 11 | try { 12 | return func(); 13 | } finally { 14 | runningActionCount--; 15 | 16 | if (runningActionCount === 0) { 17 | clearBatch(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | packages/*/dist 15 | packages/*/types 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | ui-debug.log 28 | firebase-debug.log 29 | .env 30 | .eslintcache 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "license": "MIT", 5 | "scripts": { 6 | "postinstall": "npm run bootstrap", 7 | "bootstrap": "lerna bootstrap", 8 | "test": "jest --coverage=true --config=jest.config.js", 9 | "build": "lerna run build", 10 | "prepublishOnly": "npm run test && npm run build", 11 | "deploy": "lerna publish" 12 | }, 13 | "devDependencies": { 14 | "lerna": "^4.0.0", 15 | "@types/jest": "^26.0.22", 16 | "jest": "^26.6.3", 17 | "microbundle": "^0.13.0", 18 | "ts-jest": "^26.5.4", 19 | "ts-node": "9.1.1", 20 | "typescript": "^4.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | 12 | - name: Setup Node.js 14.x 13 | uses: actions/setup-node@master 14 | with: 15 | node-version: 14.x 16 | 17 | - name: Install Dependencies 18 | run: npm install 19 | 20 | - name: Build packages 21 | run: npm run test 22 | 23 | - name: Upload to coveralls 24 | uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": false, 16 | "noEmit": true, 17 | "jsx": "react", 18 | "experimentalDecorators": true, 19 | "outDir": "types", 20 | "declaration": true, 21 | "baseUrl": ".", 22 | "paths": { 23 | "@reactivedata/reactive": ["packages/reactive-core/src"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/reactive-core/src/atom.ts: -------------------------------------------------------------------------------- 1 | import { Observer } from "./observer"; 2 | import { reactive } from "./observable"; 3 | 4 | // Hacky, it's not really an Atom in the sense that it is not the building block of observables 5 | export class Atom { 6 | private _observable = reactive({ _key: 1 }); 7 | 8 | public reportObserved(implicitObserver?: Observer) { 9 | return (reactive(this._observable, implicitObserver)._key as any) as boolean; 10 | //return (this._observable._key as any) as boolean; 11 | } 12 | 13 | public reportChanged() { 14 | this._observable._key++; 15 | } 16 | } 17 | 18 | export function createAtom(name: string, onBecomeObservedHandler?: () => void, onBecomeUnobservedHandler?: () => void) { 19 | // TODO: add support for params 20 | return new Atom(); 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/test/test.ts", 13 | "outFiles": ["${workspaceFolder}/**/*.js"] 14 | }, 15 | { 16 | "name": "Debug Jest Tests", 17 | "type": "node", 18 | "request": "launch", 19 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"], 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/reactive-core/src/autorun.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "./observable"; 2 | import { Reaction } from "./reaction"; 3 | 4 | export function autorun( 5 | func: () => T extends Promise ? never : T extends void ? T : never, 6 | extraOptions?: { name?: string } 7 | ): Reaction { 8 | const options = { name: "unnamed", fireImmediately: true, ...extraOptions }; 9 | const reaction = new Reaction(func, options); 10 | 11 | return reaction; 12 | } 13 | 14 | export function autorunAsync( 15 | func: (reactive: T) => Promise, 16 | reactiveObject: T, 17 | extraOptions?: { name?: string } 18 | ): Reaction { 19 | const options = { name: "unnamed", fireImmediately: true, ...extraOptions }; 20 | const reaction = new Reaction(() => { 21 | func(reactiveObject); // TODO: error handling 22 | }, options); 23 | reactiveObject = reactive(reactiveObject, reaction); 24 | if (options.fireImmediately) { 25 | reaction.trigger(); 26 | } 27 | return reaction; 28 | } 29 | -------------------------------------------------------------------------------- /packages/reactive-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactivedata/reactive", 3 | "version": "0.2.2", 4 | "private": false, 5 | "main": "dist/reactive.js", 6 | "module": "dist/reactive.module.js", 7 | "umd:main": "dist/reactive.umd.js", 8 | "source": "src/index.ts", 9 | "types": "types/index.d.ts", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "microbundle": "^0.13.0" 13 | }, 14 | "scripts": { 15 | "test": "echo \"Run tests from root\" && exit 1", 16 | "clean": "rm -rf dist && rm -rf types", 17 | "build": "npm run clean && microbundle build --raw --no-compress" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "gitHead": "e009c26b8372a454b66d036f654312a8b9585a72" 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yousef El-Dardiry 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 | -------------------------------------------------------------------------------- /packages/reactive-core/src/observer.ts: -------------------------------------------------------------------------------- 1 | import { $reactive, InternalObservable, ObserverConnection, ObserverConnectionSource } from "./observable"; 2 | 3 | export class Observer { 4 | public observing = new Map< 5 | InternalObservable, 6 | { 7 | iterate: false | true; 8 | byKey: Set; 9 | } 10 | >(); 11 | 12 | constructor(public readonly trigger: () => void) {} 13 | 14 | public registerConnection(source: ObserverConnectionSource) { 15 | let existing = this.observing.get(source.observable); 16 | if (!existing) { 17 | existing = { 18 | byKey: new Set(), 19 | iterate: false, 20 | }; 21 | this.observing.set(source.observable, existing); 22 | } 23 | if (source.type === "iterate") { 24 | existing.iterate = true; 25 | } else { 26 | existing.byKey.add(source.key); 27 | } 28 | } 29 | 30 | public removeObservers() { 31 | this.observing.forEach((val, key) => { 32 | if (val.iterate) { 33 | key[$reactive].connections.iterate.delete(this); 34 | } 35 | val.byKey.forEach((subkey) => { 36 | key[$reactive].connections.byKey.get(subkey).delete(this); 37 | }); 38 | }); 39 | this.observing.clear(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/reactive-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactivedata/react", 3 | "version": "0.2.2", 4 | "private": false, 5 | "main": "dist/reactive-react-bindings.js", 6 | "module": "dist/reactive-react-bindings.module.js", 7 | "umd:main": "dist/reactive-react-bindings.umd.js", 8 | "source": "src/index.ts", 9 | "types": "types/index.d.ts", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@reactivedata/reactive": "^0.2.2" 13 | }, 14 | "peerDependencies": { 15 | "react": "^16.8.0 || ^17 || ^18" 16 | }, 17 | "devDependencies": { 18 | "@testing-library/dom": "^7.30.4", 19 | "@testing-library/jest-dom": "^5.12.0", 20 | "@testing-library/react": "^13.3.0", 21 | "@types/react": "^18.0.15", 22 | "microbundle": "^0.13.0", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0" 25 | }, 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "scripts": { 30 | "test": "echo \"Run tests from root\" && exit 1", 31 | "clean": "rm -rf dist && rm -rf types", 32 | "build": "npm run clean && microbundle build --raw --no-compress && npm run fixtypedir", 33 | "fixtypedir": "mv types typestmp && mv typestmp/reactive-react/src types && rm -rf typestmp" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "gitHead": "e009c26b8372a454b66d036f654312a8b9585a72" 54 | } 55 | -------------------------------------------------------------------------------- /packages/reactive-core/src/reaction.ts: -------------------------------------------------------------------------------- 1 | import { Observer } from "./observer"; 2 | 3 | let runningReactions: Reaction[] = []; 4 | export class Reaction extends Observer { 5 | private isInitial = true; 6 | 7 | constructor( 8 | private func: () => void | Promise, 9 | private options: { fireImmediately: boolean; name: string }, 10 | private effect?: () => void | Promise 11 | ) { 12 | super(() => this._trigger()); 13 | 14 | if (!effect && !this.options.fireImmediately) { 15 | throw new Error("if no effect function passed, should always fireImmediately"); 16 | } 17 | // fire reaction 18 | this.reaction(); 19 | } 20 | 21 | private reaction = () => { 22 | runningReactions.push(this); 23 | 24 | try { 25 | this.func(); 26 | } finally { 27 | runningReactions.pop(); 28 | } 29 | 30 | if (this.effect && (!this.isInitial || this.options.fireImmediately)) { 31 | this.effect(); 32 | } 33 | this.isInitial = false; 34 | }; 35 | 36 | private _trigger() { 37 | // TODO: catch errors 38 | if (runningReactions.includes(this)) { 39 | throw new Error("already running reaction"); 40 | } 41 | this.removeObservers(); 42 | this.reaction(); 43 | } 44 | } 45 | 46 | export function hasRunningReaction() { 47 | return !!runningReactions.length; 48 | } 49 | 50 | export function runningReaction() { 51 | return runningReactions.length ? runningReactions[runningReactions.length - 1] : undefined; 52 | } 53 | 54 | export function reaction( 55 | func: () => any | Promise, 56 | effect: () => void | Promise, 57 | options?: { fireImmediately?: boolean; name?: string } 58 | ) { 59 | const newOptions = { name: "unnamed", fireImmediately: true, ...options }; 60 | const r = new Reaction(func, newOptions, effect); 61 | return r; 62 | } 63 | -------------------------------------------------------------------------------- /packages/reactive-react/src/useReactive.ts: -------------------------------------------------------------------------------- 1 | import { Observer, reactive } from "@reactivedata/reactive"; 2 | import { useEffect, useMemo, useReducer, useRef } from "react"; 3 | 4 | export function useReactive(stateObject: T, deps?: React.DependencyList): T { 5 | const [, forceUpdate] = useReducer((c) => c + 1, 0); 6 | 7 | const observer = useRef(); 8 | const mounted = useRef(false); 9 | if (!observer.current) { 10 | observer.current = new Observer(() => { 11 | if (mounted.current) { 12 | forceUpdate(); 13 | } 14 | }); 15 | } 16 | 17 | const ret = useMemo(() => { 18 | observer.current?.removeObservers(); 19 | return reactive(stateObject, observer.current); 20 | }, deps || []); 21 | 22 | useEffect(() => { 23 | mounted.current = true; 24 | if (!observer.current) { 25 | // our component is reused (strict mode on react 18 also triggers this) 26 | forceUpdate(); 27 | } 28 | return () => { 29 | mounted.current = false; 30 | observer.current?.removeObservers(); 31 | observer.current = null; 32 | }; 33 | }, []); 34 | return ret; 35 | } 36 | 37 | export function useReactives(stateObjects: T, deps?: React.DependencyList): T { 38 | const [, forceUpdate] = useReducer((c) => c + 1, 0); 39 | 40 | const observer = useRef(); 41 | const mounted = useRef(false); 42 | if (!observer.current) { 43 | observer.current = new Observer(() => { 44 | if (mounted.current) { 45 | forceUpdate(); 46 | } 47 | }); 48 | } 49 | 50 | useEffect(() => { 51 | mounted.current = true; 52 | if (!observer.current) { 53 | // our component is reused (strict mode on react 18 also triggers this) 54 | forceUpdate(); 55 | } 56 | return () => { 57 | mounted.current = false; 58 | observer.current?.removeObservers(); 59 | observer.current = null; 60 | }; 61 | }, []); 62 | 63 | return useMemo(() => { 64 | return stateObjects.map((stateObject) => { 65 | return reactive(stateObject, observer.current); 66 | }); 67 | }, deps || []) as T; 68 | } 69 | -------------------------------------------------------------------------------- /packages/reactive-core/src/reporting.ts: -------------------------------------------------------------------------------- 1 | import { isActionRunning } from "./action"; 2 | import { $reactive, ObserverConnectionSource, Operation } from "./observable"; 3 | import { Observer } from "./observer"; 4 | import { runningReaction } from "./reaction"; 5 | import { isTrackingDisabled } from "./untracked"; 6 | 7 | let batch: Operation[] = []; 8 | 9 | export function clearBatch() { 10 | const copy = [...batch]; 11 | batch = []; 12 | reportChangedArray(copy); 13 | } 14 | 15 | function reportChangedArray(operations: Array>) { 16 | // create a copy because 17 | // 1. the set observable[$reactive].connections will be changed while executing reactions (connections will be added / removed) 18 | // 2. de-duplicate reactions (only run reactions once, for example if it's subscribed to both 'get' and 'iterate') 19 | const toRun = new Set(); 20 | 21 | operations.forEach((operation) => { 22 | if (operation.type === "add" || operation.type === "delete") { 23 | operation.observable[$reactive].connections.iterate.forEach((connection) => { 24 | toRun.add(connection); 25 | }); 26 | } 27 | operation.observable[$reactive].connections.byKey.get(operation.key)?.forEach((connection) => { 28 | toRun.add(connection); 29 | }); 30 | }); 31 | 32 | toRun.forEach((observer) => { 33 | observer.trigger(); 34 | }); 35 | } 36 | 37 | export function reportChanged(operation: Operation) { 38 | if (isActionRunning()) { 39 | batch.push(operation); 40 | return; 41 | } 42 | reportChangedArray([operation]); 43 | } 44 | 45 | function addConnection(source: ObserverConnectionSource, observer: Observer) { 46 | if (source.type === "iterate") { 47 | source.observable[$reactive].connections.iterate.add(observer); 48 | } else { 49 | let set = source.observable[$reactive].connections.byKey.get(source.key); 50 | if (!set) { 51 | set = new Set(); 52 | source.observable[$reactive].connections.byKey.set(source.key, set); 53 | } 54 | set.add(observer); 55 | } 56 | } 57 | 58 | export function reportObserved(source: ObserverConnectionSource, implicitObserver: Observer) { 59 | if (isTrackingDisabled()) { 60 | return; 61 | } 62 | 63 | const reaction = runningReaction(); 64 | if (reaction) { 65 | addConnection(source, reaction); 66 | reaction.registerConnection(source); 67 | } 68 | 69 | if (implicitObserver) { 70 | addConnection(source, implicitObserver); 71 | implicitObserver.registerConnection(source); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive 2 | 3 | [![npm version](https://badge.fury.io/js/%40reactivedata%2Freactive.svg)](https://badge.fury.io/js/%40reactivedata%2Freactive) [![Coverage Status](https://coveralls.io/repos/github/YousefED/reactive/badge.svg)](https://coveralls.io/github/YousefED/reactive) 4 | 5 | A super simple, yet powerful and performant library for State Management / Reactive Programming. 6 | 7 | ## Using React 8 | 9 | ```typescript 10 | import { useReactive } from "@reactivedata/react"; 11 | 12 | export default function App() { 13 | const state = useReactive({ 14 | clickCount: 0, 15 | }); 16 | 17 | return ( 18 |
19 |

20 | The button has been clicked {state.clickCount} times! 21 |

22 |
24 | ); 25 | } 26 | ``` 27 | 28 | View on [CodeSandbox](https://codesandbox.io/s/reactivedatareact-basic-example-ihgu9?file=/src/App.tsx) 29 | 30 | Pass in any object to `useReactive` to create a Reactive state. Any properties (even nested / deep properties) can be mutated, and your component will update automatically if it's using any of the changed data. 31 | 32 | _reactive knows that your component uses state.clickCount, and when you click the button, it detects the change to clickCount and figures out which components need to be re-rendered with the new value._ 33 | 34 | ### Advanced example 35 | 36 | ```typescript 37 | const state = useReactive({ 38 | players: [{ name: "Peter" }], 39 | }); 40 | ``` 41 | 42 | Adding players (`state.players.push`) or modifying a name (`state.players[0].name = "John"`) will trigger changes and work out-of-the-box. 43 | 44 | ## Without React 45 | 46 | Reactive is perfectly usable without React, and actually has 0 external dependencies. 47 | 48 | ### Simple example 49 | 50 | ```typescript 51 | import { reactive, autorun } from "@reactivedata/reactive"; 52 | 53 | const data = reactive({ 54 | players: [{ name: "Peter" }], 55 | }); 56 | 57 | autorun(() => { 58 | console.log(`There are ${data.players.length} players, the first player name is ${data.players[0].name}`); 59 | }); 60 | 61 | data.players.push({ name: "Paul" }); 62 | data.players[0].name = "John"; 63 | ``` 64 | 65 | Will print: 66 | 67 | ``` 68 | There are 1 players, the first player name is Peter 69 | There are 2 players, the first player name is Peter 70 | There are 2 players, the first player name is John 71 | ``` 72 | 73 | View on [CodeSandbox](https://codesandbox.io/s/reactivedatareactive-basic-example-b3fs3) 74 | 75 | ## API 76 | 77 | - `reactive` 78 | - `autorun` 79 | - `autorunAsync` 80 | - `untracked` 81 | - `runInAction` 82 | 83 | (to be documented) 84 | 85 | The API surface is inspired by MobX, but with support for `autorunAsync` and an easier React interface. 86 | 87 | ### Credits ❤️ 88 | 89 | Reactive builds on Reactive Programming concepts. In particular, it's inspired by and builds upon the amazing work by [MobX](https://mobx.js.org/) and [NX Observe](https://github.com/nx-js/observer-util). 90 | -------------------------------------------------------------------------------- /packages/reactive-react/test/sectionMap.test.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { act, render, screen } from "@testing-library/react"; 3 | import * as React from "react"; 4 | import { useRef } from "react"; 5 | // @ts-ignore 6 | import { useReactive } from "../src/useReactive"; 7 | 8 | const Section = (props: { section: { title: string; todos: any[] }; smart: boolean }) => { 9 | const renderCount = useRef(0); 10 | renderCount.current++; 11 | 12 | const state = props.smart ? useReactive(props.section) : props.section; 13 | 14 | return ( 15 | <> 16 |
{renderCount.current}
17 | {state.todos.map((t, i) => ( 18 |
TODO: {t.title}
19 | ))} 20 |