├── .circleci
└── config.yml
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── examples
├── renderProfile
│ ├── index.html
│ ├── index.tsx
│ └── tsconfig.json
└── todoList
│ ├── atoms
│ └── index.ts
│ ├── index.html
│ ├── index.tsx
│ ├── src
│ ├── AddTodo.tsx
│ ├── App.tsx
│ ├── Filters.tsx
│ └── TodoList.tsx
│ └── tsconfig.json
├── jest.config.js
├── package.json
├── rollup.config.js
├── src
├── core
│ ├── __tests__
│ │ ├── makeAtom.spec.ts
│ │ ├── makeFamily.spec.ts
│ │ └── makeMolecule.spec.ts
│ ├── makeAtom.ts
│ ├── makeFamily.ts
│ └── makeMolecule.ts
├── hooks
│ ├── __tests__
│ │ ├── useEntangle.spec.tsx
│ │ ├── useReadEntangle.spec.tsx
│ │ └── useSetEntangle.spec.tsx
│ ├── useEntangle.ts
│ ├── useReadEntangle.ts
│ └── useSetEntangle.ts
├── index.ts
├── types.ts
└── utils
│ ├── __tests__
│ └── utils.spec.ts
│ └── utils.ts
├── tsconfig.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | jobs:
3 | build:
4 | docker:
5 | - image: node:14.14
6 | working_directory: ~/repo
7 | steps:
8 | - checkout
9 | - restore_cache:
10 | keys:
11 | - dep-{{ checksum "package.json" }}
12 | - dep-
13 | - run:
14 | name: Install Node Modules
15 | command: yarn
16 |
17 | - save_cache:
18 | paths:
19 | - node_modules
20 | key: dep-{{ checksum "package.json" }}
21 |
22 | - run:
23 | name: ESLint check
24 | command: yarn run lint
25 | when: on_success
26 |
27 | - run:
28 | name: Run Jest tests
29 | command: yarn run test --runInBand
30 | when: on_success
31 | no_output_timeout: 20m
32 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser
3 | parserOptions: {
4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
5 | sourceType: "module", // Allows for the use of imports
6 | ecmaFeatures: {
7 | jsx: true, // Allows for the parsing of JSX
8 | },
9 | },
10 | settings: {
11 | react: {
12 | version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use
13 | },
14 | },
15 | extends: [
16 | "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
17 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
18 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
19 | "plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
20 | ],
21 | rules: {
22 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
23 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | */**/node_modules/
43 | jspm_packages/
44 |
45 | # TypeScript v1 declaration files
46 | typings/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "printWidth": 130,
4 | "trailingComma": "es5",
5 | "jsxBracketSameLine": false,
6 | "tabWidth": 4,
7 | "semi": true,
8 | "endOfLine":"auto"}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 bennyg123
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Entangle
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Global state management tool for react hooks inspired by [RecoilJS](https://github.com/facebookexperimental/Recoil) and [Jotai](https://github.com/pmndrs/jotai) using proxies.
17 |
18 | ## Features
19 |
20 | - No need for context
21 | - Zero dependencies
22 | - Super lightweight: [~ 1kb gzipped](https://bundlephobia.com/result?p=@bennyg_123/entangle)
23 |
24 | ## Table of Contents
25 | - [Intro](#intro)
26 | - [Getting Started](#getting-started)
27 | - [API](#api)
28 | - [`makeAtom`](#make-atom)
29 | - [`makeMolecule`](#make-molecule)
30 | - [`makeAsyncMolecule`](#make-async-molecule)
31 | - [`makeAtomEffect`](#make-atom-effect)
32 | - [Hooks](#hooks)
33 | - [`useEntangle`](#use-entangle)
34 | - [`useMultiEntangle`](#use-multi-entangle)
35 | - [`useReadEntangle`](#use-read-entangle)
36 | - [`useMultiReadEntangle`](#use-multi-read-entangle)
37 | - [`useSetEntangle`](#use-set-entangle)
38 | - [`useMultiSetEntangle`](#use-multi-set-entangle)
39 | - [Advance API](#advance-api)
40 | - [`makeAtomEffectSnapshot`](#make-atom-effect-snapshot)
41 | - [`makeAtomFamily`](#make-atom-family)
42 | - [`makeMoleculeFamily`](#make-molecule-family)
43 | - [`makeAsyncMoleculeFamily`](#make-async-molecule-family)
44 | - [`Debounce`](#debounce-molecules-effects)
45 | - [Develop](#develop)
46 | - [Footnotes](#footnotes)
47 |
48 | ## Intro
49 |
50 | Inspired by [RecoilJS](https://github.com/facebookexperimental/Recoil) with its 3D state management where the state does not live with the virtual dom tree, I wanted to create a simpler and much more lightweight version for modern browsers (IE is dead!!). The current state management solutions in the react ecosystem work really well (mobx, redux, etc), but I think a recoil like library that allows for granular updates without context and without having to rerender the whole DOM tree is the future. Thus Entangle was born. The name Entangle comes from quantum entanglement where two Atoms are linked event across great distances and can affect each other.
51 |
52 | This library is written in TS and has typings shipped with it.
53 |
54 | This library should work with all browsers that support [proxies](https://caniuse.com/?search=Proxy) (aka all modern browsers). However if you need to support other browsers there is a [polyfill](https://github.com/GoogleChrome/proxy-polyfill) available, though that wont be officially supported by this library.
55 |
56 | Please try this library out and let me know if you encounter any bugs and suggestions on improvements. Its still very much in the experimental and testing phase so try at your own risk.
57 |
58 |
59 |
60 |
61 |
62 | ## Getting Started
63 |
64 | Super simple example with makeAtom
65 |
66 | ```jsx
67 | import { makeAtom, useEntangle } from "@bennyg_123/entangle";
68 |
69 | const atomValue = makeAtom("Hello");
70 |
71 | const Component1 = () => {
72 | const [atomState, setAtomState] = useEntangle(atomValue);
73 |
74 | return (
75 |
76 | setAtomState("Hello, 世界")}>Update atomState
77 |
{atomState}
78 |
79 | );
80 | }
81 |
82 | const Component2 = () => {
83 | const [atomState, setAtomState] = useEntangle(atomValue);
84 |
85 | return (
86 |
87 | setAtomState("Hello World")}>Update atomState
88 |
{atomState}
89 |
90 | );
91 | }
92 | ```
93 |
94 | In the above example, a global `atomValue` is created with the initial value passed in. Then the components that need to access that value will pass in the `atomValue` to a `useEntangle` hook inside the component.
95 |
96 | The `useEntangle` hook works the same way as a `useState` hook, the first value is the value, while the second is an updater function. If either of the buttons are clicked and they update the `atomState`, then both components (and only those components and their children) will rerender, staying in sync. Most importantly the parents will not rerender.
97 |
98 | ```tsx
99 | import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
100 |
101 | const atomValue = makeAtom("Hello");
102 | const moleculeValue = makeMolecule((get) => get(atomValue) + " world");
103 |
104 | const Component = () => {
105 | const [atomState] = useEntangle(moleculeValue);
106 |
107 | return (
108 |
109 |
{atomState}
110 |
111 | );
112 | }
113 | ```
114 |
115 | Entangle also supports composition using atoms as well. You can pass a function to `makeMolecule` that takes a get method and composes the composed value using `get` to get the atom's current value and subscribe to those changes.
116 |
117 | ```tsx
118 | import { makeAtom, makeAsyncMolecule, useEntangle } from "@bennyg_123/entangle";
119 |
120 | const atomValue = makeAtom("Hello");
121 | const asyncMoleculeValue = makeAsyncMolecule(async (get) => {
122 | const response = await fetch(`API/${get(atomValue)}`);
123 | const value = await response.json(); // { value: "Hello World" }
124 | return value;
125 | }, {
126 | value: "Default"
127 | }});
128 |
129 | const Component = () => {
130 | const [atomState] = useEntangle(asyncMoleculeValue);
131 |
132 | return (
133 |
134 |
{atomState}
135 |
136 | );
137 | }
138 | ```
139 |
140 | Entangle also supports async molecules as well with the `makeAsyncMolecule` method. You can do API calls using atom values here, and they will automatically update and subscribe to those atom changes. The value of the second parameter must match the return value of the async generator function passed in.
141 |
142 | For example the below example wont work since you passed in a string for a default value but the async function returns an object.
143 | ```tsx
144 | import { makeAtom, makeAsyncMolecule, useEntangle } from "@bennyg_123/entangle";
145 |
146 | const atomValue = makeAtom("Hello");
147 | const asyncMoleculeValue = makeAsyncMolecule(async (get) => {
148 | const response = await fetch(`API/${get(atomValue)}`);
149 | const {value} = await response.json(); // { value: "Hello World" }
150 | return { response: value };
151 | }, "HELLO WORLD");
152 |
153 | const Component = () => {
154 | const [atomState] = useEntangle(asyncMoleculeValue);
155 |
156 | return (
157 |
158 |
{atomState}
159 |
160 | );
161 | }
162 | ```
163 |
164 | For this reason it is better to add explicit types (if you are using TS) to the make methods:
165 |
166 | ```tsx
167 | import { makeAtom, makeMolecule, makeAsyncMolecule, useEntangle } from "@bennyg_123/entangle";
168 |
169 | const atomValue = makeAtom("1");
170 | const moleculeValue = makeMolecule((get) => parseInt(get(atomValue)));
171 | const atomValue = makeAsyncMolecule<{value: string}>(async (get) => ({value: get(atomValue)}));
172 |
173 | ```
174 |
175 |
176 | ## API
177 | makeAtom
178 |
179 | makeAtom creates an atom value to be used inside the useEntangle hook. All components using this value will be synced and updated when any of the components update the atom. It does not matter how deeply nested.
180 |
181 | ```jsx
182 | import { makeAtom, useEntangle } from "@bennyg_123/entangle";
183 |
184 | const atomValue = makeAtom("Hello");
185 |
186 | const Component = () => {
187 | const [atomState, setAtomState] = useEntangle(atomValue);
188 |
189 | return (
190 |
191 | setAtomState("Hello, 世界")}>Update atomState
192 |
{atomState}
193 |
194 | );
195 | }
196 | ```
197 |
198 | makeMolecule
199 |
200 | makeMolecule allows for subscriptions to an atoms changes for composing values based off other atoms.
201 |
202 | ```jsx
203 | import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
204 |
205 | const atomValue = makeAtom("Hello");
206 | const atomValue2 = makeAtom("world");
207 |
208 | // In the below example, you can pass in am optional boolean as a second argument to the getter, this will un subscribe the molecule from that atoms changes
209 | const moleculeValue = makeMolecule((get) => get(atomValue) + get(atomValue2, false));
210 |
211 | const Component = () => {
212 | const [atomState, setAtomState] = useEntangle(atomValue);
213 | const [moleculeState] = useEntangle(moleculeValue);
214 |
215 | return (
216 |
217 | setAtomState("Hello, 世界")}>Update atomState
218 |
{atomState}
219 | {moleculeState}
220 |
221 | );
222 | }
223 | ```
224 | ***It is important to note that since molecules are dependent on atoms. They are read only, thus while they can be used with `useEntangle`, calling the set function will throw an error. As a result they should be used with `useReadEntangle`***
225 |
226 | ```jsx
227 | import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
228 |
229 | const atomValue = makeAtom("Hello");
230 | const moleculeValue = makeMolecule((get) => get(atomValue) + " world");
231 |
232 | const Component = () => {
233 | const [atomState, setAtomState] = useEntangle(atomValue);
234 | const [moleculeState, setMoleculeState] = useEntangle(moleculeValue); // not recommended
235 | const readOnlyMoleculeState = useReadEntangle(moleculeValue);
236 |
237 | return (
238 |
239 | setAtomState("Hello, 世界")}>Update atomState
240 | setMoleculeState("Hello, 世界")}>Throws an error
241 |
{atomState}
242 | {moleculeState}
243 | {readOnlyMoleculeState}
244 |
245 | );
246 | }
247 | ```
248 |
249 | makeAsyncMolecule
250 |
251 | Same usage as `makeMolecule` except you pass in an async function and a default value as the second argument.
252 |
253 | ```jsx
254 | import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
255 |
256 | const atomValue = makeAtom("Hello");
257 | const moleculeValue = makeMolecule(async (get) => get(atomValue) + " world", "defaultValue");
258 |
259 | const Component = () => {
260 | const [atomState, setAtomState] = useEntangle(atomValue);
261 | const [moleculeState] = useEntangle(moleculeValue);
262 |
263 | return (
264 |
265 | setAtomState("Hello, 世界")}>Update atomState
266 |
{atomState}
267 | {moleculeState}
268 |
269 | );
270 | }
271 | ```
272 |
273 | makeAtomEffect
274 |
275 | Sometimes we want to do side effects that update other atoms outside of a component, thats where `makeAtomEffect` comes in handy.
276 |
277 | You pass it a function that has a getter and setter passed to it and in it you can get and set atoms, be aware of infinite loops though as the
278 | `makeAtomEffect` subscribes to all the getters it uses/calls
279 |
280 | ```ts
281 | import { makeAtom, makeAtomEffect } from "@bennyg_123/entangle";
282 |
283 | const atomValue1 = makeAtom("Hello");
284 | const atomValue2 = makeAtom(" World");
285 |
286 | const combinedValue = makeAtom("");
287 |
288 | makeAtomEffect((get, set) => {
289 | const value1 = get(atomValue);
290 | // Similar to get molecule, for the getter function, if you pass in a false boolean as the second parameter, it will not subscribe to the atoms changes
291 | const value2 = get(atomValue, false);
292 | set(combinedValue, value1 + value2);
293 | })
294 | ```
295 |
296 | ### Hooks
297 |
298 | useEntangle
299 |
300 | `useEntangle` entangles the atoms together with the components and syncs them. The API is the same as `useState` and whenever an atom is updated, all other components that has `useEntangle` with that atom value or has `useEntangle` with a molecule that is composed with that atom value will get updated.
301 |
302 | ***if a molecule is passed in, calling the set function will throw an error. Thus it is advised to use molecules with `useReadEntangle` instead. ***
303 |
304 | ```jsx
305 | import { makeAtom, useEntangle } from "@bennyg_123/entangle";
306 |
307 | const atomValue = makeAtom("Hello");
308 |
309 | const Component = () => {
310 | const [atomState, setAtomState] = useEntangle(atomValue);
311 |
312 | return (
313 |
314 | setAtomState("Hello, 世界")}>Update atomState
315 |
{atomState}
316 |
317 | );
318 | }
319 | ```
320 |
321 | useMultiEntangle
322 |
323 | For reading and setting multiple atoms, you can use `useMultiEntangle` with multiple atoms. You pass in a list of atoms as arguments and it'll return getters and setters in that order.
324 |
325 | ***Unfortunately due to my own limited knowledge with advanced ts, I was unable to make typing work with the `useMulti` hooks. I am actively looking for a way for TS to infer the types of atoms passed in and evaluate typings, but any help would be greatly appreciated.***
326 |
327 | ```tsx
328 |
329 | import { makeAtom, useMultiEntangle } from "@bennyg_123/entangle";
330 |
331 | const atom1 = makeAtom("Hello");
332 | const atom2 = makeAtom("World");
333 | const atom3 = makeAtom1("!!!");
334 |
335 | const Component = () => {
336 | const [
337 | [atom1Value, atom2Value, atom3Value],
338 | [setAtom1, setAtom2, setAtom3]
339 | ] = useMultiEntangle(atom1, atom2, atom3);
340 |
341 | ...
342 | }
343 | ```
344 |
345 | useReadEntangle
346 |
347 |
348 | Sometimes in our components we don't want to allow for updates to an atom and only want to consume the values, thats where useReadEntangle comes in handy.
349 | It only returns a read only value and lets the component subscribe to the atom changes.
350 |
351 | ```tsx
352 | import { makeAtom, useReadEntangle } from "@bennyg_123/entangle";
353 |
354 | const atom1 = makeAtom("Hello");
355 |
356 | const Component = () => {
357 | const value = useReadEntangle(atom1);
358 |
359 | return (
360 | {value}
361 | )
362 | }
363 | ```
364 |
365 | useMultiReadEntangle
366 |
367 | When one needs to read from multiple atoms and stay subscribed use `useMultiReadEntangle`. Pass in multiple atoms and get the values back in an array.
368 |
369 | ```tsx
370 |
371 | import { makeAtom, useMultiReadEntangle } from "@bennyg_123/entangle";
372 |
373 | const atom1 = makeAtom("Hello");
374 | const atom2 = makeAtom("World");
375 | const atom3 = makeAtom1("!!!");
376 |
377 | const Component = () => {
378 | const [atom1Value, atom2Value, atom3Value] = useMultiReadEntangle(atom1, atom2, atom3);
379 |
380 | ...
381 | }
382 | ```
383 |
384 | useSetEntangle
385 |
386 | Sometimes a component only needs to set an atom's value and not subscribe to those changes, as a result useSetEntangle will only return a function that'll set an atoms value and update other components subscribed but not the current component. ***useSetEntangle will not take in a molecule***
387 |
388 | ```tsx
389 | import { makeAtom, useSetEntangle } from "@bennyg_123/entangle";
390 |
391 | const atom1 = makeAtom("Hello");
392 |
393 | const Component = () => {
394 | const setValue = useSetEntangle(atom1);
395 | // Is not subscribed to ATOM changes
396 |
397 | return (
398 | setValue("World")}>Update Atom
399 | )
400 | }
401 | ```
402 |
403 | useMultiSetEntangle
404 |
405 | When one needs to set multiple atoms use `useMultiSetEntangle`. Pass in multiple atoms and get the setters back in an array.
406 |
407 | ```tsx
408 |
409 | import { makeAtom, useMultiSetEntangle } from "@bennyg_123/entangle";
410 |
411 | const atom1 = makeAtom("Hello");
412 | const atom2 = makeAtom("World");
413 | const atom3 = makeAtom1("!!!");
414 |
415 | const Component = () => {
416 | const [setAtom1, setAtom2, setAtom3] = useMultiSetEntangle(atom1, atom2, atom3);
417 |
418 | ...
419 | }
420 | ```
421 |
422 | ## Advance API
423 |
424 | makeAtomEffectSnapshot
425 |
426 | For certain situations it might advantageous to manually call a side effect function without having it subscribe to atom changes. For this `makeAtomEffectSnapshot` can be used.
427 |
428 | `makeAtomEffectSnapshot` takes in a function or async function exactly like makeAtomEffect, with a getter and setter parameter and returns a function that can be called with arguments when the developer wants the side effect function to be run.
429 |
430 | ```tsx
431 | import { makeAtom, makeAtomEffectSnapshot } from "@bennyg_123/entangle";
432 |
433 | const atom1 = makeAtom("Hello");
434 | const snapshotFN = makeAtomEffectSnapshot(async (get, arg1) => {
435 | writeToDB(get(atom1) + arg1);
436 | });
437 |
438 | const Component = () => {
439 | useEffect(() => {
440 | snapshotFN("ARG")
441 | }, [])
442 | // Is not subscribed to ATOM changes
443 |
444 | return (<>>)
445 | }
446 | ```
447 |
448 | makeAtomFamily
449 |
450 | When we need to have a array or set of atoms, makeAtomFamily can help. It is an atom generator that takes either an initial value or function that returns an initial value, and outputs a helper function to generate atoms on the fly.
451 |
452 | You can pass in values as arguments for initialization, and then use it the exact same as a regular atom. The first argument must be a string as this acts as a key to differentiate an atom from each other, thus if one component updates an atom, then the other components using an atomFamily wont get updated. This also allows atoms in families to be shared if they use the same key.
453 |
454 | ```tsx
455 | import { makeAtomFamily } from "@bennyg_123/entangle";
456 |
457 | const atomFamily = makeAtomFamily("Hello");
458 |
459 | const Component1 = () => {
460 | const setValue = useEntangle(atomFamily("A"));
461 |
462 | // Component1 will not update Component2
463 | return (
464 | setValue("World")}>Update Atom
465 | )
466 | }
467 |
468 | const Component2 = () => {
469 | const setValue = useEntangle(atomFamily("B"));
470 |
471 | // Component2 will not update Component1
472 | return (
473 | setValue("World")}>Update Atom
474 | )
475 | }
476 |
477 | const Component3 = () => {
478 | const setValue = useEntangle(atomFamily("A"));
479 |
480 | // Component1 will update Component3
481 | return (
482 | setValue("World")}>Update Atom
483 | )
484 | }
485 | ```
486 |
487 | ```tsx
488 | import { makeAtomFamily } from "@bennyg_123/entangle";
489 |
490 | // First argument is always a string that acts as a key to differentiate atoms
491 | const atomFamily = makeAtomFamily((arg1: string, arg2) => parseInt(arg1) + arg2);
492 |
493 | const Component = () => {
494 | const setValue = useEntangle(atomFamily("1", 2));
495 |
496 | return (
497 | // All subsequent sets to the atom should be set like a regular atom and not via the function
498 | setValue(32)}>Update Atom
499 | )
500 | }
501 |
502 | const Component2 = () => {
503 | const setValue = useEntangle(atomFamily("3", 4));
504 |
505 | return (
506 | setValue(24)}>Update Atom
507 | )
508 | }
509 | ```
510 |
511 | makeMoleculeFamily
512 |
513 | Same as makeAtomFamily but instead of instantiating atoms, it instantiates molecules. The initializer function has a getter function (same as makeMolecule), a key, and arguments passed in. The return function subsequently takes a unique string key and any additional arguments that need to be passed to the molecule function.
514 |
515 | ```tsx
516 | import { makeAtom, makeMoleculeFamily } from "@bennyg_123/entangle";
517 |
518 | const atom = makeAtom("Hello");
519 | const moleculeFamily = makeMoleculeFamily((get, key, arg1) => `${get(atom)} ${key} ${arg1}`);
520 |
521 | const Component1 = () => {
522 | const value = useReadEntangle(moleculeFamily("A", 123));
523 |
524 | // will render `Hello A 123`
525 | return (
526 | {value}
527 | )
528 | }
529 | ```
530 |
531 | makeAsyncMoleculeFamily
532 |
533 | Same as makeMoleculeFamily except this takes in an async function and also takes either an initial value or a synchronous function to generate an initial value for the async molecules.
534 |
535 | ```tsx
536 | import { makeAtom, makeAsyncMoleculeFamily } from "@bennyg_123/entangle";
537 |
538 | const atom = makeAtom("Hello");
539 | const asyncMoleculeFamily = makeAsyncMoleculeFamily(async (get, key, arg1) => {
540 | value = await db.get(); // returns ABCD
541 | `${get(atom)} ${key} ${arg1} ${value}`
542 | }, "Loading");
543 |
544 | const Component1 = () => {
545 | const value = useReadEntangle(asyncMoleculeFamily("A", 123));
546 |
547 | // will render `Loading` at first then `Hello A 123 ABCD` when the db call is done
548 | return (
549 | {value}
550 | )
551 | }
552 | ```
553 |
554 | ```tsx
555 | import { makeAtom, makeAsyncMoleculeFamily } from "@bennyg_123/entangle";
556 |
557 | const atom = makeAtom("Hello");
558 | const asyncMoleculeFamily = makeAsyncMoleculeFamily(async (get, key, arg1) => {
559 | value = await db.get(); // returns ABCD
560 | `${get(atom)} ${key} ${arg1} ${value}`
561 | }, (get, key, arg1) => `Loading ${key} ${arg1}`);
562 |
563 | const Component1 = () => {
564 | const value = useReadEntangle(asyncMoleculeFamily("A", 123));
565 |
566 | // will render `Loading A 123` at first then `Hello A 123 ABCD` when the db call is done
567 | return (
568 | {value}
569 | )
570 | }
571 | ```
572 |
573 | Debounce
574 |
575 | Since the `makeAtomEffect`, `makeAsyncMolecule` and `makeMolecule` functions run automatically, sometimes when a large amount of changes are made at the same, this could result in running expensive functions multiple times. Thus there is the option to debounce these functions by waiting a set time before running them. This can be achieved by passing a time in ms as the last argument when initializing an Atom Effect or a Molecule.
576 |
577 | ```tsx
578 | // This effect will debounce and be run 500 ms after receiving the last atom update. If an atom is updated before 500ms then the debounce timer is reset and the effect wont run.
579 | makeAtomEffect((get, set) => { ... }, 500);
580 |
581 | // Equivalent debounce usage for molecules and async molecules
582 | makeMolecule((get, set) => { ... }, 500);
583 | makeAsyncMolecule((get, set) => { ... }, {}, 500)
584 | ```
585 |
586 | ## Develop
587 |
588 | To develop, you can fork this repo.
589 |
590 | To build:
591 | ```
592 | yarn && yarn run build
593 | ```
594 |
595 | To run test:
596 | ```
597 | yarn && yarn run test
598 | ```
599 |
600 | To run lint:
601 | ```
602 | yarn && yarn run lint
603 | ```
604 |
605 | To run example page:
606 | ```
607 | yarn && yarn run example
608 | ```
609 | ## Footnotes
610 |
611 | Thank you so much for trying this library out. Please leave feedback in the issues section. Have fun.
612 |
--------------------------------------------------------------------------------
/examples/renderProfile/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/renderProfile/index.tsx:
--------------------------------------------------------------------------------
1 | import "regenerator-runtime/runtime";
2 | import React from "react";
3 | import ReactDom from "react-dom";
4 | import { makeAsyncMolecule, makeAtom, makeMolecule, useEntangle } from "../../src/index";
5 |
6 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
7 |
8 | const parentAtomValue = makeAtom(1);
9 | const childAtomValue = makeAtom("I am a child");
10 | const moleculeChildAtomValue = makeMolecule((get) => get(childAtomValue) + " and I am a molecule");
11 | const asyncChildAtomValue = makeAsyncMolecule(async (get) => {
12 | await sleep(1500);
13 | const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${get(parentAtomValue)}`);
14 | const value = await response.json();
15 | return value;
16 | }, "LOADING");
17 |
18 | const AsyncChild = () => {
19 | const [asyncChildState] = useEntangle(asyncChildAtomValue);
20 |
21 | return (
22 | <>
23 | AsyncChild state is : {JSON.stringify(asyncChildState)}
24 | >
25 | );
26 | };
27 |
28 | const MoleculeChild = () => {
29 | const [moleculeChildState] = useEntangle(moleculeChildAtomValue);
30 |
31 | return (
32 | <>
33 | Child state is : {moleculeChildState}
34 | >
35 | );
36 | };
37 |
38 | const Child = () => {
39 | const [childState] = useEntangle(childAtomValue);
40 |
41 | return (
42 | <>
43 | Child state is : {childState}
44 |
45 | >
46 | );
47 | };
48 |
49 | const Parent = () => {
50 | const [parentState] = useEntangle(parentAtomValue);
51 |
52 | return (
53 | <>
54 | Parent state is : {parentState}
55 |
56 |
57 |
58 |
59 |
60 | >
61 | );
62 | };
63 |
64 | const Controls = () => {
65 | const [parentValue, setParentState] = useEntangle(parentAtomValue);
66 | const [, setChildState] = useEntangle(childAtomValue);
67 |
68 | return (
69 | <>
70 | setParentState(parentValue + 1)}>Update Parent
71 | setChildState("I am a cool child")}>Update Child
72 | >
73 | );
74 | };
75 |
76 | const App = () => (
77 |
82 | );
83 |
84 | ReactDom.render( , document.getElementById("root"));
85 |
--------------------------------------------------------------------------------
/examples/renderProfile/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "esModuleInterop": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/examples/todoList/atoms/index.ts:
--------------------------------------------------------------------------------
1 | import { makeAtom, makeMolecule, makeAtomEffectSnapshot } from "../../../src/index";
2 |
3 | const LIST = makeAtom<{ id: number; title: string; completed: boolean }[]>([]);
4 |
5 | export const SELECTED_FILTER = makeAtom<"COMPLETED" | "UNCOMPLETED" | "ALL">("ALL");
6 |
7 | export const FILTERED_LIST = makeMolecule((get) => {
8 | const filter = get(SELECTED_FILTER);
9 | const list = get(LIST);
10 |
11 | switch (filter) {
12 | case "ALL":
13 | return list;
14 | case "COMPLETED":
15 | return list.filter(({ completed }) => completed === true);
16 | case "UNCOMPLETED":
17 | return list.filter(({ completed }) => completed === false);
18 | }
19 | });
20 |
21 | export const ADD_TO_LIST = makeAtomEffectSnapshot((get, set, title: string) => {
22 | const list = get(LIST);
23 | let i = 0;
24 |
25 | while (list.find(({ id }) => id === i) !== undefined) {
26 | i++;
27 | }
28 |
29 | set(LIST, [...list, { id: i, completed: false, title }]);
30 | });
31 |
32 | export const TOGGLE_COMPLETED = makeAtomEffectSnapshot((get, set, id: number) => {
33 | const list = get(LIST);
34 |
35 | const item = list.find(({ id: _id }) => _id === id);
36 | item.completed = !item.completed;
37 |
38 | set(LIST, [...list]);
39 | });
40 |
--------------------------------------------------------------------------------
/examples/todoList/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/todoList/index.tsx:
--------------------------------------------------------------------------------
1 | import "regenerator-runtime/runtime";
2 | import React from "react";
3 | import ReactDom from "react-dom";
4 | import App from "./src/App";
5 |
6 | ReactDom.render( , document.getElementById("root"));
7 |
--------------------------------------------------------------------------------
/examples/todoList/src/AddTodo.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import { ADD_TO_LIST } from "../atoms";
3 |
4 | const AddTodo = () => {
5 | const todoRef = useRef(null);
6 |
7 | const handleAddTodo = () => {
8 | if (todoRef.current?.value) {
9 | ADD_TO_LIST(todoRef.current.value);
10 | }
11 | };
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Add Todo
20 |
21 |
22 | );
23 | };
24 |
25 | export default AddTodo;
26 |
--------------------------------------------------------------------------------
/examples/todoList/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AddTodo from "./AddTodo";
3 | import Filters from "./Filters";
4 | import TodoList from "./TodoList";
5 |
6 | const App = () => (
7 |
12 | );
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/examples/todoList/src/Filters.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useEntangle } from "../../../src";
3 | import { SELECTED_FILTER } from "../atoms";
4 |
5 | const Filters = () => {
6 | const [selectedFilter, setSelectedFilter] = useEntangle(SELECTED_FILTER);
7 |
8 | const updateSelectedFilter = (e) => {
9 | if (e.target.value) {
10 | setSelectedFilter(e.target.value);
11 | }
12 | };
13 |
14 | return (
15 |
16 | {["ALL", "COMPLETED", "UNCOMPLETED"].map((filter) => (
17 |
18 |
25 | {filter.toLowerCase()}
26 |
27 | ))}
28 |
29 | );
30 | };
31 |
32 | export default Filters;
33 |
--------------------------------------------------------------------------------
/examples/todoList/src/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useReadEntangle } from "../../../src";
3 | import { FILTERED_LIST, TOGGLE_COMPLETED } from "../atoms";
4 |
5 | const TodoList = () => {
6 | const filteredList = useReadEntangle(FILTERED_LIST);
7 |
8 | return (
9 |
17 | );
18 | };
19 |
20 | export default TodoList;
21 |
--------------------------------------------------------------------------------
/examples/todoList/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "esModuleInterop": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | // The root of your source code, typically /src
4 | // `` is a token Jest substitutes
5 | roots: ["/src"],
6 |
7 | // Jest transformations -- this adds support for TypeScript
8 | // using ts-jest
9 | transform: {
10 | "^.+\\.tsx?$": "ts-jest",
11 | },
12 |
13 | setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
14 | // Test spec file resolution pattern
15 | // Matches parent folder `__tests__` and filename
16 | // should contain `test` or `spec`.
17 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
18 | moduleNameMapper: {
19 | "\\.(css|less)$": "/src/__mocks__/styleMock.js",
20 | },
21 |
22 | // Module file extensions for importing
23 | moduleFileExtensions: ["tsx", "ts", "js"],
24 | };
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bennyg_123/entangle",
3 | "version": "0.3.8",
4 | "main": "dist/entangle.cjs.js",
5 | "types": "dist/index.d.ts",
6 | "module": "dist/entangle.es.js",
7 | "unpkg": "dist/entangle.umd.js",
8 | "description": "lightweight state management library based off recoil js using proxies",
9 | "repository": "git@github.com:bennyg123/entangle.git",
10 | "author": "Benny Guan",
11 | "license": "MIT",
12 | "sideEffects": false,
13 | "scripts": {
14 | "prebuild": "rimraf dist",
15 | "lint": "eslint src --ext .ts --fix",
16 | "test": "jest",
17 | "build": "rollup -c",
18 | "example": "parcel ./examples/renderProfile/index.html",
19 | "example:todo": "parcel ./examples/todoList/index.html"
20 | },
21 | "peerDependencies": {
22 | "react": "^16.8.0"
23 | },
24 | "devDependencies": {
25 | "@ampproject/rollup-plugin-closure-compiler": "^0.26.0",
26 | "@testing-library/jest-dom": "^5.11.6",
27 | "@testing-library/react": "^11.2.2",
28 | "@testing-library/react-hooks": "^3.6.0",
29 | "@types/jest": "^26.0.20",
30 | "@types/react": "^17.0.0",
31 | "@types/react-dom": "^17.0.0",
32 | "@typescript-eslint/eslint-plugin": "^4.9.1",
33 | "@typescript-eslint/parser": "^4.9.1",
34 | "eslint": "^7.15.0",
35 | "eslint-config-prettier": "^7.0.0",
36 | "eslint-plugin-prettier": "^3.2.0",
37 | "eslint-plugin-react": "^7.21.5",
38 | "husky": "^4.3.5",
39 | "jest": "^26.6.3",
40 | "jet": "^0.6.6-0",
41 | "lint-staged": "^10.5.3",
42 | "parcel-bundler": "^1.12.4",
43 | "prettier": "^2.2.1",
44 | "react": "^17.0.1",
45 | "react-dom": "^17.0.1",
46 | "react-test-renderer": "^16.9.0",
47 | "rimraf": "^3.0.2",
48 | "rollup": "^2.34.2",
49 | "rollup-plugin-copy": "^3.3.0",
50 | "rollup-plugin-summary": "^1.2.3",
51 | "rollup-plugin-typescript2": "^0.29.0",
52 | "ts-jest": "^26.4.4",
53 | "typescript": "^4.1.2"
54 | },
55 | "husky": {
56 | "hooks": {
57 | "pre-commit": "lint-staged"
58 | }
59 | },
60 | "lint-staged": {
61 | "*.{js,ts,tsx}": [
62 | "yarn run lint --fix"
63 | ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import rollupTypescript from "rollup-plugin-typescript2";
2 | import typescript from "typescript";
3 | import packageJSON from "./package.json";
4 | import compiler from "@ampproject/rollup-plugin-closure-compiler";
5 | import summary from "rollup-plugin-summary";
6 |
7 | export default {
8 | input: "src/index.ts",
9 | output: [
10 | {
11 | file: `${packageJSON.module}`,
12 | format: "es",
13 | exports: "named",
14 | },
15 | {
16 | file: `${packageJSON.main}`,
17 | format: "cjs",
18 | exports: "named",
19 | },
20 | {
21 | file: `${packageJSON.unpkg}`,
22 | format: "umd",
23 | exports: "named",
24 | name: "Entangle",
25 | globals: {
26 | react: "React",
27 | },
28 | },
29 | ],
30 | // external: ["react"],
31 | plugins: [
32 | rollupTypescript({
33 | typescript: typescript,
34 | }), // Converts the TSX files to JS
35 | compiler(), // minifies the js bundle
36 | summary(),
37 | ],
38 | };
39 |
--------------------------------------------------------------------------------
/src/core/__tests__/makeAtom.spec.ts:
--------------------------------------------------------------------------------
1 | import { act } from "@testing-library/react";
2 | import { makeAtom, makeAtomEffect, makeAtomEffectSnapshot } from "../makeAtom";
3 |
4 | jest.useFakeTimers();
5 |
6 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
7 |
8 | describe("makeAtom", () => {
9 | test("makeAtom returns the right object with primitives", () => {
10 | ["ZAKU", 782, 3.14, false, { pilot: "Char Azanable" }].forEach((primitive) => {
11 | expect(makeAtom(primitive)).toStrictEqual({
12 | proxy: { value: primitive },
13 | setCallback: expect.any(Function),
14 | readOnly: false,
15 | });
16 | });
17 | });
18 |
19 | test("makeAtom's setCallback is called when value is updated", () => {
20 | const callbackFN = jest.fn();
21 | const msAtomValue = makeAtom("ZAKU");
22 |
23 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
24 |
25 | msAtomValue.setCallback(callbackFN);
26 |
27 | msAtomValue.proxy.value = "SAZABI";
28 | expect(msAtomValue.proxy.value).toEqual("SAZABI");
29 |
30 | expect(callbackFN).toHaveBeenCalledWith("SAZABI");
31 | });
32 |
33 | test("makeAtom cleanup callbacks correctly", () => {
34 | const callbackFN = jest.fn();
35 | const callbackFN2 = jest.fn();
36 | const msAtomValue = makeAtom("ZAKU");
37 |
38 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
39 |
40 | msAtomValue.setCallback(callbackFN);
41 | const cleanup = msAtomValue.setCallback(callbackFN2);
42 |
43 | msAtomValue.proxy.value = "SAZABI";
44 | expect(msAtomValue.proxy.value).toEqual("SAZABI");
45 |
46 | expect(callbackFN).toHaveBeenCalledWith("SAZABI");
47 | expect(callbackFN2).toHaveBeenCalledWith("SAZABI");
48 |
49 | cleanup();
50 |
51 | msAtomValue.proxy.value = "ZEONG";
52 | expect(msAtomValue.proxy.value).toEqual("ZEONG");
53 |
54 | expect(callbackFN).toHaveBeenCalledWith("ZEONG");
55 | expect(callbackFN2).not.toHaveBeenCalledWith("ZEONG");
56 |
57 | expect(callbackFN).toHaveBeenCalledTimes(2);
58 | expect(callbackFN2).toHaveBeenCalledTimes(1);
59 | });
60 | });
61 |
62 | describe("makeAtomEffect", () => {
63 | test("makeAtomEffect gets and sets atom value correctly", () => {
64 | const msAtomValue = makeAtom("ZAKU");
65 | const newMSAtomValue = makeAtom("");
66 |
67 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
68 | expect(newMSAtomValue.proxy.value).toEqual("");
69 |
70 | makeAtomEffect((get, set) => {
71 | const oldValue = get(msAtomValue);
72 | set(newMSAtomValue, oldValue);
73 | });
74 |
75 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
76 | expect(newMSAtomValue.proxy.value).toEqual("ZAKU");
77 | });
78 |
79 | test("makeAtomEffect is subscribed to atoms", () => {
80 | const msAtomValue = makeAtom("ZAKU");
81 | const newMSAtomValue = makeAtom("");
82 |
83 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
84 | expect(newMSAtomValue.proxy.value).toEqual("");
85 |
86 | makeAtomEffect((get, set) => {
87 | const oldValue = get(msAtomValue);
88 | set(newMSAtomValue, oldValue);
89 | });
90 |
91 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
92 | expect(newMSAtomValue.proxy.value).toEqual("ZAKU");
93 |
94 | msAtomValue.proxy.value = "SAZABI";
95 |
96 | expect(msAtomValue.proxy.value).toEqual("SAZABI");
97 | expect(newMSAtomValue.proxy.value).toEqual("SAZABI");
98 | });
99 |
100 | test("makeAtomEffect is not subscribed to atoms when passing in false", () => {
101 | const pilotAtomValue = makeAtom("Char");
102 | const msAtomValue = makeAtom("ZAKU");
103 | const newMSAtomValue = makeAtom("");
104 |
105 | expect(pilotAtomValue.proxy.value).toEqual("Char");
106 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
107 | expect(newMSAtomValue.proxy.value).toEqual("");
108 |
109 | makeAtomEffect((get, set) => {
110 | const pilotValue = get(pilotAtomValue);
111 | const msValue = get(msAtomValue, false);
112 | set(newMSAtomValue, `${pilotValue}:${msValue}`);
113 | });
114 |
115 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
116 | expect(newMSAtomValue.proxy.value).toEqual("Char:ZAKU");
117 |
118 | msAtomValue.proxy.value = "SAZABI";
119 |
120 | expect(msAtomValue.proxy.value).toEqual("SAZABI");
121 | expect(newMSAtomValue.proxy.value).toEqual("Char:ZAKU");
122 | });
123 |
124 | test("makeAtomEffect works with async functions correctly", () => {
125 | const msAtomValue = makeAtom("ZAKU");
126 | const newMSAtomValue = makeAtom("");
127 |
128 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
129 | expect(newMSAtomValue.proxy.value).toEqual("");
130 |
131 | makeAtomEffect(async (get, set) => {
132 | sleep(1);
133 | const oldValue = get(msAtomValue);
134 | set(newMSAtomValue, oldValue);
135 | });
136 |
137 | act(() => {
138 | jest.runAllTimers();
139 | });
140 |
141 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
142 | expect(newMSAtomValue.proxy.value).toEqual("ZAKU");
143 | });
144 |
145 | test("makeAtomEffect works with debounced functions", async () => {
146 | const msAtomValue = makeAtom("ZAKU");
147 | const newMSAtomValue = makeAtom("");
148 | const runChecker = jest.fn();
149 |
150 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
151 | expect(newMSAtomValue.proxy.value).toEqual("");
152 |
153 | makeAtomEffect(async (get, set) => {
154 | runChecker();
155 | const oldValue = get(msAtomValue);
156 | set(newMSAtomValue, oldValue);
157 | }, 500);
158 |
159 | msAtomValue.proxy.value = "Zeong";
160 | msAtomValue.proxy.value = "Hyakku Shinki";
161 | msAtomValue.proxy.value = "Sazabi";
162 |
163 | act(() => {
164 | jest.runOnlyPendingTimers();
165 | });
166 |
167 | expect(runChecker).toHaveBeenCalledTimes(2);
168 | expect(msAtomValue.proxy.value).toEqual("Sazabi");
169 | expect(newMSAtomValue.proxy.value).toEqual("Sazabi");
170 | });
171 | });
172 |
173 | describe("makeAtomEffectSnapshot", () => {
174 | test("makeAtomEffectSnapshot returns a function", () => {
175 | const msAtomValue = makeAtom("ZAKU");
176 | const newMSAtomValue = makeAtom("");
177 |
178 | const atomEffectSnapshot = makeAtomEffectSnapshot((get, set) => {
179 | const oldValue = get(msAtomValue);
180 | set(newMSAtomValue, oldValue);
181 | });
182 |
183 | expect(atomEffectSnapshot).toEqual(expect.any(Function));
184 | });
185 |
186 | test("makeAtomEffectSnapshot gets and sets correctly", () => {
187 | const msAtomValue = makeAtom("ZAKU");
188 | const newMSAtomValue = makeAtom("");
189 |
190 | const atomEffectSnapshot = makeAtomEffectSnapshot((get, set) => {
191 | const oldValue = get(msAtomValue);
192 | set(newMSAtomValue, oldValue);
193 | });
194 |
195 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
196 | expect(newMSAtomValue.proxy.value).toEqual("");
197 |
198 | atomEffectSnapshot();
199 |
200 | expect(msAtomValue.proxy.value).toEqual("ZAKU");
201 | expect(newMSAtomValue.proxy.value).toEqual("ZAKU");
202 | });
203 |
204 | test("makeAtomEffectSnapshot handles arguments correctly", () => {
205 | const newMSAtomValue = makeAtom("MS: ");
206 |
207 | const atomEffectSnapshot = makeAtomEffectSnapshot((get, set, ms: string) => {
208 | set(newMSAtomValue, get(newMSAtomValue) + ms);
209 | });
210 |
211 | atomEffectSnapshot("ZAKU");
212 |
213 | expect(newMSAtomValue.proxy.value).toEqual("MS: ZAKU");
214 | });
215 |
216 | test("makeAtomEffectSnapshot is not subscribed to atoms", () => {
217 | const mockCallbackFN = jest.fn();
218 | const newMSAtomValue = makeAtom("MS: ");
219 |
220 | const atomEffectSnapshot = makeAtomEffectSnapshot((get, set, ms: string) => {
221 | mockCallbackFN();
222 | set(newMSAtomValue, get(newMSAtomValue) + ms);
223 | });
224 |
225 | atomEffectSnapshot("ZAKU");
226 |
227 | expect(newMSAtomValue.proxy.value).toEqual("MS: ZAKU");
228 |
229 | newMSAtomValue.proxy.value = "MS: SAZABI";
230 |
231 | expect(mockCallbackFN).toHaveBeenCalledTimes(1);
232 |
233 | atomEffectSnapshot("ZAKU");
234 |
235 | expect(mockCallbackFN).toHaveBeenCalledTimes(2);
236 | });
237 | });
238 |
--------------------------------------------------------------------------------
/src/core/__tests__/makeFamily.spec.ts:
--------------------------------------------------------------------------------
1 | import { act } from "@testing-library/react";
2 | import { makeAtom } from "../makeAtom";
3 | import { makeAsyncMoleculeFamily, makeAtomFamily, makeMoleculeFamily } from "../makeFamily";
4 | jest.useFakeTimers();
5 |
6 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
7 |
8 | const GUNDAM_ATOM = (ms: string) => ({
9 | proxy: { value: `GUNDAM: ${ms}` },
10 | setCallback: expect.any(Function),
11 | readOnly: false,
12 | });
13 |
14 | describe("makeAtomFamily", () => {
15 | test("makeAtomFamily returns an ATOM for each different callback fn with primitive", () => {
16 | const atomFamily = makeAtomFamily("GUNDAM: ");
17 |
18 | ["RX 78-2", "ZETA", "Nu"].map((ms) => expect(atomFamily(ms)).toStrictEqual(GUNDAM_ATOM("")));
19 | });
20 |
21 | test("makeAtomFamily returns an ATOM for each different callback fn with fn", () => {
22 | const atomFamily = makeAtomFamily((ms: string) => "GUNDAM: " + ms);
23 |
24 | ["RX 78-2", "ZETA", "Nu"].map((ms) => expect(atomFamily(ms)).toStrictEqual(GUNDAM_ATOM(ms)));
25 | });
26 |
27 | test("makeAtomFamily is only called once for each unique atom", () => {
28 | const atomFamilyCaller = jest.fn();
29 |
30 | const atomFamily = makeAtomFamily((ms: string) => {
31 | atomFamilyCaller(ms);
32 | return `GUNDAM: ${ms}`;
33 | });
34 |
35 | ["RX 78-2", "ZETA", "Nu"].map((ms) => expect(atomFamily(ms)).toStrictEqual(GUNDAM_ATOM(ms)));
36 |
37 | ["RX 78-2", "ZETA", "Nu"].map((ms) => expect(atomFamily(ms)).toStrictEqual(GUNDAM_ATOM(ms)));
38 |
39 | expect(atomFamilyCaller).toHaveBeenCalledTimes(3);
40 |
41 | atomFamily("Unicorn");
42 |
43 | expect(atomFamilyCaller).toHaveBeenCalledTimes(4);
44 | });
45 |
46 | test("makeMoleculeFamily returns an ATOM for each different callback fn", () => {
47 | const allianceAtom = makeAtom("ESFS");
48 | const moleculeFamily = makeMoleculeFamily((get, pilot) => ({
49 | alliance: get(allianceAtom),
50 | pilot,
51 | }));
52 |
53 | expect(moleculeFamily("Amuro")).toStrictEqual({
54 | proxy: {
55 | value: {
56 | alliance: "ESFS",
57 | pilot: "Amuro",
58 | },
59 | },
60 | setCallback: expect.any(Function),
61 | readOnly: true,
62 | });
63 |
64 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
65 | proxy: {
66 | value: {
67 | alliance: "ESFS",
68 | pilot: "Bright Noa",
69 | },
70 | },
71 | setCallback: expect.any(Function),
72 | readOnly: true,
73 | });
74 | });
75 |
76 | test("makeMoleculeFamily is subscribed to atom changes", () => {
77 | const allianceAtom = makeAtom("ESFS");
78 | const moleculeFamily = makeMoleculeFamily((get, pilot) => ({
79 | alliance: get(allianceAtom),
80 | pilot,
81 | }));
82 |
83 | expect(moleculeFamily("Amuro")).toStrictEqual({
84 | proxy: {
85 | value: {
86 | alliance: "ESFS",
87 | pilot: "Amuro",
88 | },
89 | },
90 | setCallback: expect.any(Function),
91 | readOnly: true,
92 | });
93 |
94 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
95 | proxy: {
96 | value: {
97 | alliance: "ESFS",
98 | pilot: "Bright Noa",
99 | },
100 | },
101 | setCallback: expect.any(Function),
102 | readOnly: true,
103 | });
104 |
105 | allianceAtom.proxy.value = "Londo Bell";
106 |
107 | expect(moleculeFamily("Amuro")).toStrictEqual({
108 | proxy: {
109 | value: {
110 | alliance: "Londo Bell",
111 | pilot: "Amuro",
112 | },
113 | },
114 | setCallback: expect.any(Function),
115 | readOnly: true,
116 | });
117 |
118 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
119 | proxy: {
120 | value: {
121 | alliance: "Londo Bell",
122 | pilot: "Bright Noa",
123 | },
124 | },
125 | setCallback: expect.any(Function),
126 | readOnly: true,
127 | });
128 | });
129 |
130 | test("makeAsyncMoleculeFamily returns an ATOM for each different callback fn", async () => {
131 | const allianceAtom = makeAtom("ESFS");
132 | const moleculeFamily = makeAsyncMoleculeFamily(
133 | async (get, pilot: string) => {
134 | sleep(100);
135 |
136 | return {
137 | alliance: get(allianceAtom),
138 | pilot,
139 | };
140 | },
141 | {
142 | alliance: "",
143 | pilot: "",
144 | }
145 | );
146 |
147 | expect(moleculeFamily("Amuro")).toStrictEqual({
148 | proxy: {
149 | value: {
150 | alliance: "",
151 | pilot: "",
152 | },
153 | },
154 | setCallback: expect.any(Function),
155 | readOnly: true,
156 | });
157 |
158 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
159 | proxy: {
160 | value: {
161 | alliance: "",
162 | pilot: "",
163 | },
164 | },
165 | setCallback: expect.any(Function),
166 | readOnly: true,
167 | });
168 |
169 | await act(async () => {
170 | jest.runAllTimers();
171 | });
172 |
173 | expect(moleculeFamily("Amuro")).toStrictEqual({
174 | proxy: {
175 | value: {
176 | alliance: "ESFS",
177 | pilot: "Amuro",
178 | },
179 | },
180 | setCallback: expect.any(Function),
181 | readOnly: true,
182 | });
183 |
184 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
185 | proxy: {
186 | value: {
187 | alliance: "ESFS",
188 | pilot: "Bright Noa",
189 | },
190 | },
191 | setCallback: expect.any(Function),
192 | readOnly: true,
193 | });
194 | });
195 |
196 | test("makeAsyncMoleculeFamily returns the correct initial value for a initial function", async () => {
197 | const allianceAtom = makeAtom("ESFS");
198 | const moleculeFamily = makeAsyncMoleculeFamily(
199 | async (get, pilot: string) => {
200 | sleep(100);
201 |
202 | return {
203 | alliance: get(allianceAtom),
204 | pilot,
205 | };
206 | },
207 | (get, pilot: string) => ({
208 | alliance: "",
209 | pilot,
210 | })
211 | );
212 |
213 | expect(moleculeFamily("Amuro")).toStrictEqual({
214 | proxy: {
215 | value: {
216 | alliance: "",
217 | pilot: "Amuro",
218 | },
219 | },
220 | setCallback: expect.any(Function),
221 | readOnly: true,
222 | });
223 |
224 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
225 | proxy: {
226 | value: {
227 | alliance: "",
228 | pilot: "Bright Noa",
229 | },
230 | },
231 | setCallback: expect.any(Function),
232 | readOnly: true,
233 | });
234 |
235 | await act(async () => {
236 | jest.runAllTimers();
237 | });
238 |
239 | expect(moleculeFamily("Amuro")).toStrictEqual({
240 | proxy: {
241 | value: {
242 | alliance: "ESFS",
243 | pilot: "Amuro",
244 | },
245 | },
246 | setCallback: expect.any(Function),
247 | readOnly: true,
248 | });
249 |
250 | expect(moleculeFamily("Bright Noa")).toStrictEqual({
251 | proxy: {
252 | value: {
253 | alliance: "ESFS",
254 | pilot: "Bright Noa",
255 | },
256 | },
257 | setCallback: expect.any(Function),
258 | readOnly: true,
259 | });
260 | });
261 | });
262 |
--------------------------------------------------------------------------------
/src/core/__tests__/makeMolecule.spec.ts:
--------------------------------------------------------------------------------
1 | import { makeAtom } from "../makeAtom";
2 | import { act } from "@testing-library/react";
3 | import { makeAsyncMolecule, makeMolecule } from "../makeMolecule";
4 |
5 | jest.useFakeTimers();
6 |
7 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
8 |
9 | describe("makeMolecule", () => {
10 | test("makeMolecule returns an ATOM", () => {
11 | const msAtom = makeAtom("ZAKU");
12 | const pilotAtom = makeAtom("Char");
13 |
14 | const profileMolecule = makeMolecule((get) => ({
15 | ms: get(msAtom),
16 | pilot: get(pilotAtom),
17 | }));
18 |
19 | expect(profileMolecule).toStrictEqual({
20 | proxy: {
21 | value: {
22 | ms: "ZAKU",
23 | pilot: "Char",
24 | },
25 | },
26 | setCallback: expect.any(Function),
27 | readOnly: true,
28 | });
29 | });
30 |
31 | test("makeMolecule is subscribed to atom changes", () => {
32 | const msAtom = makeAtom("ZAKU");
33 | const pilotAtom = makeAtom("Char");
34 |
35 | const profileMolecule = makeMolecule((get) => ({
36 | ms: get(msAtom),
37 | pilot: get(pilotAtom),
38 | }));
39 |
40 | expect(profileMolecule.proxy.value).toStrictEqual({
41 | ms: "ZAKU",
42 | pilot: "Char",
43 | });
44 |
45 | msAtom.proxy.value = "SAZABI";
46 |
47 | expect(profileMolecule.proxy.value).toStrictEqual({
48 | ms: "SAZABI",
49 | pilot: "Char",
50 | });
51 | });
52 |
53 | test("makeMolecule is not subscribed to atom changes when passed false", () => {
54 | const msAtom = makeAtom("ZAKU");
55 | const pilotAtom = makeAtom("Char");
56 |
57 | const profileMolecule = makeMolecule((get) => ({
58 | ms: get(msAtom, false),
59 | pilot: get(pilotAtom),
60 | }));
61 |
62 | expect(profileMolecule.proxy.value).toStrictEqual({
63 | ms: "ZAKU",
64 | pilot: "Char",
65 | });
66 |
67 | msAtom.proxy.value = "SAZABI";
68 |
69 | expect(profileMolecule.proxy.value).toStrictEqual({
70 | ms: "ZAKU",
71 | pilot: "Char",
72 | });
73 |
74 | pilotAtom.proxy.value = "Char Azanable";
75 |
76 | expect(profileMolecule.proxy.value).toStrictEqual({
77 | ms: "SAZABI",
78 | pilot: "Char Azanable",
79 | });
80 | });
81 |
82 | test("makeMolecule is debounced correctly", () => {
83 | const msAtom = makeAtom("ZAKU");
84 | const pilotAtom = makeAtom("Char");
85 | const runChecker = jest.fn();
86 |
87 | const profileMolecule = makeMolecule((get) => {
88 | runChecker();
89 | return {
90 | ms: get(msAtom),
91 | pilot: get(pilotAtom),
92 | };
93 | }, 500);
94 |
95 | expect(profileMolecule.proxy.value).toStrictEqual({
96 | ms: "ZAKU",
97 | pilot: "Char",
98 | });
99 |
100 | msAtom.proxy.value = "Zeong";
101 | msAtom.proxy.value = "Hyakku Shiki";
102 | msAtom.proxy.value = "SAZABI";
103 |
104 | jest.runAllTimers();
105 |
106 | expect(profileMolecule.proxy.value).toStrictEqual({
107 | ms: "SAZABI",
108 | pilot: "Char",
109 | });
110 |
111 | expect(runChecker).toHaveBeenCalledTimes(2);
112 | });
113 | });
114 |
115 | describe("makeAsyncMolecule", () => {
116 | test("makeAsyncMolecule returns an atom and updates asynchronously", async () => {
117 | const msAtom = makeAtom("ZAKU");
118 | const pilotAtom = makeAtom("Char");
119 |
120 | const profileMolecule = makeAsyncMolecule(
121 | async (get) => {
122 | sleep(100);
123 | return {
124 | ms: get(msAtom),
125 | pilot: get(pilotAtom),
126 | };
127 | },
128 | { ms: "", pilot: "" }
129 | );
130 |
131 | expect(profileMolecule).toStrictEqual({
132 | proxy: {
133 | value: {
134 | ms: "",
135 | pilot: "",
136 | },
137 | },
138 | setCallback: expect.any(Function),
139 | readOnly: true,
140 | });
141 |
142 | await act(async () => {
143 | jest.runAllTimers();
144 | });
145 |
146 | expect(profileMolecule).toStrictEqual({
147 | proxy: {
148 | value: {
149 | ms: "ZAKU",
150 | pilot: "Char",
151 | },
152 | },
153 | setCallback: expect.any(Function),
154 | readOnly: true,
155 | });
156 | });
157 |
158 | test("makeAsyncMolecule is subscribed to changes", async () => {
159 | const msAtom = makeAtom("ZAKU");
160 | const pilotAtom = makeAtom("Char");
161 |
162 | const profileMolecule = makeAsyncMolecule(
163 | async (get) => {
164 | sleep(100);
165 | return {
166 | ms: get(msAtom),
167 | pilot: get(pilotAtom),
168 | };
169 | },
170 | { ms: "", pilot: "" }
171 | );
172 |
173 | await act(async () => {
174 | jest.runAllTimers();
175 | });
176 |
177 | expect(profileMolecule).toStrictEqual({
178 | proxy: {
179 | value: {
180 | ms: "ZAKU",
181 | pilot: "Char",
182 | },
183 | },
184 | setCallback: expect.any(Function),
185 | readOnly: true,
186 | });
187 |
188 | msAtom.proxy.value = "SAZABI";
189 |
190 | expect(profileMolecule).toStrictEqual({
191 | proxy: {
192 | value: {
193 | ms: "ZAKU",
194 | pilot: "Char",
195 | },
196 | },
197 | setCallback: expect.any(Function),
198 | readOnly: true,
199 | });
200 |
201 | await act(async () => {
202 | jest.runAllTimers();
203 | });
204 |
205 | expect(profileMolecule).toStrictEqual({
206 | proxy: {
207 | value: {
208 | ms: "SAZABI",
209 | pilot: "Char",
210 | },
211 | },
212 | setCallback: expect.any(Function),
213 | readOnly: true,
214 | });
215 | });
216 |
217 | test("makeAsyncMolecule is not subscribed to changes when passing in false to subscribed", async () => {
218 | const msAtom = makeAtom("ZAKU");
219 | const pilotAtom = makeAtom("Char");
220 |
221 | const profileMolecule = makeAsyncMolecule(
222 | async (get) => {
223 | sleep(100);
224 | return {
225 | ms: get(msAtom, false),
226 | pilot: get(pilotAtom),
227 | };
228 | },
229 | { ms: "", pilot: "" }
230 | );
231 |
232 | await act(async () => {
233 | jest.runAllTimers();
234 | });
235 |
236 | expect(profileMolecule).toStrictEqual({
237 | proxy: {
238 | value: {
239 | ms: "ZAKU",
240 | pilot: "Char",
241 | },
242 | },
243 | setCallback: expect.any(Function),
244 | readOnly: true,
245 | });
246 |
247 | msAtom.proxy.value = "SAZABI";
248 |
249 | expect(profileMolecule).toStrictEqual({
250 | proxy: {
251 | value: {
252 | ms: "ZAKU",
253 | pilot: "Char",
254 | },
255 | },
256 | setCallback: expect.any(Function),
257 | readOnly: true,
258 | });
259 |
260 | await act(async () => {
261 | jest.runAllTimers();
262 | });
263 |
264 | expect(profileMolecule).toStrictEqual({
265 | proxy: {
266 | value: {
267 | ms: "ZAKU",
268 | pilot: "Char",
269 | },
270 | },
271 | setCallback: expect.any(Function),
272 | readOnly: true,
273 | });
274 | });
275 |
276 | test("makeAsyncMolecule is debounced correctly", async () => {
277 | const msAtom = makeAtom("ZAKU");
278 | const pilotAtom = makeAtom("Char");
279 | const runChecker = jest.fn();
280 |
281 | const profileMolecule = makeAsyncMolecule(
282 | async (get) => {
283 | sleep(100);
284 | runChecker();
285 | return {
286 | ms: get(msAtom),
287 | pilot: get(pilotAtom),
288 | };
289 | },
290 | { ms: "", pilot: "" },
291 | 500
292 | );
293 |
294 | expect(profileMolecule).toStrictEqual({
295 | proxy: {
296 | value: {
297 | ms: "",
298 | pilot: "",
299 | },
300 | },
301 | setCallback: expect.any(Function),
302 | readOnly: true,
303 | });
304 |
305 | await act(async () => {
306 | msAtom.proxy.value = "Zeong";
307 | msAtom.proxy.value = "Hyakku Shiki";
308 | msAtom.proxy.value = "SAZABI";
309 |
310 | jest.runAllTimers();
311 | });
312 |
313 | expect(runChecker).toHaveBeenCalledTimes(2);
314 | expect(profileMolecule).toStrictEqual({
315 | proxy: {
316 | value: {
317 | ms: "SAZABI",
318 | pilot: "Char",
319 | },
320 | },
321 | setCallback: expect.any(Function),
322 | readOnly: true,
323 | });
324 | });
325 | });
326 |
--------------------------------------------------------------------------------
/src/core/makeAtom.ts:
--------------------------------------------------------------------------------
1 | import { ATOM_CALLBACK, ATOM, ATOM_EFFECT_FN, ATOM_EFFECT_SNAPSHOT_FN } from "../types";
2 | import { defaultGetter, defaultSetter } from "../utils/utils";
3 |
4 | /**
5 | * Basic unit that contains a value to which molecules and hooks can subscribe to its changes.
6 | *
7 | * @param {T} value initialValue for the atom
8 | * @param {boolean} readOnly boolean to determine whether the atom is readOnly or not
9 | * @returns {ATOM} returns an atom to be used in a hook or molecule function
10 | */
11 | export const makeAtom = (value: T, readOnly = false): ATOM => {
12 | const callbacks: ATOM_CALLBACK[] = [];
13 |
14 | return {
15 | proxy: new Proxy(
16 | { value },
17 | {
18 | set: (target, property, value) => {
19 | target["value"] = value;
20 | callbacks.forEach((cb) => cb(value));
21 | return true;
22 | },
23 | }
24 | ),
25 | setCallback: (cb) => {
26 | callbacks.push(cb);
27 | return () => callbacks.splice(callbacks.indexOf(cb), 1);
28 | },
29 | readOnly,
30 | };
31 | };
32 |
33 | /**
34 | * Side effect function that can subscribe to atoms and also update them, can be used outside of components.
35 | *
36 | * @param {ATOM_EFFECT_FN} atomEffectFn function with a getter or setter to run updates to atoms outside of a hook
37 | * @returns {void}
38 | */
39 | export const makeAtomEffect = (atomEffectFn: ATOM_EFFECT_FN, debounce?: number): void => {
40 | let debounceID = 0;
41 | atomEffectFn((atomValue, subscribed = true) => {
42 | if (subscribed) {
43 | atomValue.setCallback(
44 | debounce && debounce > 0
45 | ? () => {
46 | window.clearTimeout(debounceID);
47 | debounceID = window.setTimeout(async () => {
48 | await atomEffectFn(defaultGetter, defaultSetter);
49 | }, debounce);
50 | }
51 | : async () => {
52 | await atomEffectFn(defaultGetter, defaultSetter);
53 | }
54 | );
55 | }
56 |
57 | return atomValue.proxy.value;
58 | }, defaultSetter);
59 | };
60 |
61 | /**
62 | * Similar to makeAtomEffect but this returns a function that can be called manually and is not subscribed to the atom values it reads.
63 | *
64 | * @param {ATOM_EFFECT_FN} atomEffectFn function with a getter or setter to run updates to atoms outside of a hook
65 | * @returns {(...args: K) => void} function to manually call to run the side effect function passed
66 | */
67 | export const makeAtomEffectSnapshot = (atomSnapshotFn: ATOM_EFFECT_SNAPSHOT_FN) => (...args: K): void =>
68 | atomSnapshotFn(defaultGetter, defaultSetter, ...args);
69 |
--------------------------------------------------------------------------------
/src/core/makeFamily.ts:
--------------------------------------------------------------------------------
1 | import { ASYNC_ATOM_MOLECULE_FAMILY_FN, ATOM, ATOM_MOLECULE_FAMILY_FN, ATOM_MOLECULE_FAMILY_INITIAL_VALUE_FN } from "../types";
2 | import { defaultGetter } from "../utils/utils";
3 | import { makeAtom } from "./makeAtom";
4 | import { makeAsyncMolecule, makeMolecule } from "./makeMolecule";
5 |
6 | enum FT {
7 | A, // ATOM
8 | M, // MOLECULE
9 | AM, // ASYNC_MOLECULE
10 | }
11 |
12 | const makeFamily = (
13 | type: FT,
14 | initialValue?: T | ((...args: [string, ...K]) => T),
15 | moleculeFN?: ATOM_MOLECULE_FAMILY_FN | ASYNC_ATOM_MOLECULE_FAMILY_FN,
16 | initialValueFN?: ATOM_MOLECULE_FAMILY_INITIAL_VALUE_FN
17 | ): ((...args: [string, ...K]) => ATOM) => {
18 | const atomMap: { [keys: string]: ATOM } = {};
19 |
20 | return (...args) => {
21 | const keyValue = args[0];
22 | if (!atomMap[keyValue]) {
23 | if (type === FT.A) {
24 | atomMap[keyValue] = makeAtom(
25 | typeof initialValue === "function"
26 | ? (initialValue as (...args: [string, ...K]) => T)(...args)
27 | : (initialValue as T)
28 | );
29 | } else if (type === FT.M) {
30 | atomMap[keyValue] = makeMolecule((get) => (moleculeFN as ATOM_MOLECULE_FAMILY_FN)(get, ...args)) as ATOM;
31 | } else {
32 | const asyncInitialValue =
33 | initialValueFN === undefined
34 | ? initialValue
35 | : (initialValueFN as ATOM_MOLECULE_FAMILY_INITIAL_VALUE_FN)(defaultGetter, ...args);
36 | atomMap[keyValue] = makeAsyncMolecule(
37 | async (get) => await (moleculeFN as ATOM_MOLECULE_FAMILY_FN)(get, ...args),
38 | asyncInitialValue
39 | ) as ATOM;
40 | }
41 | }
42 | return atomMap[keyValue];
43 | };
44 | };
45 |
46 | /**
47 | * Creates an generator function that can generate unique atoms given a key that is passed in. Same keys will result in the same atom being returned and not recreated.
48 | *
49 | * @param {T | (key: string, ...args:K[]) => T} initialValue takes in either a value or function that generates a value
50 | * @returns {(key: string, ...args: K[])} return function whose arguments as passed to the function that generates initialValue (if a function was passed). First argument is a unique string to help differentiate atoms.
51 | */
52 | export const makeAtomFamily = (
53 | initialValue: T | ((...args: [string, ...K]) => T)
54 | ): ((...args: [string, ...K]) => ATOM) => makeFamily(FT.A, initialValue) as (...args: [string, ...K]) => ATOM;
55 |
56 | /**
57 | * Creates an generator function that can generate unique molecules given a key that is passed in. Same keys will result in the same molecule being returned and not recreated.
58 | *
59 | * @param {(get, key, ...args: K[]) => T} moleculeFN molecule function that has a atom getter function that returns a composite value based off the atoms
60 | * @returns {(get, key, ...args: K[])} returns a function whose arguments are passed to moleculeFN. First argument is a unique string to help differentiate molecules.
61 | */
62 | export const makeMoleculeFamily = (
63 | moleculeFN: ATOM_MOLECULE_FAMILY_FN
64 | ): ((...args: [string, ...K]) => ATOM) => makeFamily(FT.M, undefined, moleculeFN, undefined);
65 |
66 | /**
67 | * Creates an generator function that can generate unique async molecules given a key that is passed in. Same keys will result in the same molecule being returned and not recreated.
68 | *
69 | * @param {(get, key, ...args: K[]) => Promise} moleculeFN async molecule function that has a atom getter function that returns a composite value based off the atoms
70 | * @param {T | (get, key, ...args: K[]) => T} initialValue value or synchronous function to generate the initial value for the molecule before the main function is run
71 | * @returns {(get, key, ...args: K[])} returns a function whose arguments are passed to moleculeFN. First argument is a unique string to help differentiate molecules.
72 | */
73 | export const makeAsyncMoleculeFamily = (
74 | moleculeFN: ASYNC_ATOM_MOLECULE_FAMILY_FN,
75 | initialValue: T | ATOM_MOLECULE_FAMILY_INITIAL_VALUE_FN
76 | ): ((...args: [string, ...K]) => ATOM) =>
77 | makeFamily(
78 | FT.AM,
79 | typeof initialValue === "function" ? undefined : initialValue,
80 | moleculeFN,
81 | typeof initialValue === "function" ? (initialValue as ATOM_MOLECULE_FAMILY_INITIAL_VALUE_FN) : undefined
82 | );
83 |
--------------------------------------------------------------------------------
/src/core/makeMolecule.ts:
--------------------------------------------------------------------------------
1 | import { ATOM } from "../types";
2 | import { defaultGetter } from "../utils/utils";
3 | import { makeAtom } from "./makeAtom";
4 | /**
5 | * A composite function that returns a value from other atoms, it also subscribes to the atom values it reads from
6 | *
7 | * @param {(get: typeof defaultGetter) => T} generateMolecule a non-async function that has a getter function passed in and returns a value
8 | * @returns ATOM
9 | */
10 | export const makeMolecule = (
11 | generateMolecule: (get: typeof defaultGetter) => T extends Promise ? never : T,
12 | debounce: number | undefined = undefined
13 | ): ATOM => {
14 | let proxy: { value?: T } = {};
15 | let debounceID = 0;
16 |
17 | // Since every molecule is composed of different atoms we need to add callbacks to each of those atoms
18 | // So that when any of them update, the molecule is automatically updated as well.
19 | const atom = makeAtom(
20 | generateMolecule((atomValue, subscribed = true) => {
21 | // On the first pass of generateMolecule execution, the updater function is called with this callback function
22 |
23 | // If we dont want the molecule to subscribe to a certain atoms changes we can pass in false to subscribed
24 | if (subscribed) {
25 | atomValue.setCallback(
26 | debounce && debounce > 0
27 | ? () => {
28 | window.clearTimeout(debounceID);
29 |
30 | debounceID = window.setTimeout(() => {
31 | proxy.value = generateMolecule(defaultGetter);
32 | }, debounce);
33 | }
34 | : () => {
35 | proxy.value = generateMolecule(defaultGetter);
36 | }
37 | );
38 | }
39 |
40 | return atomValue.proxy.value;
41 | }),
42 | true
43 | );
44 |
45 | proxy = atom.proxy;
46 |
47 | return atom;
48 | };
49 | /**
50 | * Am async composite function that returns a value from other atoms, it also subscribes to the atom values it reads from.
51 | *
52 | * @param {(get: typeof defaultGetter)=> Promise} asyncGenerateMolecule async function that has a getter passed in to produce a composite value from other atoms
53 | * @param {T} defaultValue defaultValue for the atom before it gets updated
54 | * @returns ATOM
55 | */
56 | export const makeAsyncMolecule = (
57 | asyncGenerateMolecule: (get: typeof defaultGetter) => Promise,
58 | defaultValue: T,
59 | debounce: number | undefined = undefined
60 | ): ATOM => {
61 | const atom = makeAtom(defaultValue, true);
62 | let debounceID = 0;
63 |
64 | (async () => {
65 | atom.proxy.value = await asyncGenerateMolecule((atomValue, subscribed = true) => {
66 | // On the first pass of generateMolecule execution, the updater function is called with this callback function
67 |
68 | // if the user opts to not subscribe to certain atoms, then can pass false to subscribe
69 | if (subscribed) {
70 | atomValue.setCallback(
71 | debounce && debounce > 0
72 | ? () => {
73 | window.clearTimeout(debounceID);
74 | debounceID = window.setTimeout(async () => {
75 | atom.proxy.value = await asyncGenerateMolecule(defaultGetter);
76 | }, debounce);
77 | }
78 | : async () => {
79 | // On the second and subsequent calls whenever one of the individual atoms get updated, the generateMolecule is called again to regenerate the molecule
80 | atom.proxy.value = await asyncGenerateMolecule(defaultGetter);
81 | }
82 | );
83 | }
84 |
85 | return atomValue.proxy.value;
86 | });
87 | })();
88 |
89 | return atom;
90 | };
91 |
--------------------------------------------------------------------------------
/src/hooks/__tests__/useEntangle.spec.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { render, act as renderAct, fireEvent } from "@testing-library/react";
3 | import { renderHook, act as renderHookAct } from "@testing-library/react-hooks";
4 | import { makeAtom } from "../../core/makeAtom";
5 | import { makeAsyncMolecule, makeMolecule } from "../../core/makeMolecule";
6 | import { makeAsyncMoleculeFamily, makeAtomFamily, makeMoleculeFamily } from "../../core/makeFamily";
7 | import { useEntangle, useMultiEntangle } from "../useEntangle";
8 |
9 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
10 |
11 | jest.useFakeTimers();
12 |
13 | describe("useEntangle", () => {
14 | test("useEntangle works with makeAtom", () => {
15 | const msAtom = makeAtom("ZAKU");
16 |
17 | const { result } = renderHook(() => useEntangle(msAtom));
18 |
19 | expect(result.current[0]).toEqual("ZAKU");
20 |
21 | renderHookAct(() => {
22 | result.current[1]("SAZABI");
23 | });
24 |
25 | expect(result.current[0]).toEqual("SAZABI");
26 | });
27 |
28 | test("useEntangle works with makeMolecule and cannot be updated", () => {
29 | const msAtom = makeAtom("ZAKU");
30 | const profileMolecule = makeMolecule((get) => ({
31 | ms: get(msAtom),
32 | pilot: "Char",
33 | }));
34 |
35 | const { result: atomHook } = renderHook(() => useEntangle(msAtom));
36 | const { result: profileHook } = renderHook(() => useEntangle(profileMolecule));
37 |
38 | expect(profileHook.current[0]).toEqual({
39 | ms: "ZAKU",
40 | pilot: "Char",
41 | });
42 |
43 | renderHookAct(() => {
44 | expect(() => profileHook.current[1]({ ms: "SAZABI", pilot: "Char" })).toThrow(
45 | new Error("Read Only ATOMS cannot be set")
46 | );
47 | });
48 |
49 | expect(profileHook.current[0]).toEqual({
50 | ms: "ZAKU",
51 | pilot: "Char",
52 | });
53 |
54 | renderHookAct(() => {
55 | atomHook.current[1]("SAZABI");
56 | });
57 |
58 | expect(atomHook.current[0]).toEqual("SAZABI");
59 | expect(profileHook.current[0]).toEqual({
60 | ms: "SAZABI",
61 | pilot: "Char",
62 | });
63 | });
64 |
65 | test("useEntangle works with makeAsyncMolecule and cannot be updated", async () => {
66 | const msAtom = makeAtom("ZAKU");
67 | const profileMolecule = makeAsyncMolecule(
68 | async (get) => {
69 | await sleep(1000);
70 | return {
71 | ms: get(msAtom),
72 | pilot: "Char",
73 | };
74 | },
75 | {
76 | pilot: "",
77 | ms: "",
78 | }
79 | );
80 |
81 | const { result: atomHook } = renderHook(() => useEntangle(msAtom));
82 | const { result: profileHook } = renderHook(() => useEntangle(profileMolecule));
83 |
84 | expect(profileHook.current[0]).toEqual({
85 | ms: "",
86 | pilot: "",
87 | });
88 |
89 | await renderHookAct(async () => {
90 | expect(() => profileHook.current[1]({ ms: "SAZABI", pilot: "Char" })).toThrow(
91 | new Error("Read Only ATOMS cannot be set")
92 | );
93 | jest.runAllTimers();
94 | });
95 |
96 | expect(profileHook.current[0]).toEqual({
97 | ms: "ZAKU",
98 | pilot: "Char",
99 | });
100 |
101 | await renderHookAct(async () => {
102 | atomHook.current[1]("SAZABI");
103 | jest.runAllTimers();
104 | });
105 |
106 | expect(atomHook.current[0]).toEqual("SAZABI");
107 | expect(profileHook.current[0]).toEqual({
108 | ms: "SAZABI",
109 | pilot: "Char",
110 | });
111 | });
112 |
113 | test("useEntangle works with makeAtomFamily", () => {
114 | const renderCallback = jest.fn();
115 | const msAtomFamily = makeAtomFamily((ms) => {
116 | renderCallback();
117 | return `MOBILE SUIT: ${ms}`;
118 | });
119 |
120 | const { result: atomHook1 } = renderHook(() => useEntangle(msAtomFamily("ZAKU")));
121 | const { result: atomHook2 } = renderHook(() => useEntangle(msAtomFamily("RX 78-2")));
122 | const { result: atomHook3 } = renderHook(() => useEntangle(msAtomFamily("ZEONG")));
123 |
124 | expect(atomHook1.current[0]).toEqual("MOBILE SUIT: ZAKU");
125 | expect(atomHook2.current[0]).toEqual("MOBILE SUIT: RX 78-2");
126 | expect(atomHook3.current[0]).toEqual("MOBILE SUIT: ZEONG");
127 |
128 | expect(renderCallback).toHaveBeenCalledTimes(3);
129 |
130 | const { result: atomHook4 } = renderHook(() => useEntangle(msAtomFamily("ZAKU"))); // cached atom value is returned
131 | expect(atomHook4.current[0]).toEqual("MOBILE SUIT: ZAKU");
132 |
133 | expect(renderCallback).toHaveBeenCalledTimes(3);
134 |
135 | renderHookAct(() => {
136 | atomHook4.current[1]("SAZABI");
137 | });
138 |
139 | expect(atomHook4.current[0]).toEqual("SAZABI");
140 | expect(atomHook4.current[0]).toEqual("SAZABI");
141 | });
142 |
143 | test("useEntangle works with makeMoleculeFamily", () => {
144 | const renderCallback = jest.fn();
145 | const msAtom = makeAtom("MOBILE SUIT:");
146 | const profileMoleculeFamily = makeMoleculeFamily((get, ms, pilot) => {
147 | renderCallback();
148 | return {
149 | ms: `${get(msAtom)} ${ms}`,
150 | pilot,
151 | };
152 | });
153 |
154 | const { result: atomHook1 } = renderHook(() => useEntangle(profileMoleculeFamily("ZAKU", "Char")));
155 | const { result: atomHook2 } = renderHook(() => useEntangle(profileMoleculeFamily("RX 78-2", "Amuro")));
156 | const { result: atomHook3 } = renderHook(() => useEntangle(profileMoleculeFamily("ZEONG", "Char")));
157 |
158 | expect(atomHook1.current[0]).toEqual({
159 | ms: `MOBILE SUIT: ZAKU`,
160 | pilot: `Char`,
161 | });
162 |
163 | expect(atomHook2.current[0]).toEqual({
164 | ms: `MOBILE SUIT: RX 78-2`,
165 | pilot: `Amuro`,
166 | });
167 |
168 | expect(atomHook3.current[0]).toEqual({
169 | ms: `MOBILE SUIT: ZEONG`,
170 | pilot: `Char`,
171 | });
172 | });
173 | test("useEntangle works with makeAsyncMoleculeFamily", async () => {
174 | const renderCallback = jest.fn();
175 | const msAtom = makeAtom("MOBILE SUIT:");
176 | const profileMoleculeFamily = makeAsyncMoleculeFamily(
177 | async (get, ms: string, pilot: string) => {
178 | sleep(100);
179 | renderCallback();
180 | return {
181 | ms: `${get(msAtom)} ${ms}`,
182 | pilot,
183 | };
184 | },
185 | {
186 | ms: "",
187 | pilot: "",
188 | }
189 | );
190 |
191 | const { result: atomHook1 } = renderHook(() => useEntangle(profileMoleculeFamily("ZAKU", "Char")));
192 | const { result: atomHook2 } = renderHook(() => useEntangle(profileMoleculeFamily("RX 78-2", "Amuro")));
193 | const { result: atomHook3 } = renderHook(() => useEntangle(profileMoleculeFamily("ZEONG", "Char")));
194 |
195 | expect(atomHook1.current[0]).toEqual({
196 | ms: "",
197 | pilot: "",
198 | });
199 |
200 | expect(atomHook2.current[0]).toEqual({
201 | ms: "",
202 | pilot: "",
203 | });
204 |
205 | expect(atomHook3.current[0]).toEqual({
206 | ms: "",
207 | pilot: "",
208 | });
209 |
210 | await renderHookAct(async () => {
211 | jest.runAllTimers();
212 | });
213 |
214 | expect(atomHook1.current[0]).toEqual({
215 | ms: `MOBILE SUIT: ZAKU`,
216 | pilot: `Char`,
217 | });
218 |
219 | expect(atomHook2.current[0]).toEqual({
220 | ms: `MOBILE SUIT: RX 78-2`,
221 | pilot: `Amuro`,
222 | });
223 |
224 | expect(atomHook3.current[0]).toEqual({
225 | ms: `MOBILE SUIT: ZEONG`,
226 | pilot: `Char`,
227 | });
228 | });
229 |
230 | test("useEntangle only updates components subscribed to it and not parent components", async () => {
231 | const msRender = jest.fn();
232 | const pilotRender = jest.fn();
233 | const profileRender = jest.fn();
234 | const parentRender = jest.fn();
235 |
236 | const msAtom = makeAtom("ZAKU");
237 | const pilotAtom = makeAtom("Char");
238 |
239 | const profileMolecule = makeAsyncMolecule(
240 | async (get) => {
241 | await sleep(100);
242 | return {
243 | ms: get(msAtom),
244 | pilot: get(pilotAtom),
245 | };
246 | },
247 | {
248 | ms: "",
249 | pilot: "",
250 | }
251 | );
252 |
253 | const MSComponent = () => {
254 | const [ms, setMS] = useEntangle(msAtom);
255 |
256 | useEffect(() => {
257 | msRender();
258 | });
259 |
260 | return (
261 | <>
262 | {ms}
263 | setMS("SAZABI")}>
264 | Update MS
265 |
266 | >
267 | );
268 | };
269 |
270 | const PilotComponent = () => {
271 | const [pilot, setPilot] = useEntangle(pilotAtom);
272 |
273 | useEffect(() => {
274 | pilotRender();
275 | });
276 |
277 | return (
278 | <>
279 | {pilot}
280 | setPilot("RED COMET")}>
281 | Update MS
282 |
283 | >
284 | );
285 | };
286 |
287 | const ProfileComponent = () => {
288 | const [profile] = useEntangle(profileMolecule);
289 |
290 | useEffect(() => {
291 | profileRender();
292 | });
293 |
294 | return {JSON.stringify(profile)}
;
295 | };
296 |
297 | const ParentComponent = () => {
298 | useEffect(() => {
299 | parentRender();
300 | });
301 |
302 | return (
303 | <>
304 |
305 |
306 |
307 | >
308 | );
309 | };
310 |
311 | const { container } = render( );
312 |
313 | [
314 | {
315 | className: "MS_VALUE",
316 | value: "ZAKU",
317 | },
318 | {
319 | className: "PILOT_VALUE",
320 | value: "Char",
321 | },
322 | {
323 | className: "PROFILE",
324 | value: JSON.stringify({
325 | ms: "",
326 | pilot: "",
327 | }),
328 | },
329 | ].map(({ className, value }) => {
330 | expect(container.getElementsByClassName(className)[0].innerHTML).toEqual(value);
331 | });
332 |
333 | expect(msRender).toHaveBeenCalledTimes(1);
334 | expect(pilotRender).toHaveBeenCalledTimes(1);
335 | expect(profileRender).toHaveBeenCalledTimes(1);
336 | expect(parentRender).toHaveBeenCalledTimes(1);
337 |
338 | await renderAct(async () => {
339 | jest.runAllTimers();
340 | });
341 |
342 | expect(msRender).toHaveBeenCalledTimes(1);
343 | expect(pilotRender).toHaveBeenCalledTimes(1);
344 | expect(profileRender).toHaveBeenCalledTimes(2);
345 | expect(parentRender).toHaveBeenCalledTimes(1);
346 |
347 | await renderAct(async () => {
348 | fireEvent.click(container.getElementsByClassName("UPDATE_MS")[0]);
349 | });
350 |
351 | expect(container.getElementsByClassName("PROFILE")[0].innerHTML).toEqual(
352 | JSON.stringify({
353 | ms: "ZAKU",
354 | pilot: "Char",
355 | })
356 | );
357 |
358 | expect(msRender).toHaveBeenCalledTimes(2);
359 | expect(pilotRender).toHaveBeenCalledTimes(1);
360 | expect(profileRender).toHaveBeenCalledTimes(2);
361 | expect(parentRender).toHaveBeenCalledTimes(1);
362 |
363 | expect(container.getElementsByClassName("MS_VALUE")[0].innerHTML).toEqual("SAZABI");
364 |
365 | await renderAct(async () => {
366 | jest.runAllTimers();
367 | });
368 |
369 | expect(container.getElementsByClassName("PROFILE")[0].innerHTML).toEqual(
370 | JSON.stringify({
371 | ms: "SAZABI",
372 | pilot: "Char",
373 | })
374 | );
375 |
376 | await renderAct(async () => {
377 | fireEvent.click(container.getElementsByClassName("UPDATE_PILOT")[0]);
378 | jest.runAllTimers();
379 | });
380 |
381 | expect(container.getElementsByClassName("PROFILE")[0].innerHTML).toEqual(
382 | JSON.stringify({
383 | ms: "SAZABI",
384 | pilot: "RED COMET",
385 | })
386 | );
387 | });
388 |
389 | test("useEntangle cleans up callbacks correctly", () => {
390 | const atom = makeAtom("Hello");
391 |
392 | const Component = () => {
393 | useEntangle(atom);
394 |
395 | return <>>;
396 | };
397 |
398 | const { unmount } = render( );
399 |
400 | expect(atom.setCallback(() => void 0)).toEqual(expect.any(Function));
401 | });
402 | });
403 |
404 | describe("useMultiEntangle", () => {
405 | test("useMultiEntangle is able to read multiple atoms and set multiple atoms and stay subscribed", () => {
406 | const msAtom = makeAtom("ZAKU");
407 | const pilotAtom = makeAtom("Char");
408 | const allianceMolecule = makeMolecule((get) => (get(msAtom) === "ZAKU" ? "ZEON" : "ESFS"));
409 |
410 | const { result } = renderHook(() => useMultiEntangle(msAtom, pilotAtom, allianceMolecule));
411 |
412 | expect(result.current[0]).toStrictEqual(["ZAKU", "Char", "ZEON"]);
413 | expect(result.current[1]).toStrictEqual([expect.any(Function), expect.any(Function), expect.any(Function)]);
414 |
415 | renderHookAct(() => {
416 | result.current[1][0]("Hyakku Shiki");
417 | });
418 |
419 | expect(result.current[0]).toStrictEqual(["Hyakku Shiki", "Char", "ESFS"]);
420 |
421 | renderHookAct(() => {
422 | result.current[1][1]("Quattro Bajeena");
423 | });
424 |
425 | expect(result.current[0]).toStrictEqual(["Hyakku Shiki", "Quattro Bajeena", "ESFS"]);
426 | });
427 | });
428 |
--------------------------------------------------------------------------------
/src/hooks/__tests__/useReadEntangle.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { renderHook, act as renderAct } from "@testing-library/react-hooks";
3 | import { makeAtom } from "../../core/makeAtom";
4 | import { useEntangle } from "../useEntangle";
5 | import { useMultiReadEntangle, useReadEntangle } from "../useReadEntangle";
6 | import { act, fireEvent, render } from "@testing-library/react";
7 | import { makeMolecule } from "../../core/makeMolecule";
8 |
9 | describe("useReadEntangle", () => {
10 | test("useReadEntangle returns an atom value", () => {
11 | const msAtom = makeAtom("ZAKU");
12 | const { result } = renderHook(() => useReadEntangle(msAtom));
13 |
14 | expect(result.current).toEqual("ZAKU");
15 | });
16 |
17 | test("useReadEntangle updates with the atom", () => {
18 | const msAtom = makeAtom("ZAKU");
19 |
20 | const AtomReadOnlyComponent = () => {
21 | const ms = useReadEntangle(msAtom);
22 |
23 | return {ms} ;
24 | };
25 |
26 | const AtomComponent = () => {
27 | const [ms, setMS] = useEntangle(msAtom);
28 |
29 | return (
30 | <>
31 | setMS("SAZABI")}>
32 | UPDATE MS
33 |
34 | >
35 | );
36 | };
37 |
38 | const { container } = render(
39 | <>
40 |
41 |
42 | >
43 | );
44 |
45 | expect(container.getElementsByClassName("MS_READ_ONLY")[0].innerHTML).toEqual("ZAKU");
46 |
47 | act(() => {
48 | fireEvent.click(container.getElementsByClassName("UPDATE_MS")[0]);
49 | });
50 |
51 | expect(container.getElementsByClassName("MS_READ_ONLY")[0].innerHTML).toEqual("SAZABI");
52 | });
53 | });
54 |
55 | describe("useMultiReadEntangle", () => {
56 | test("useMultiReadEntangle returns multiple atom values and stay subscribed", () => {
57 | const msAtom = makeAtom("ZAKU");
58 | const pilotAtom = makeAtom("Char");
59 | const allianceMolecule = makeMolecule((get) => (get(msAtom) === "ZAKU" ? "ZEON" : "ESFS"));
60 |
61 | const { result } = renderHook(() => useMultiReadEntangle(msAtom, pilotAtom, allianceMolecule));
62 |
63 | expect(result.current).toStrictEqual(["ZAKU", "Char", "ZEON"]);
64 |
65 | renderAct(() => {
66 | msAtom.proxy.value = "Hyakku Shiki";
67 | });
68 |
69 | expect(result.current).toStrictEqual(["Hyakku Shiki", "Char", "ESFS"]);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/src/hooks/__tests__/useSetEntangle.spec.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { renderHook } from "@testing-library/react-hooks";
3 | import { act, fireEvent, render } from "@testing-library/react";
4 | import { makeAtom } from "../../core/makeAtom";
5 | import { useEntangle } from "../useEntangle";
6 | import { useMultiSetEntangle, useSetEntangle } from "../useSetEntangle";
7 | import { makeAsyncMolecule, makeMolecule } from "../../core/makeMolecule";
8 | import { makeAsyncMoleculeFamily, makeMoleculeFamily, makeAtomFamily } from "../../core/makeFamily";
9 | import { defaultGetter } from "../../utils/utils";
10 |
11 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
12 |
13 | describe("useSetEntangle", () => {
14 | test("useSetEntangle returns an function to set atoms", () => {
15 | const msAtom = makeAtom("ZAKU");
16 | const { result } = renderHook(() => useSetEntangle(msAtom));
17 |
18 | expect(result.current).toEqual(expect.any(Function));
19 |
20 | result.current("SAZABI");
21 |
22 | expect(msAtom.proxy.value).toEqual("SAZABI");
23 | });
24 |
25 | test("useSetEntangle updates the atom without rerendering", () => {
26 | const reRendered = jest.fn();
27 | const msAtom = makeAtom("ZAKU");
28 |
29 | const AtomSetOnlyComponent = () => {
30 | const setMS = useSetEntangle(msAtom);
31 |
32 | useEffect(() => {
33 | reRendered();
34 | });
35 |
36 | return (
37 | setMS("SAZABI")}>
38 | UPDATE MS
39 |
40 | );
41 | };
42 |
43 | const AtomComponent = () => {
44 | const [ms, setMS] = useEntangle(msAtom);
45 |
46 | return (
47 | <>
48 | {ms}
49 | >
50 | );
51 | };
52 |
53 | const { container } = render(
54 | <>
55 |
56 |
57 | >
58 | );
59 |
60 | expect(container.getElementsByClassName("MS_VALUE")[0].innerHTML).toEqual("ZAKU");
61 |
62 | expect(reRendered).toHaveBeenCalledTimes(1);
63 |
64 | act(() => {
65 | fireEvent.click(container.getElementsByClassName("UPDATE_MS")[0]);
66 | });
67 |
68 | expect(reRendered).toHaveBeenCalledTimes(1);
69 |
70 | expect(container.getElementsByClassName("MS_VALUE")[0].innerHTML).toEqual("SAZABI");
71 | });
72 |
73 | test("useSetEntangle throws an error when used with a molecule", () => {
74 | const msAtom = makeAtom("ZAKU");
75 |
76 | const moleculeFN = (get: typeof defaultGetter) => `MS: ${get(msAtom)}`;
77 | const asyncMoleculeFN = async (get: typeof defaultGetter) => {
78 | await sleep(100);
79 |
80 | return `MS: ${get(msAtom)}`;
81 | };
82 |
83 | const msMolecule = makeMolecule(moleculeFN);
84 | const msAsyncMolecule = makeAsyncMolecule(asyncMoleculeFN, "");
85 | const msAtomFromFamily = makeAtomFamily("ZAKU")("");
86 | const msMoleculeFromFamily = makeMoleculeFamily(moleculeFN)("");
87 | const msAsyncMoleculeFromFamily = makeAsyncMoleculeFamily(asyncMoleculeFN, "")("");
88 |
89 | [msMolecule, msAsyncMolecule, msMoleculeFromFamily, msAsyncMoleculeFromFamily].map((atom) => {
90 | expect(() => useSetEntangle(atom)).toThrow(new Error("Read Only ATOMS cannot be used with useSetEntangle"));
91 | });
92 |
93 | [msAtom, msAtomFromFamily].map((atom) => {
94 | expect(() => useSetEntangle(atom)).not.toThrow(new Error("Read Only ATOMS cannot be used with useSetEntangle"));
95 | });
96 | });
97 | });
98 |
99 | describe("useMultiSetEntangle", () => {
100 | test("useMultiSetEntangle returns multiple atom setters", () => {
101 | const msAtom = makeAtom("ZAKU");
102 | const pilotAtom = makeAtom("Char");
103 |
104 | const { result } = renderHook(() => useMultiSetEntangle(msAtom, pilotAtom));
105 |
106 | expect(result.current).toStrictEqual([expect.any(Function), expect.any(Function)]);
107 | });
108 |
109 | test("useMultiSetEntangle throws an error when used with a molecule", () => {
110 | const msAtom = makeAtom("ZAKU");
111 | const pilotAtom = makeAtom("Char");
112 | const allianceMolecule = makeMolecule((get) => (get(msAtom) === "ZAKU" ? "ZEON" : "ESFS"));
113 |
114 | expect(() => useMultiSetEntangle(msAtom, pilotAtom, allianceMolecule)).toThrow(
115 | new Error("Read Only ATOMS cannot be used with useSetEntangle")
116 | );
117 | });
118 |
119 | test("useMultiSetEntangle does not stay subscribed to atoms", () => {
120 | const msAtom = makeAtom("ZAKU");
121 | const pilotAtom = makeAtom("Char");
122 | const reRendered = jest.fn();
123 |
124 | const Component = () => {
125 | const [setMS, setPilot] = useMultiSetEntangle(msAtom, pilotAtom);
126 |
127 | useEffect(() => {
128 | reRendered();
129 | });
130 |
131 | return (
132 | {
135 | setMS("Hyakku Shiki");
136 | setPilot("Quattro Bajeena");
137 | }}
138 | >
139 | );
140 | };
141 |
142 | const { container } = render( );
143 |
144 | expect(reRendered).toHaveBeenCalledTimes(1);
145 |
146 | act(() => {
147 | fireEvent.click(container.getElementsByClassName("UPDATE_ATOMS")[0]);
148 | });
149 |
150 | expect(reRendered).toHaveBeenCalledTimes(1);
151 |
152 | expect(msAtom.proxy.value).toEqual("Hyakku Shiki");
153 | expect(pilotAtom.proxy.value).toEqual("Quattro Bajeena");
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/src/hooks/useEntangle.ts:
--------------------------------------------------------------------------------
1 | import { ATOM } from "../types";
2 | import { useCallback, useEffect, useState } from "react";
3 |
4 | /**
5 | * Hook to subscribe a component to an atom and its changes
6 | *
7 | * @param {ATOM} atomValue atom to subscribe the component to
8 | * @returns {[T, (newValue: T) => T]} the value of the atom and a function to update the atom (Atoms only not molecules)
9 | */
10 | export const useEntangle = (atomValue: ATOM): [value: T, setValue: (newValue: T) => T] => {
11 | const [entangleState, setEntangleState] = useState(atomValue.proxy.value);
12 | const setValue = useCallback(
13 | (newValue: T) => {
14 | if (atomValue.readOnly) throw new Error("Read Only ATOMS cannot be set");
15 | return (atomValue.proxy.value = newValue);
16 | },
17 | [atomValue]
18 | );
19 |
20 | useEffect(() => {
21 | const cleanup = atomValue.setCallback(setEntangleState);
22 |
23 | return () => cleanup();
24 | }, []);
25 |
26 | return [entangleState, setValue];
27 | };
28 |
29 | /**
30 | * Helper hook to listen to multiple atoms. Essentially this is just calling useEntangle on multiple atoms.
31 | * @param {ATOM[]} ...args
32 | * @returns {[unknown[], (newValue: unknown => unknown)[]]} returns an array of values and setters in the order of the atoms passed in
33 | */
34 | // TODO: FIX TYPING
35 | export const useMultiEntangle = (...args: ATOM[]): [unknown[], ((newValue: unknown) => unknown)[]] =>
36 | args.reduce(
37 | (currentAtomArr: [unknown[], ((newValue: unknown) => unknown)[]], atom: ATOM) => {
38 | const [atomValue, setAtomValue] = useEntangle(atom);
39 | currentAtomArr[0].push(atomValue);
40 | currentAtomArr[1].push(setAtomValue);
41 | return currentAtomArr;
42 | },
43 | [[], []]
44 | );
45 |
--------------------------------------------------------------------------------
/src/hooks/useReadEntangle.ts:
--------------------------------------------------------------------------------
1 | import { ATOM } from "../types";
2 | import { useEntangle, useMultiEntangle } from "./useEntangle";
3 |
4 | /**
5 | * Hook to get a read only value of an atom without the setter function
6 | *
7 | * @param {ATOM} atomValue atom to read the value from
8 | * @returns {T} value of the atom
9 | */
10 | export const useReadEntangle = (atomValue: ATOM): T => useEntangle(atomValue)[0];
11 |
12 | /**
13 | * Helper hook to get the values of multiple atoms. Essentially this is just calling useEntangle on multiple atoms and returning the values
14 | * @param {ATOM[]} ...args
15 | * @returns {[unknown[]]} returns an array of values in the order of the atoms passed in
16 | */
17 | export const useMultiReadEntangle = (...args: ATOM[]): unknown[] => useMultiEntangle(...args)[0];
18 |
--------------------------------------------------------------------------------
/src/hooks/useSetEntangle.ts:
--------------------------------------------------------------------------------
1 | import { ATOM } from "../types";
2 |
3 | /**
4 | * Helper function to set an atom without subscribing to its changes
5 | *
6 | * @param {ATOM} atomValue atom to set a new value to
7 | * @returns {(newValue: T) => T} setter function to update the atom
8 | */
9 | export const useSetEntangle = (atomValue: ATOM): ((newValue: T) => T) => {
10 | if (atomValue.readOnly) throw new Error("Read Only ATOMS cannot be used with useSetEntangle");
11 | return (newValue: T) => (atomValue.proxy.value = newValue);
12 | };
13 |
14 | /**
15 | * Helper hook to get the setters of multiple atoms. This allows for multiple setters without subscription.
16 | * @param {ATOM[]} ...args
17 | * @returns {[(newValue: unknown) => unknown]} returns an array of setters in the order of the atoms passed in
18 | */
19 | export const useMultiSetEntangle = (...args: ATOM[]): ((newValue: unknown) => unknown)[] =>
20 | args.map((atomValue) => {
21 | if (atomValue.readOnly) throw new Error("Read Only ATOMS cannot be used with useSetEntangle");
22 | return (newValue: unknown) => (atomValue.proxy.value = newValue);
23 | });
24 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { makeAtom, makeAtomEffect, makeAtomEffectSnapshot } from "./core/makeAtom";
2 | import { makeMolecule, makeAsyncMolecule } from "./core/makeMolecule";
3 | import { makeAtomFamily, makeMoleculeFamily, makeAsyncMoleculeFamily } from "./core/makeFamily";
4 | import { useEntangle, useMultiEntangle } from "./hooks/useEntangle";
5 | import { useReadEntangle, useMultiReadEntangle } from "./hooks/useReadEntangle";
6 | import { useSetEntangle, useMultiSetEntangle } from "./hooks/useSetEntangle";
7 | import {
8 | ATOM,
9 | ATOM_CALLBACK,
10 | ATOM_EFFECT_FN,
11 | ATOM_EFFECT_SNAPSHOT_FN,
12 | ATOM_MOLECULE_FAMILY_FN,
13 | ASYNC_ATOM_MOLECULE_FAMILY_FN,
14 | } from "./types";
15 |
16 | export {
17 | // Core
18 | makeAtom,
19 | makeMolecule,
20 | makeAsyncMolecule,
21 | // Effect
22 | makeAtomEffect,
23 | makeAtomEffectSnapshot,
24 | // Family generators
25 | makeAtomFamily,
26 | makeMoleculeFamily,
27 | makeAsyncMoleculeFamily,
28 | // Hooks
29 | useEntangle,
30 | useSetEntangle,
31 | useReadEntangle,
32 | // Multi Hooks
33 | useMultiEntangle,
34 | useMultiSetEntangle,
35 | useMultiReadEntangle,
36 | // Types
37 | ATOM,
38 | ATOM_CALLBACK,
39 | ATOM_EFFECT_FN,
40 | ATOM_EFFECT_SNAPSHOT_FN,
41 | ATOM_MOLECULE_FAMILY_FN,
42 | ASYNC_ATOM_MOLECULE_FAMILY_FN,
43 | };
44 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { defaultGetter } from "./utils/utils";
2 |
3 | export type ATOM_CALLBACK = (newValue: T) => void;
4 |
5 | export type ATOM_EFFECT_FN = (
6 | get: (atomValue: ATOM, subscribed?: boolean) => T,
7 | set: (atomValue: ATOM, newValue: T) => T
8 | ) => void;
9 |
10 | export type ATOM = {
11 | proxy: { value: T };
12 | setCallback: (callbackFN: (newValue: T) => void) => () => void;
13 | readOnly: boolean;
14 | };
15 |
16 | export type ATOM_EFFECT_SNAPSHOT_FN = (
17 | get: (atomValue: ATOM) => T,
18 | set: (atomValue: ATOM, newValue: T) => T,
19 | ...args: K
20 | ) => void;
21 |
22 | export type ATOM_MOLECULE_FAMILY_FN = (
23 | get: (atomValue: ATOM) => J,
24 | ...args: [string, ...K]
25 | ) => T extends Promise ? never : T;
26 |
27 | export type ASYNC_ATOM_MOLECULE_FAMILY_FN = (
28 | get: (atomValue: ATOM) => J,
29 | ...args: [string, ...K]
30 | ) => Promise;
31 |
32 | export type ATOM_MOLECULE_FAMILY_INITIAL_VALUE_FN = (
33 | get: (atomValue: ATOM) => J,
34 | ...args: [string, ...K]
35 | ) => T;
36 |
--------------------------------------------------------------------------------
/src/utils/__tests__/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { makeAtom } from "../../core/makeAtom";
2 | import { defaultGetter, defaultSetter } from "../utils";
3 |
4 | describe("utils", () => {
5 | test("defaultGetter gets value from atom", () => {
6 | const msAtom = makeAtom("ZAKU");
7 |
8 | expect(defaultGetter(msAtom)).toEqual("ZAKU");
9 | });
10 |
11 | test("defaultSettertGetter sets value to atom", () => {
12 | const msAtom = makeAtom("ZAKU");
13 |
14 | expect(defaultGetter(msAtom)).toEqual("ZAKU");
15 |
16 | defaultSetter(msAtom, "SAZABI");
17 |
18 | expect(defaultGetter(msAtom)).toEqual("SAZABI");
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import { ATOM } from "../types";
2 |
3 | /**
4 | * Helper function to get the value from an atom.
5 | *
6 | * @param {ATOM} atomValue ATOM to get value from
7 | * @param {ATOM} subscribed For the molecules and effects, if we dont want the atom to be subscribed we can pass in false
8 | * @returns {T} Value of the atom
9 | */
10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
11 | export const defaultGetter = (atomValue: ATOM, subscribed?: boolean): T => atomValue.proxy.value;
12 |
13 | /**
14 | * Helper function to set a new value to a supplied atom.
15 | *
16 | * @param {ATOM} atomValue ATOM where the new value will be set
17 | * @param {T} newValue new value to replace the ATOM current value
18 | * @returns {T} Value of the atom
19 | */
20 | export const defaultSetter = (atomValue: ATOM, newValue: T): T => (atomValue.proxy.value = newValue);
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "ES6",
13 | "declaration": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src/*.ts", "src/*.d.ts"],
20 | "exclude": ["node_modules", "src/*.spec.ts"]
21 | }
22 |
--------------------------------------------------------------------------------