├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── component.ts ├── index.ts └── reactive.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib 4 | *.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨ Reactive Magic ✨ 2 | 3 | A simple library for building reactive applications. Inspired by [Meteor's Tracker](https://docs.meteor.com/api/tracker.html) and [Flyd](https://github.com/paldepind/flyd). 4 | 5 | This library has some very simply yet powerful internals that help you build complex applications quickly. 6 | 7 | ```sh 8 | npm install --save reactive-magic 9 | ``` 10 | 11 | ## Tutorial [[example](https://github.com/ccorcos/reactive-magic-example)] 12 | 13 | Let's create some reactive values: 14 | 15 | ```js 16 | import { Value } from 'reactive-magic' 17 | const x = new Value(1) 18 | const y = new Value(1) 19 | 20 | console.log(y.get()) 21 | // => 1 22 | y.set(2) 23 | console.log(y.get()) 24 | // => 2 25 | ``` 26 | 27 | You can create a `Value` that derives from other `Value`s by passing a function to `DerivedValue`. This function will re-evaluate anytime it's dependent `Value`s change. 28 | 29 | ```js 30 | import { DerivedValue } from 'reactive-magic' 31 | const z = new DerivedValue(() => x.get() + y.get()) 32 | console.log(z.get()) 33 | // => 3 34 | x.set(10) 35 | console.log(z.get()) 36 | // => 12 37 | ``` 38 | 39 | You can also ignore the output of `DerivedValue` if you simply want to do something as a side-effect of a some `Value`s changing: 40 | 41 | 42 | ```js 43 | new DerivedValue(() => console.log(x.get(), y.get(), z.get())) 44 | // => 10 2 12 45 | y(3) 46 | // => 10 3 13 47 | ``` 48 | 49 | Note that you cannot set the value of a derived value (a `Value` created with `DerivedValue`). 50 | 51 | This sets us up for creating a React API that feels very magical. We can create stores and use them wherever and everything will just update seamlessly. 52 | 53 | Here's how you might create a Counter component that has a local store: 54 | 55 | ```js 56 | import React from "react"; 57 | import { Component, Value } from "reactive-magic" 58 | 59 | export default class Counter extends Component { 60 | count = new Value(0) 61 | 62 | increment = () => { 63 | this.count.update(count => count + 1); 64 | }; 65 | 66 | decrement = () => { 67 | this.count.update(count => count - 1); 68 | }; 69 | 70 | view() { 71 | return ( 72 |
73 | 74 | {this.count.get()} 75 | 76 |
77 | ); 78 | } 79 | } 80 | ``` 81 | 82 | The Component API has 4 functions. 83 | 84 | - `willMount(props)` 85 | - `didMount(props)` 86 | - `willUpdate(props)` 87 | - `didUpdate(props)` 88 | - `willUnmount(props)` 89 | - `view(props)` 90 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-magic", 3 | "version": "2.1.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/react": { 8 | "version": "16.0.27", 9 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.27.tgz", 10 | "integrity": "sha512-7lGRcNEI0Iit54ferTJvsNQdY7m97fQmmhFT/3lxpC31b/qshkDoIKTXcVMLteVEza4F+ReEyZIZqSuk4LRb4A==", 11 | "dev": true 12 | }, 13 | "asap": { 14 | "version": "2.0.6", 15 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 16 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 17 | }, 18 | "core-js": { 19 | "version": "1.2.7", 20 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", 21 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" 22 | }, 23 | "encoding": { 24 | "version": "0.1.12", 25 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 26 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 27 | "requires": { 28 | "iconv-lite": "0.4.19" 29 | } 30 | }, 31 | "fbjs": { 32 | "version": "0.8.16", 33 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", 34 | "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", 35 | "requires": { 36 | "core-js": "1.2.7", 37 | "isomorphic-fetch": "2.2.1", 38 | "loose-envify": "1.3.1", 39 | "object-assign": "4.1.1", 40 | "promise": "7.3.1", 41 | "setimmediate": "1.0.5", 42 | "ua-parser-js": "0.7.17" 43 | } 44 | }, 45 | "iconv-lite": { 46 | "version": "0.4.19", 47 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 48 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 49 | }, 50 | "is-stream": { 51 | "version": "1.1.0", 52 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 53 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 54 | }, 55 | "isomorphic-fetch": { 56 | "version": "2.2.1", 57 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 58 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 59 | "requires": { 60 | "node-fetch": "1.7.3", 61 | "whatwg-fetch": "2.0.3" 62 | } 63 | }, 64 | "js-tokens": { 65 | "version": "3.0.2", 66 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 67 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" 68 | }, 69 | "loose-envify": { 70 | "version": "1.3.1", 71 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", 72 | "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", 73 | "requires": { 74 | "js-tokens": "3.0.2" 75 | } 76 | }, 77 | "node-fetch": { 78 | "version": "1.7.3", 79 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 80 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 81 | "requires": { 82 | "encoding": "0.1.12", 83 | "is-stream": "1.1.0" 84 | } 85 | }, 86 | "object-assign": { 87 | "version": "4.1.1", 88 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 89 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 90 | }, 91 | "promise": { 92 | "version": "7.3.1", 93 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 94 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 95 | "requires": { 96 | "asap": "2.0.6" 97 | } 98 | }, 99 | "prop-types": { 100 | "version": "15.6.0", 101 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", 102 | "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", 103 | "requires": { 104 | "fbjs": "0.8.16", 105 | "loose-envify": "1.3.1", 106 | "object-assign": "4.1.1" 107 | } 108 | }, 109 | "react": { 110 | "version": "16.2.0", 111 | "resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz", 112 | "integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==", 113 | "requires": { 114 | "fbjs": "0.8.16", 115 | "loose-envify": "1.3.1", 116 | "object-assign": "4.1.1", 117 | "prop-types": "15.6.0" 118 | } 119 | }, 120 | "setimmediate": { 121 | "version": "1.0.5", 122 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 123 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" 124 | }, 125 | "typescript": { 126 | "version": "2.6.2", 127 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", 128 | "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", 129 | "dev": true 130 | }, 131 | "ua-parser-js": { 132 | "version": "0.7.17", 133 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", 134 | "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" 135 | }, 136 | "whatwg-fetch": { 137 | "version": "2.0.3", 138 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", 139 | "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-magic", 3 | "version": "2.1.5", 4 | "description": "Magical reactivity at your fingertips", 5 | "main": "index.js", 6 | "typings": "index.d.js", 7 | "keywords": [ 8 | "reactive", 9 | "react", 10 | "observable", 11 | "stream" 12 | ], 13 | "author": "Chet Corcos (http://www.chetcorcos.com/)", 14 | "license": "MIT", 15 | "scripts": { 16 | "build": "rm -rf lib && tsc && cp -rf package.json lib", 17 | "watch": "tsc --watch", 18 | "release": "npm run build && cd lib && npm publish" 19 | }, 20 | "dependencies": { 21 | "react": "^16.2.0" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^16.0.27", 25 | "typescript": "^2.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react" 2 | import { DerivedValue } from "./reactive" 3 | 4 | export default class Component

extends PureComponent

{ 5 | _view: DerivedValue 6 | 7 | constructor(props: P) { 8 | super(props) 9 | this._view = new DerivedValue(() => this.view(this.props)) 10 | this._view.dependency.add(this._update) 11 | } 12 | 13 | willMount(props: P) {} 14 | componentWillMount() { 15 | this.willMount(this.props) 16 | } 17 | 18 | didMount(props: P) {} 19 | componentDidMount() { 20 | this.didMount(this.props) 21 | } 22 | 23 | willUpdate(props: P) {} 24 | componentWillUpdate(nextProps: P) { 25 | this._view.stale = true 26 | this.willUpdate(nextProps) 27 | } 28 | 29 | didUpdate(props: P) {} 30 | componentDidUpdate() { 31 | this.didUpdate(this.props) 32 | } 33 | 34 | willUnmount(props: P) {} 35 | componentWillUnmount() { 36 | this.willUnmount(this.props) 37 | this._view.stop() 38 | this._view.dependency.delete(this._update) 39 | } 40 | 41 | _updating = false 42 | _update = () => { 43 | if (!this._updating) { 44 | this._updating = true 45 | setTimeout(() => { 46 | this.forceUpdate() 47 | this._updating = false 48 | }) 49 | } 50 | } 51 | 52 | view(props: P): React.ReactNode { 53 | return null 54 | } 55 | 56 | render(): React.ReactNode { 57 | return this._view.get() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./reactive" 2 | -------------------------------------------------------------------------------- /src/reactive.ts: -------------------------------------------------------------------------------- 1 | export interface Gettable { 2 | get(): V 3 | } 4 | 5 | export interface Settable extends Gettable { 6 | set(v: V): void 7 | } 8 | 9 | // Basic event emitter to keep track of dependencies 10 | export class Dependency { 11 | listeners: Set<() => void> = new Set() 12 | add(listener: () => void) { 13 | this.listeners.add(listener) 14 | } 15 | delete(listener: () => void) { 16 | this.listeners.delete(listener) 17 | } 18 | emit() { 19 | this.listeners.forEach(listener => listener()) 20 | } 21 | } 22 | 23 | // A stack of dependencies that represent every .get() during a computation 24 | const computations: Array> = [] 25 | 26 | // Adds its dependency to the currrent computation on .get() and emits on .set() 27 | export class Value implements Settable { 28 | value: V 29 | dependency = new Dependency() 30 | constructor(value: V) { 31 | this.value = value 32 | } 33 | get(): V { 34 | const computation = computations[0] 35 | computation && computation.add(this.dependency) 36 | return this.value 37 | } 38 | set(value: V): void { 39 | this.value = value 40 | this.dependency.emit() 41 | } 42 | update(fn: (v: V) => V): void { 43 | this.set(fn(this.get())) 44 | } 45 | } 46 | 47 | // A value that is derrived from other values 48 | export class DerivedValue implements Gettable { 49 | value: V 50 | dependency = new Dependency() 51 | computation = new Set() 52 | fn: () => V 53 | stale = true 54 | constructor(fn: () => V) { 55 | this.fn = fn 56 | } 57 | run() { 58 | computations.push(new Set()) 59 | this.value = this.fn() 60 | const computation = computations.shift() 61 | this.stop() 62 | computation.forEach(dep => dep.add(this.onUpdate)) 63 | this.computation = computation 64 | } 65 | onUpdate = () => { 66 | this.stale = true 67 | this.dependency.emit() 68 | } 69 | flush() { 70 | if (this.stale) { 71 | this.stale = false 72 | this.run() 73 | } 74 | } 75 | get(): V { 76 | this.flush() 77 | const deps = computations[0] 78 | deps && deps.add(this.dependency) 79 | return this.value 80 | } 81 | stop() { 82 | this.computation.forEach(dep => dep.delete(this.onUpdate)) 83 | } 84 | } 85 | 86 | // A reactive constraint by providing inverse functions 87 | export class Constraint implements Settable { 88 | value: DerivedValue 89 | setter: (v: V) => void 90 | constructor({ get, set }: { get: () => V; set: (v: V) => void }) { 91 | this.value = new DerivedValue(get) 92 | this.setter = set 93 | } 94 | get(): V { 95 | return this.value.get() 96 | } 97 | set(value: V): void { 98 | this.setter(value) 99 | } 100 | update(fn: (v: V) => V): void { 101 | this.set(fn(this.get())) 102 | } 103 | } 104 | 105 | function equal(x: Set, y: Set) { 106 | if (x === y) { 107 | return true 108 | } 109 | if (x.size !== y.size) { 110 | return false 111 | } 112 | for (var value of Array.from(x)) { 113 | if (!y.has(value)) { 114 | return false 115 | } 116 | } 117 | return true 118 | } 119 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "target": "es5", 6 | "module": "commonjs", 7 | "jsx": "react", 8 | "moduleResolution": "node", 9 | "noImplicitAny": true, 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "removeComments": true, 13 | "strictNullChecks": false, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "lib": ["es2015", "es2016", "dom"], 17 | "outDir": "lib", 18 | "declaration": true 19 | }, 20 | "include": [ 21 | "src/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------