├── .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 |
--------------------------------------------------------------------------------