├── .editorconfig ├── .github └── workflows │ └── coverage.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── babel.config.cjs ├── docs ├── dark-mode.md ├── nodejs.md ├── redux.md └── shared-state.md ├── hooks-bindings ├── package.json └── src │ ├── index.d.ts │ └── index.js ├── jest.config.json ├── logo.png ├── package.json ├── preact ├── package.json └── src │ ├── index.d.ts │ └── index.js ├── react ├── package.json └── src │ ├── index.d.ts │ └── index.js ├── src ├── index.d.ts └── index.js ├── tests ├── box-class.test.ts ├── deprecated.test.ts ├── event.test.ts ├── it-works.test.ts ├── preact │ ├── component.test.ts │ ├── use-box.test.ts │ └── use-boxes.test.ts ├── promise.test.ts ├── react │ ├── component.test.tsx │ ├── use-box.test.ts │ └── use-boxes.test.ts └── unsubscriber.test.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | quote_type = single 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | indent_style = space 15 | indent_size = 2 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: [push] 3 | env: 4 | CI: true 5 | 6 | jobs: 7 | run: 8 | name: node ${{ matrix.node }} on ${{ matrix.os }} 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node: [18] 15 | os: [ubuntu-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: node --version 23 | - run: yarn --version 24 | - run: yarn 25 | - run: yarn build 26 | - run: yarn test --coverage 27 | - uses: coverallsapp/github-action@master 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs and editors 2 | .idea 3 | .project 4 | .classpath 5 | .c9/ 6 | *.launch 7 | .settings/ 8 | *.sublime-workspace 9 | 10 | # IDE - VSCode 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | 17 | # OS files 18 | .DS_Store 19 | Thumbs.db 20 | 21 | # dependencies 22 | node_modules 23 | 24 | # logs 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | lerna-debug.log* 29 | 30 | # jest 31 | /coverage 32 | 33 | # build 34 | dist 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 re-js 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 | 2 |
3 | 4 | # Remini 5 | 6 | Remini 7 | 8 | **Simple** and powerful **state management** in React and Preact 9 | 10 | [![npm version](https://img.shields.io/npm/v/remini?style=flat-square)](https://www.npmjs.com/package/remini) 11 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/remini?style=flat-square)](https://bundlephobia.com/result?p=remini) 12 | [![code coverage](https://img.shields.io/coveralls/github/re-js/remini?style=flat-square)](https://coveralls.io/github/re-js/remini) 13 | [![typescript supported](https://img.shields.io/npm/types/typescript?style=flat-square)](./src/index.d.ts) 14 | 15 | ➪ **Easy** to learn 16 | 17 | ➪ Small **and quick** 18 | 19 | ➪ For any scale **apps** 20 | 21 |
22 | 23 | 24 | ## Get started 25 | 26 | At first you have a **state** 😊 27 | 28 | ```javascript 29 | const $user = box({ email: 'a@x.com' }) 30 | const $enabled = box(true) 31 | const $counter = box(42) 32 | const $books = box([ 'The Little Prince', 'Alice in Wonderland' ]) 33 | ``` 34 | 35 | At second **bind state to React** component! 36 | 37 | ```javascript 38 | const Books = () => { 39 | const books = useBox($books) 40 | return 43 | } 44 | ``` 45 | 46 | At third **update the state** 👍 47 | 48 | ```javascript 49 | const BookForm = () => { 50 | const [name, setName] = React.useState('') 51 | return

52 | setName(event.target.value)} 55 | /> 56 | 59 |

60 | } 61 | ``` 62 | 63 | [![Edit Simple and powerful state management with Remini](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/simple-and-powerful-state-management-with-remini-7ejjhd?file=/src/App.js) 64 | 65 | At fourth **share your logic** 😉 66 | 67 | ```javascript 68 | // ./books.shared.js 69 | export const $books = box([]) 70 | export const $loading = box(false) 71 | 72 | export const load = async () => { 73 | set($loading, true) 74 | 75 | const response = await fetch('https://example.com/api/books') 76 | const books = await response.json() 77 | 78 | set($books, books) 79 | set($loading, false) 80 | } 81 | ``` 82 | 83 | ```javascript 84 | const BooksLoad = () => { 85 | const loading = useBox($loading) 86 | return

87 | {loading ? 'Loading...' : ( 88 | 89 | )} 90 |

91 | } 92 | ``` 93 | 94 | 95 | ## Why Remini? 96 | 97 | Your coding time saver! 98 | 99 | Minimal, well structured, and flexible codebase save a lot of developer time for maintain and grouth your React applications. 100 | 101 | How it works 102 | 103 | Usually when you just start React project or have a very small one, your codebase is short, undestable and simple, you can easily google examples of common issues. 104 | 105 | But as you write the business logic of your application, the code gets larger and it becomes more and more difficult to understand the abundance of files, tricks and code pieces. 106 | 107 | You should clear understand where is a place to your logic, how you can write as many code as you want without reduce your application maintance. 108 | 109 | - How to make a simple React application who can easily upscale to large application by business demand 110 | - How to organize your code clean with minimal states and convinient separated logic 111 | - How to speed up your application and reduce boilerplate 112 | 113 | My answer is **Remini** 😍 114 | 115 | 116 | ## Multiple stores vs single store 117 | 118 | One of the manifestations is the **multiple-store** architecture. The main reason is the independent modules decomposition. For flexible growth, you should separate your code. Your app should be built on top of separated modules composition. There is each module contains some data and logic. 119 | 120 | It’s a very good architecture decision because you can develop and test each module separately. You can easily reuse modules between projects. And when you use a lazy load for some parts of your app, you will never have any problem with it, just import it and use it. It should be simple! 121 | 122 | Ok. The first one is the **separated module decomposition**, and what's the next? 123 | 124 | If each module has its own state and logic it is very convenient to use separate stores to control data flow. 125 | 126 | At that moment the good time to make the postulate: **each store should be simple**, and never recommend to make deeply nested state. The better way is following to KISS principle. 127 | 128 | 129 | ## Selection from store 130 | 131 | One of the most frequently used functions during work with the state is the selection. Selection is the transformation of your state, fairly for **performance reasons**. You should update your view components only when updated the data used inside. This is the **rendering optimization**. 132 | 133 | For example, your user state is big it has a lot of user settings and some stuff. If you have an avatar view component, it should be updated only when the avatar changes, not for each user state update. 134 | 135 | ```javascript 136 | import { box, get, wrap } from 'remini' 137 | 138 | const $user = box({ 139 | name: 'Joe', 140 | email: 'a@x.com', 141 | settings: {}, 142 | avatar: 'https://avatar.com/1.jpg' 143 | }) 144 | 145 | const $avatar = wrap(() => get($user).avatar) 146 | ``` 147 | 148 | ```javascript 149 | import { useBox } from 'remini/react' 150 | 151 | const Avatar = () => { 152 | const avatar = useBox($avatar) 153 | return ( 154 | 155 | ) 156 | } 157 | ``` 158 | 159 | You can see how it’s easy to make that tiny, but very effective optimization! 160 | 161 | You don't have to render everything. You should render only what you need! No more, no less) 162 | 163 | 164 | ## Composition of stores 165 | 166 | Step by step on the application growing upstairs you will have cases of the necessary combination of multiple stores to one. It should be simple) 167 | 168 | ```javascript 169 | import { box, get, wrap } from 'remini' 170 | 171 | const $firstName = box('John') 172 | const $lastName = box('Doe') 173 | 174 | const $fullName = wrap(() => { 175 | return get($firstName) + ' ' + get($lastName) 176 | }) 177 | ``` 178 | 179 | Here we combine several stores into one for convenient use in some view components. 180 | 181 | 182 | ## References 183 | 184 | - [The dark mode switcher](./docs/dark-mode.md) 185 | - [Shared state](./docs/shared-state.md) 186 | - [Work together with Redux](./docs/redux.md) 187 | - [Pure reactivity in Node.js](./docs/nodejs.md) 188 | 189 | 190 | ## Install 191 | 192 | ```bash 193 | npm install remini 194 | # or 195 | yarn add remini 196 | ``` 197 | 198 | Enjoy your code! 199 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /docs/dark-mode.md: -------------------------------------------------------------------------------- 1 | # The dark mode switcher 2 | 3 | A good example of a shared state benefit is the Dark mode switcher. Because you should get access to user choice in a big set of React components, it is very inconvenient to use props passing pattern. 4 | 5 | What is necessary to implement: 6 | 7 | - Provide convenient functions for changing user choices. 8 | - Provide access to user choice around the app code. 9 | - Keep user choice across browser sessions. 10 | 11 | Will clearly demonstrate how to create, use and propagate a shared state. 12 | 13 | Each shared state is stored in a special place created by calling the `box` function. This will be a reactive variable, which means we will be able to update all places where it is used when it changes. 14 | 15 | We will keep the dark mode enabled state in this way. 16 | 17 | To update the value of a reactive variable, we will use the `update` function. That takes the dark mode reactive variable as the first argument and the updater function as the second one. The updater function receives the current state in the first argument and returned the new state of dark mode. 18 | 19 | ```javascript 20 | // ./dark-mode.shared.js 21 | import { box, update } from 'remini' 22 | 23 | // create new reactive variable with "false" by default 24 | export const $darkMode = box(false) 25 | 26 | // create a function that should change dark mode to opposite each time calling 27 | export const toggleDarkMode = () => { 28 | update($darkMode, enabled => !enabled) 29 | } 30 | ``` 31 | 32 | Now we can read and subscribe to dark mode changes everywhere we need. 33 | 34 | For easy binding to the React components, the `useBox` hook function is used. It allows you to get the value of the reactive variable, as well as automatically update the React component when the value changes. 35 | 36 | ```javascript 37 | import { useBox } from 'remini/react' 38 | import { $darkMode, toggleDarkMode } from './dark-mode.shared' 39 | 40 | export const DarkModeButton = () => { 41 | const darkMode = useBox($darkMode) 42 | 43 | return ( 44 | 47 | ) 48 | } 49 | ``` 50 | 51 | Excellent! Now you can easily derive dark mode state to any React component using the same way. This is very simple, you should get state of the dark mode using the `useBox` hook, and it's all that you need. Each time when dark mode state will be changed, and all components using it will be updated automatically. 52 | 53 | And finally, we should make some code updates, because we almost forget to save user choice to browser local storage, to keep persistent between browser sessions. 54 | 55 | For accessing storage we will use the "localStorage" browser API. We will call "getItem" to retrieve the saved state, and call "setItem" to save it. 56 | 57 | ```javascript 58 | // import { set, on } from 'remini' 59 | 60 | // get choice from previous browser session 61 | set($darkMode, localStorage.getItem('darkMode') === 'on') 62 | 63 | // update user choice in browser local storage each time then it changed 64 | on($darkMode, enabled => { 65 | localStorage.setItem('darkMode', enabled ? 'on' : 'off') 66 | }) 67 | ``` 68 | 69 | The last operation in this example call of `on` function. It means that we subscribe to changes in dark mode reactive variable, and react on them each time changes for saving state to browser persistence storage. 70 | 71 | Brilliant! Now you can use it everywhere you want, it's worked well and should provide benefits for your users! 72 | 73 | [![Edit DarkMode module with Remini](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/darkmode-module-with-remini-5updlc?file=/src/App.js) 74 | 75 | It's looking good and provides you with convenient opportunities for controlling your shared state, and deriving in any parts of your application. You can create as many reactive variables as you want, it's quick and useful! 76 | -------------------------------------------------------------------------------- /docs/nodejs.md: -------------------------------------------------------------------------------- 1 | # Pure reactivity in Node.js 2 | 3 | ```javascript 4 | import { box, get, set, update, wrap, on } from 'remini' 5 | 6 | const $value = box(0) 7 | const $next = wrap(() => get($value) + 1) 8 | 9 | on($next, n => console.log('Next value: ' + n)) 10 | 11 | update($value, n => n + 1) // Next value: 2 12 | set($value, 2) // Next value: 3 13 | 14 | console.log(get($next)) // 3 15 | ``` 16 | 17 | [![Try it on RunKit](https://badge.runkitcdn.com/>.svg)](https://runkit.com/betula/62ac2287cdb97e00080fc9d5) 18 | -------------------------------------------------------------------------------- /docs/redux.md: -------------------------------------------------------------------------------- 1 | # Work together with Redux 2 | 3 | It's easy! You can simply create Remini reactive variable from Redux store, and use it everywhere you want! 4 | 5 | ```javascript 6 | // ./remini-store.js 7 | import { box, set } from 'remini' 8 | import { store } from './redux-store' 9 | 10 | export const $store = box(store.getState()) 11 | 12 | store.subscribe(() => { 13 | set($store, store.getState()) 14 | }) 15 | ``` 16 | 17 | And you can make cached selectors for performance optimization reasons. 18 | 19 | ```javascript 20 | // ./remini-selectors.js 21 | import { get, wrap } from 'remini' 22 | import { $store } from './remini-store' 23 | 24 | export const $user = wrap(() => get($store).user) 25 | 26 | export const $fullName = wrap( 27 | () => `${get($user).firstName} ${get($user).lastName}` 28 | ); 29 | ``` 30 | 31 | And use it everywhere. 32 | 33 | ```javascript 34 | import { useBox } from 'remini/react' 35 | import { $fullName } from './remini-selectors' 36 | 37 | export const UserInfo = () => { 38 | const fullName = useBox($fullName) 39 | 40 | return

{fullName}

41 | } 42 | ``` 43 | 44 | As you can see, everything is quite simple and can be effectively used together! 45 | 46 | [![Edit Redux with Remini](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/redux-with-remini-ou9v4e?file=/src/components/UserInfo.js) 47 | -------------------------------------------------------------------------------- /docs/shared-state.md: -------------------------------------------------------------------------------- 1 | # Shared state with Remini 2 | 3 | The key to winning is the shared state and logic. Pure React doesn't have a convenient way to organize shared states that can be used whole the application. 4 | 5 | Suggested React ways are passing state by props thought nesting components from parent to child, and using Context for giving shared access to some state values in children components. Both ways can't share state with any part of your app! 6 | 7 | Architecture with a shared state provides more simplest code. You can control your state and logic in separate files that can be accessed whole the app. You can easily change your shared state and read it everywhere. 8 | 9 | ## Simple counter demo 10 | 11 | ```javascript 12 | import { box, update } from 'remini'; 13 | import { useBox } from 'remini/react'; 14 | 15 | const $count = box(0) 16 | const inc = () => update($count, c => c + 1) 17 | 18 | const Counter = () => { 19 | const count = useBox($count) 20 | return

21 | {count} 22 | 23 |

; 24 | }; 25 | ``` 26 | 27 | ## Perfect frontend with modular architecture. 28 | 29 | - No need to wrap the application to Context Provider for each module. 30 | - Import and use, easy code for embedding. 31 | 32 | ## Modular counter demo 33 | 34 | ```javascript 35 | // ./counter.shared.js 36 | import { box, wrap, get, set } from 'remini' 37 | 38 | export const $count = box(0) 39 | export const $next = wrap(() => get($count) + 1) 40 | 41 | export const inc = () => update($count, n => n + 1) 42 | export const reset = () => set($count, 0) 43 | ``` 44 | 45 | ```javascript 46 | import { get } from 'remini' 47 | import { component } from 'remini/react' 48 | import { $count, $next, inc, reset } from './counter.shared' 49 | 50 | const Counter = component(() => ( 51 |

52 | {get($count)} 53 | 54 | {get($next)} 55 |

56 | )) 57 | 58 | const Reset = () => ( 59 | 60 | ) 61 | 62 | export const App = () => ( 63 | <> 64 | 65 | 66 | 67 | 68 | ) 69 | ``` 70 | 71 | [![Edit Counter with Remini](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/counter-with-remini-mp2ldi?file=/src/App.js) 72 | 73 | And configure [babel jsx wrapper](https://github.com/re-js/babel-plugin-jsx-wrapper) for automatic observation arrow function components if you want. 74 | -------------------------------------------------------------------------------- /hooks-bindings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remini-hooks-bindings", 3 | "version": "0.0.0", 4 | "description": "Hooks based bindings factory for Remini", 5 | "private": true, 6 | "license": "MIT", 7 | "types": "src/index.d.ts", 8 | "main": "dist/hooks-bindings.js", 9 | "module": "dist/hooks-bindings.module.js", 10 | "umd:main": "dist/hooks-bindings.umd.js", 11 | "source": "src/index.js", 12 | "peerDependencies": { 13 | "reactive-box": ">=0.8", 14 | "remini": ">=0.2" 15 | }, 16 | "author": "Slava Bereza (http://betula.co)", 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./src/index.d.ts", 21 | "browser": "./dist/hooks-bindings.module.js", 22 | "umd": "./dist/hooks-bindings.umd.js", 23 | "import": "./dist/hooks-bindings.mjs", 24 | "require": "./dist/hooks-bindings.js" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hooks-bindings/src/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export declare const bindings: ( 3 | useReducer: any, 4 | useEffect: any, 5 | useRef: any, 6 | useMemo: any 7 | ) => any; 8 | -------------------------------------------------------------------------------- /hooks-bindings/src/index.js: -------------------------------------------------------------------------------- 1 | import { on, get } from 'remini'; 2 | import { expr } from 'reactive-box'; 3 | 4 | // 5 | // Bindings factory 6 | // 7 | 8 | export function bindings(useReducer, useEffect, useRef, useMemo, memo) { 9 | 10 | let context_is_observe; 11 | let observe_no_memo_flag; 12 | 13 | const useForceUpdate = () => ( 14 | useReducer(() => [], [])[1] 15 | ); 16 | 17 | const component = ((target) => { 18 | function fn() { 19 | const force_update = useForceUpdate(); 20 | const ref = useRef(); 21 | if (!ref.current) ref.current = expr(target, force_update); 22 | useEffect(() => ref.current[1], []); 23 | 24 | const stack = context_is_observe; 25 | context_is_observe = 1; 26 | try { 27 | return ref.current[0].apply(this, arguments); 28 | } finally { 29 | context_is_observe = stack; 30 | } 31 | } 32 | return (observe_no_memo_flag || !memo) 33 | ? ((observe_no_memo_flag = 0), fn) 34 | : memo(fn) 35 | }); 36 | 37 | if (memo) { 38 | component.nomemo = (target) => ( 39 | (observe_no_memo_flag = 1), 40 | component(target) 41 | ); 42 | } 43 | 44 | const useBox = (target, deps) => { 45 | deps || (deps = []); 46 | const force_update = context_is_observe || useForceUpdate(); 47 | const h = useMemo(() => { 48 | if (!target) return [target, () => {}]; 49 | if (target[0]) target = target[0]; 50 | 51 | if (typeof target === 'function') { 52 | if (context_is_observe) { 53 | return [target, 0, 1]; 54 | } else { 55 | const stop = on(target, force_update); 56 | return [target, () => stop, 1]; 57 | } 58 | } else { 59 | return [target, () => {}]; 60 | } 61 | }, deps); 62 | 63 | context_is_observe || useEffect(h[1], [h]); 64 | return h[2] ? h[0]() : h[0]; 65 | }; 66 | 67 | const useBoxes = (targets, deps) => { 68 | return useBox(() => { 69 | let ret = Array.isArray(targets) ? [] : {}; 70 | Object.keys(targets).forEach(key => { 71 | ret[key] = get(targets[key]) 72 | }); 73 | return ret; 74 | }, deps); 75 | }; 76 | 77 | 78 | return { 79 | component, 80 | useBox, 81 | useBoxes, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.js$": "babel-jest", 4 | "^.+\\.mjs$": "babel-jest", 5 | "^.+\\.ts$": "babel-jest", 6 | "^.+\\.tsx$": "babel-jest" 7 | }, 8 | "testEnvironment": "jest-environment-jsdom", 9 | "modulePathIgnorePatterns": [ 10 | "/src/" 11 | ], 12 | "testMatch": [ 13 | "/tests/**/*.test.ts*" 14 | ], 15 | "moduleNameMapper": { 16 | "^remini$": "/src", 17 | "^remini/react$": "/react/src", 18 | "^remini/preact$": "/preact/src", 19 | "^remini/hooks-bindings": "/hooks-bindings/src", 20 | "^htm/preact$": "/node_modules/htm/preact/index.umd.js", 21 | "^htm/react$": "/node_modules/htm/react/index.umd.js", 22 | "^htm$": "/node_modules/htm/dist/htm.umd.js", 23 | "^preact$": "/node_modules/preact/dist/preact.umd.js", 24 | "^@testing-library/preact$": "/node_modules/@testing-library/preact/dist/cjs/index.js", 25 | "^preact/hooks$": "/node_modules/preact/hooks/dist/hooks.umd.js", 26 | "^preact/test-utils$": "/node_modules/preact/test-utils/dist/testUtils.umd.js" 27 | }, 28 | "transformIgnorePatterns": [ 29 | "node_modules/(?!unsubscriber|reactive-box|evemin)" 30 | ], 31 | "coveragePathIgnorePatterns": [ 32 | "node_modules", 33 | "tests" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/re-js/remini/731ae70b2344a463b35313587b7a056f26a15afb/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remini", 3 | "version": "1.4.2", 4 | "description": "Simple and powerful state management in React and Preact", 5 | "repository": { 6 | "url": "https://github.com/re-js/remini" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/re-js/remini/issues" 10 | }, 11 | "homepage": "https://github.com/re-js/remini#readme", 12 | "license": "MIT", 13 | "types": "src/index.d.ts", 14 | "main": "dist/remini.js", 15 | "module": "dist/remini.module.js", 16 | "umd:main": "dist/remini.umd.js", 17 | "source": "src/index.js", 18 | "files": [ 19 | "src", 20 | "dist", 21 | "hooks-bindings/dist", 22 | "hooks-bindings/src", 23 | "hooks-bindings/package.json", 24 | "preact/dist", 25 | "preact/src", 26 | "preact/package.json", 27 | "react/dist", 28 | "react/src", 29 | "react/package.json" 30 | ], 31 | "scripts": { 32 | "test": "jest", 33 | "clear-cache": "jest --clearCache", 34 | "build": "yarn clean && microbundle build --raw --generateTypes false && microbundle build --raw --target web --cwd hooks-bindings --generateTypes false && microbundle build --raw --target node --cwd preact --generateTypes false && microbundle build --raw --target node --cwd react --generateTypes false", 35 | "dev": "microbundle watch --raw --format cjs", 36 | "clean": "rm -rf hooks-bindings/dist preact/dist react/dist dist" 37 | }, 38 | "dependencies": { 39 | "reactive-box": ">=0.9.0 && <3.0.0", 40 | "unsubscriber": ">=2.2.0 && <3.0.0", 41 | "evemin": ">=2.0.0 && <3.0.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/preset-env": "7.23.5", 45 | "@babel/preset-typescript": "7.23.3", 46 | "@testing-library/preact": "3.2.2", 47 | "@testing-library/react": "13.2.0", 48 | "@types/jest": "27.5.0", 49 | "@types/react": "18.0.9", 50 | "htm": "3.1.1", 51 | "jest": "28.1.1", 52 | "jest-environment-jsdom": "28.1.1", 53 | "microbundle": "0.15.1", 54 | "preact": "10.8.1", 55 | "react": "18.1.0", 56 | "react-dom": "18.1.0", 57 | "typescript": "4.7.4" 58 | }, 59 | "peerDependencies": { 60 | "react": ">=16.8", 61 | "preact": ">=10.2" 62 | }, 63 | "peerDependenciesMeta": { 64 | "react": { 65 | "optional": true 66 | }, 67 | "preact": { 68 | "optional": true 69 | } 70 | }, 71 | "author": "Slava Bereza (http://betula.co)", 72 | "keywords": [ 73 | "state", 74 | "model", 75 | "reactive", 76 | "shared state", 77 | "state management", 78 | "react hooks", 79 | "react", 80 | "preact", 81 | "typescript", 82 | "javascript", 83 | "remini", 84 | "minimal", 85 | "minimalistic", 86 | "light", 87 | "small", 88 | "quick", 89 | "any scale", 90 | "fast" 91 | ], 92 | "browserslist": [ 93 | "last 2 Chrome versions" 94 | ], 95 | "sideEffects": false, 96 | "publishConfig": { 97 | "access": "public" 98 | }, 99 | "exports": { 100 | ".": { 101 | "types": "./src/index.d.ts", 102 | "browser": "./dist/remini.module.js", 103 | "umd": "./dist/remini.umd.js", 104 | "import": "./dist/remini.mjs", 105 | "require": "./dist/remini.js" 106 | }, 107 | "./hooks-bindings": { 108 | "types": "./hooks-bindings/src/index.d.ts", 109 | "browser": "./hooks-bindings/dist/hooks-bindings.module.js", 110 | "umd": "./hooks-bindings/dist/hooks-bindings.umd.js", 111 | "import": "./hooks-bindings/dist/hooks-bindings.mjs", 112 | "require": "./hooks-bindings/dist/hooks-bindings.js" 113 | }, 114 | "./preact": { 115 | "types": "./preact/src/index.d.ts", 116 | "browser": "./preact/dist/preact.module.js", 117 | "umd": "./preact/dist/preact.umd.js", 118 | "import": "./preact/dist/preact.mjs", 119 | "require": "./preact/dist/preact.js" 120 | }, 121 | "./react": { 122 | "types": "./react/src/index.d.ts", 123 | "browser": "./react/dist/react.module.js", 124 | "umd": "./react/dist/react.umd.js", 125 | "import": "./react/dist/react.mjs", 126 | "require": "./react/dist/react.js" 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remini-preact", 3 | "version": "0.0.0", 4 | "description": "Preact bindings for Remini", 5 | "private": true, 6 | "license": "MIT", 7 | "types": "src/index.d.ts", 8 | "main": "dist/preact.js", 9 | "module": "dist/preact.module.js", 10 | "umd:main": "dist/preact.umd.js", 11 | "source": "src/index.js", 12 | "peerDependencies": { 13 | "preact": ">=10.2", 14 | "remini": ">=0.2" 15 | }, 16 | "author": "Slava Bereza (http://betula.co)", 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./src/index.d.ts", 21 | "browser": "./dist/preact.module.js", 22 | "umd": "./dist/preact.umd.js", 23 | "import": "./dist/preact.mjs", 24 | "require": "./dist/preact.js" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /preact/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'preact'; 2 | import { Box } from 'remini'; 3 | 4 | type ComponentDecorator = { 5 |

(component: FunctionComponent

): FunctionComponent

; 6 | } 7 | 8 | export declare const component: ComponentDecorator; 9 | export declare const useBox:

(box: Box

) => P; 10 | export declare const useBoxes: { 11 | (boxes: [Box]): [A]; 12 | (boxes: [Box,Box]): [A,B]; 13 | (boxes: [Box,Box,Box]): [A,B,C]; 14 | (boxes: [Box,Box,Box,Box]): [A,B,C,D]; 15 | (boxes: [Box,Box,Box,Box,Box]): [A,B,C,D,E]; 16 | (boxes: [Box,Box,Box,Box,Box,Box]): [A,B,C,D,E,F]; 17 | } 18 | -------------------------------------------------------------------------------- /preact/src/index.js: -------------------------------------------------------------------------------- 1 | import { useReducer, useEffect, useRef, useMemo } from 'preact/hooks'; 2 | import { bindings } from 'remini/hooks-bindings'; 3 | 4 | // 5 | // Exports 6 | // 7 | 8 | const { 9 | component, 10 | useBox, 11 | useBoxes, 12 | } = bindings( 13 | useReducer, 14 | useEffect, 15 | useRef, 16 | useMemo 17 | ); 18 | 19 | export { 20 | component, 21 | useBox, 22 | useBoxes, 23 | } 24 | -------------------------------------------------------------------------------- /react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remini-react", 3 | "version": "0.0.0", 4 | "description": "React bindings for Remini", 5 | "private": true, 6 | "license": "MIT", 7 | "types": "src/index.d.ts", 8 | "main": "dist/react.js", 9 | "module": "dist/react.module.js", 10 | "umd:main": "dist/react.umd.js", 11 | "source": "src/index.js", 12 | "peerDependencies": { 13 | "react": ">=16.8", 14 | "remini": ">=0.2" 15 | }, 16 | "author": "Slava Bereza (http://betula.co)", 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./src/index.d.ts", 21 | "browser": "./dist/react.module.js", 22 | "umd": "./dist/react.umd.js", 23 | "import": "./dist/react.mjs", 24 | "require": "./dist/react.js" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /react/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ForwardRefRenderFunction, MemoExoticComponent } from 'react'; 2 | import { Box } from 'remini'; 3 | 4 | type ComponentMemoDecorator = { 5 |

(component: FC

): MemoExoticComponent>; 6 | } 7 | 8 | type ComponentDecorator = { 9 |

(component: ForwardRefRenderFunction): ForwardRefRenderFunction 10 | } 11 | 12 | export declare const component: ComponentMemoDecorator & { 13 | nomemo: ComponentDecorator 14 | } 15 | export declare const useBox:

(box: Box

) => P; 16 | export declare const useBoxes: { 17 | (boxes: [Box]): [A]; 18 | (boxes: [Box,Box]): [A,B]; 19 | (boxes: [Box,Box,Box]): [A,B,C]; 20 | (boxes: [Box,Box,Box,Box]): [A,B,C,D]; 21 | (boxes: [Box,Box,Box,Box,Box]): [A,B,C,D,E]; 22 | (boxes: [Box,Box,Box,Box,Box,Box]): [A,B,C,D,E,F]; 23 | } 24 | -------------------------------------------------------------------------------- /react/src/index.js: -------------------------------------------------------------------------------- 1 | import { useReducer, useEffect, useRef, useMemo, memo } from 'react'; 2 | import { bindings } from 'remini/hooks-bindings'; 3 | 4 | // 5 | // Exports 6 | // 7 | 8 | const { 9 | component, 10 | useBox, 11 | useBoxes, 12 | } = bindings( 13 | useReducer, 14 | useEffect, 15 | useRef, 16 | useMemo, 17 | memo 18 | ); 19 | 20 | export { 21 | component, 22 | useBox, 23 | useBoxes, 24 | } 25 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'evemin'; 2 | export { event, Event } from 'evemin'; 3 | 4 | type Mode = 'writable' | 'readable'; 5 | 6 | export type Box = [ 7 | () => T, 8 | M extends 'writable' 9 | ? ((value: T) => void) 10 | : ((value: T) => void) | void 11 | ] 12 | 13 | export declare const box: (value: T) => Box; 14 | export declare const update:

(box: Box | BoxFaceWritableClass

, fn: (value: P) => P) => void; 15 | export declare const get:

(box: Box

| BoxFaceClass

) => P; 16 | export declare const getter:

(box: Box

| BoxFaceClass

) => () => P; 17 | export declare const set:

(box: Box | BoxFaceWritableClass

, value: P) => void; 18 | export declare const setter:

(box: Box | BoxFaceWritableClass

) => (value: P) => P; 19 | 20 | export declare const val: typeof get; 21 | export declare const put: typeof set; 22 | 23 | /** @deprecated will be removed in 2.0.0, use "get" method instead */ 24 | export declare const read: typeof get; 25 | 26 | /** @deprecated will be removed in 2.0.0, use "set" method instead */ 27 | export declare const write: typeof set; 28 | 29 | /** @deprecated will be removed in 2.0.0, use "wrap" method instead */ 30 | export declare const select: { 31 | (box: Box

| BoxFaceClass

, fn: (value: P) => R): Box; 32 | } 33 | 34 | 35 | export declare const wrap: { 36 |

( 37 | getter: (() => P) | Box

| BoxFaceClass

, 38 | ): Box

; 39 |

( 40 | getter: (() => P) | Box

| BoxFaceClass

, 41 | setter: ((value: P) => void) | Box | BoxFaceWritableClass

42 | ): Box; 43 | } 44 | 45 | export declare const readonly:

(box: Box

| BoxFaceClass

) => Box

; 46 | 47 | export declare const on: { 48 |

( 49 | target: (() => P) | Box

| Event

| BoxFaceClass

, 50 | listener: (value: P, prev: P | void) => void 51 | ): () => void; 52 | }; 53 | export declare const once: { 54 |

( 55 | target: (() => P) | Box

| Event

| BoxFaceClass

, 56 | listener: (value: P, prev: P | void) => void 57 | ): () => void; 58 | } 59 | export declare const sync: { 60 |

( 61 | target: (() => P) | Box

| BoxFaceClass

, 62 | listener: (value: P, prev: P | void) => void 63 | ): () => void; 64 | } 65 | 66 | type Area = { 67 | (fn: ((...args: M) => T), ...args: M): T; 68 | fn: any)>(fn: M) => M; 69 | } 70 | 71 | export declare const batch: Area; 72 | export declare const untrack: Area; 73 | 74 | type PromiseFunction = { 75 |

( 76 | target: (() => P) | Box

| Event

| BoxFaceClass

77 | ): Promise

; 78 | }; 79 | 80 | export declare const promiseTruthy: PromiseFunction; 81 | export declare const promiseFalsy: PromiseFunction; 82 | export declare const promiseNext: PromiseFunction; 83 | 84 | export declare const waitTruthy: PromiseFunction; 85 | export declare const waitFalsy: PromiseFunction; 86 | export declare const waitNext: PromiseFunction; 87 | 88 | export declare class BoxFaceClass { 89 | 0: () => T; 90 | constructor(getter: () => T); 91 | } 92 | export declare class BoxFaceWritableClass extends BoxFaceClass { 93 | 1: (value: T) => void; 94 | constructor(getter: () => T, setter: (value: T) => void); 95 | } 96 | export declare class BoxClass extends BoxFaceWritableClass { 97 | constructor(value: T); 98 | } 99 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { un, unsubscriber, run, collect } from 'unsubscriber' 2 | import { sel, expr, box, untrack as _re_untrack, batch as _re_batch } from 'reactive-box' 3 | import { event, listen } from 'evemin' 4 | 5 | 6 | const 7 | 8 | // 9 | // Common 10 | // 11 | 12 | _safe_call = (fn, ctx, args, m) => { 13 | const f = m(); 14 | try { return fn.apply(ctx, args) } 15 | finally { f() } 16 | }, 17 | _safe_scope_fn = (m) => ( 18 | (fn) => function () { 19 | return _safe_call(fn, this, arguments, m); 20 | } 21 | ), 22 | _safe_scope = (m) => ( 23 | function () { 24 | return _safe_call(arguments[0], this, Array.prototype.slice.call(arguments, 1), m); 25 | } 26 | ), 27 | 28 | batch = _safe_scope(_re_batch), 29 | batch_fn = batch.fn = _safe_scope_fn(_re_batch), 30 | 31 | untrack = _safe_scope(_re_untrack), 32 | untrack_fn = untrack.fn = _safe_scope_fn(_re_untrack), 33 | 34 | 35 | // 36 | // Entity 37 | // 38 | 39 | wrap = (r, w) => [(r[0] ? r[0] : sel(r)[0])] 40 | // if not w, should be array with one element 41 | .concat(!w ? [] : untrack_fn((v) => w[1] ? w[1](v) : w(v))), 42 | 43 | getter = (r) => r[0], 44 | get = (r) => r[0](), 45 | setter = (r) => r[1], 46 | set = (r, v) => r[1](v), 47 | update = untrack_fn((r, fn) => set(r, fn(get(r)))), 48 | 49 | readonly = (r) => [r[0]], 50 | 51 | 52 | // 53 | // Subscription 54 | // 55 | 56 | _sub_fn = (m /* 1 once, 2 sync */) => (r, fn) => { 57 | let v, off; 58 | if (typeof r === 'function' && r[0]) { 59 | off = listen(r, (d) => { 60 | const prev = v; 61 | fn(v = d, prev); 62 | m === 1 && off(); 63 | }); 64 | un(off); 65 | 66 | } else { 67 | r = r[0] ? r[0] : sel(r)[0]; 68 | const e = expr(r, () => { 69 | const prev = v; 70 | fn(v = m === 1 71 | ? r() 72 | : e[0](), 73 | prev 74 | ); 75 | }); 76 | un(off = e[1]); 77 | v = e[0](); 78 | if (m === 2) untrack(() => fn(v)); 79 | } 80 | 81 | return off; 82 | }, 83 | 84 | on = _sub_fn(), 85 | once = _sub_fn(1), 86 | sync = _sub_fn(2), 87 | 88 | 89 | // 90 | // Waiting 91 | // 92 | 93 | _promise_fn = (m /* 1 truthy, 2 falsy, 3 next */) => (r) => new Promise(ok => { 94 | const 95 | u = unsubscriber(), 96 | stop = () => run(u); 97 | un(stop); 98 | collect(u, () => (m === 3 ? once : sync)(r, (v) => ( 99 | ((m === 1 && v) 100 | || (m === 2 && !v) 101 | || m === 3) 102 | && (stop(), ok(v)) 103 | ))); 104 | }), 105 | 106 | promiseTruthy = _promise_fn(1), 107 | promiseFalsy = _promise_fn(2), 108 | promiseNext = _promise_fn(3), 109 | 110 | waitTruthy = promiseTruthy, 111 | waitFalsy = promiseFalsy, 112 | waitNext = promiseNext, 113 | 114 | 115 | // 116 | // Deprecated, will remove in 2.0.0 117 | // 118 | 119 | put = set, 120 | write = set, 121 | val = get, 122 | read = get, 123 | select = (r, f) => [sel(() => f(get(r)))[0]] 124 | 125 | 126 | // 127 | // Box classes 128 | // 129 | 130 | class BoxFaceClass { 131 | constructor(getter) { 132 | this[0] = getter 133 | } 134 | } 135 | 136 | class BoxFaceWritableClass extends BoxFaceClass { 137 | constructor(getter, setter) { 138 | super(getter) 139 | this[1] = setter 140 | } 141 | } 142 | 143 | class BoxClass extends BoxFaceWritableClass { 144 | constructor(value) { 145 | const b = box(value); 146 | super(b[0], b[1]) 147 | } 148 | }; 149 | 150 | // 151 | // Exports 152 | // 153 | 154 | export { 155 | box, 156 | getter, get, setter, set, update, 157 | wrap, 158 | on, once, sync, 159 | readonly, 160 | batch, untrack, 161 | event, 162 | promiseTruthy, promiseFalsy, promiseNext, 163 | waitTruthy, waitFalsy, waitNext, 164 | un, 165 | 166 | BoxFaceWritableClass, 167 | BoxFaceClass, 168 | BoxClass, 169 | 170 | // deprecated, will remove in 2.0.0 171 | val, put, read, write, select 172 | }; 173 | 174 | 175 | // 176 | // Enjoy and Happy Coding! 177 | // 178 | -------------------------------------------------------------------------------- /tests/box-class.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | get, on, 3 | BoxClass, 4 | update 5 | } from 'remini'; 6 | 7 | class AB extends BoxClass<{ a: number; b: number; }> { 8 | constructor(a, b) { 9 | super({ a, b }) 10 | } 11 | get a() { 12 | return get(this).a; 13 | } 14 | getState() { 15 | return get(this); 16 | } 17 | get b() { 18 | return this.getState().b; 19 | } 20 | } 21 | 22 | describe('class feature', () => { 23 | 24 | test('it works', () => { 25 | const spy = jest.fn(); 26 | const a = new AB(1,5); 27 | expect(a.a).toBe(1); 28 | expect(a.b).toBe(5); 29 | 30 | on(a, state => spy(state)); 31 | expect(spy).toBeCalledTimes(0); 32 | 33 | update(a, state => ({ 34 | ...state, 35 | b: 6, 36 | c: 11 37 | })); 38 | 39 | expect(spy).toBeCalledTimes(1); 40 | expect(spy).toBeCalledWith({ a: 1, b: 6, c: 11 }); 41 | expect(a.b).toBe(6); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /tests/deprecated.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | read, write, select, val, put, 3 | get, set, box, update 4 | } from 'remini'; 5 | 6 | describe('deprecated methods', () => { 7 | 8 | test('val', () => { 9 | expect(val).toBe(get); 10 | }); 11 | 12 | test('put', () => { 13 | expect(put).toBe(set); 14 | }); 15 | 16 | test('write', () => { 17 | expect(write).toBe(set); 18 | }); 19 | 20 | test('read', () => { 21 | expect(read).toBe(get); 22 | }); 23 | 24 | test('select', () => { 25 | const a = box(1); 26 | const b = box(2); 27 | 28 | const k = select(a, (v) => v + 5); 29 | const n = select(k, (v) => '&' + v); 30 | const m = select(b, (v) => v + get(n)); 31 | 32 | expect(get(m)).toBe('2&6'); 33 | set(a, 10); 34 | expect(get(m)).toBe('2&15'); 35 | 36 | update(b, (v) => v + 3); 37 | expect(get(m)).toBe('5&15'); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /tests/event.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | on, once, sync, 3 | event 4 | } from 'remini'; 5 | import { unsubscriber, collect, run } from 'unsubscriber'; 6 | 7 | describe('event feature', () => { 8 | 9 | test('on', () => { 10 | const spy = jest.fn(); 11 | 12 | const e = event(); 13 | 14 | on(e, spy); 15 | expect(spy).toBeCalledTimes(0); 16 | 17 | e(10); 18 | expect(spy).toBeCalledWith(10, void 0); 19 | spy.mockReset(); 20 | 21 | e(10); 22 | expect(spy).toBeCalledWith(10, 10); 23 | spy.mockReset(); 24 | }); 25 | 26 | test('on with un', () => { 27 | const spy = jest.fn(); 28 | 29 | const e = event(); 30 | const unsubs = unsubscriber(); 31 | 32 | collect(unsubs, () => { 33 | on(e, spy); 34 | }); 35 | 36 | expect(spy).toBeCalledTimes(0); 37 | 38 | e(10); 39 | expect(spy).toBeCalledTimes(1); 40 | spy.mockReset(); 41 | 42 | run(unsubs); 43 | 44 | e(10); 45 | expect(spy).toBeCalledTimes(0); 46 | }); 47 | 48 | test('once', () => { 49 | const spy = jest.fn(); 50 | 51 | const e = event(); 52 | 53 | once(e, spy); 54 | expect(spy).toBeCalledTimes(0); 55 | 56 | e(10); 57 | expect(spy).toBeCalledWith(10, void 0); 58 | spy.mockReset(); 59 | 60 | e(10); 61 | expect(spy).not.toBeCalled(); 62 | }); 63 | 64 | test('sync', () => { 65 | const spy = jest.fn(); 66 | 67 | const e = event(); 68 | 69 | sync(e as any, spy); 70 | expect(spy).toBeCalledTimes(0); 71 | 72 | e(10); 73 | expect(spy).toBeCalledWith(10, void 0); 74 | spy.mockReset(); 75 | 76 | e(10); 77 | expect(spy).toBeCalledWith(10, 10); 78 | spy.mockReset(); 79 | }); 80 | }); 81 | 82 | -------------------------------------------------------------------------------- /tests/it-works.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | box, wrap, get, set, update, readonly, 3 | on, once, sync, 4 | batch, untrack, getter 5 | } from 'remini'; 6 | 7 | describe('should works', () => { 8 | 9 | test('re, get, set', () => { 10 | const a = box(0); 11 | set(a, 10); 12 | expect(get(a)).toBe(10); 13 | }); 14 | 15 | test('getter', () => { 16 | const a = box(0); 17 | const fn = getter(a); 18 | expect(fn()).toBe(0); 19 | update(a, (v) => v + 3); 20 | expect(fn()).toBe(3); 21 | }); 22 | 23 | test('update', () => { 24 | const a = box(0); 25 | update(a, (v) => v + 3); 26 | expect(get(a)).toBe(3); 27 | }); 28 | 29 | test('wrap', () => { 30 | const a = box(1); 31 | const b = box(2); 32 | 33 | const k = wrap(a); 34 | const p = wrap(() => get(a) + get(a)); 35 | const n = wrap(a, (v) => set(a, v + 1)); 36 | const m = wrap(() => get(a) + 2, (n) => update(b, (v) => v + n)); 37 | const q = wrap(b, b); 38 | 39 | expect(get(k)).toBe(1); 40 | expect(get(p)).toBe(2); 41 | expect(get(n)).toBe(1); 42 | expect(get(m)).toBe(3); 43 | 44 | set(n, 10); 45 | expect(get(k)).toBe(11); 46 | expect(get(p)).toBe(22); 47 | expect(get(n)).toBe(11); 48 | expect(get(m)).toBe(13); 49 | 50 | set(m, 10); 51 | expect(get(b)).toBe(12); 52 | 53 | update(q, (v) => v + 10); 54 | expect(get(q)).toBe(22); 55 | }); 56 | 57 | test('wrap with one argument should be array with one element', () => { 58 | const a = box(1); 59 | 60 | const k = wrap(a); 61 | const m = wrap(a, a); 62 | 63 | expect(k.length).toBe(1); 64 | expect(m.length).toBe(2); 65 | }); 66 | 67 | test('readonly', () => { 68 | const a = box(1); 69 | const k = readonly(a); 70 | 71 | expect(get(k)).toBe(1); 72 | update(a, (v) => v + 1); 73 | expect(get(k)).toBe(2); 74 | 75 | expect(() => set(k as any, 10)).toThrow(); 76 | expect(() => update(k as any, (v: number) => v + 1)).toThrow(); 77 | }); 78 | 79 | test('on', () => { 80 | const x = jest.fn(); 81 | const y = jest.fn(); 82 | 83 | const a = box(1); 84 | const b = box(2); 85 | 86 | on(() => get(a) + get(b), (v) => x(v)); 87 | on(a, (v) => y(v)); 88 | 89 | expect(x).not.toBeCalled(); 90 | expect(y).not.toBeCalled(); 91 | 92 | set(b, 3); 93 | expect(x).toBeCalledWith(4); x.mockReset(); 94 | 95 | set(a, 5); 96 | expect(x).toBeCalledWith(8); 97 | expect(y).toBeCalledWith(5); 98 | }); 99 | 100 | test('sync', () => { 101 | const x = jest.fn(); 102 | const y = jest.fn(); 103 | 104 | const a = box(1); 105 | const b = box(2); 106 | 107 | sync(() => get(a) + get(b), (v) => x(v)); 108 | sync(a, (v) => y(v)); 109 | 110 | expect(x).toBeCalledWith(3); x.mockReset(); 111 | expect(y).toBeCalledWith(1); y.mockReset(); 112 | 113 | set(b, 3); 114 | expect(x).toBeCalledWith(4); x.mockReset(); 115 | 116 | set(a, 5); 117 | expect(x).toBeCalledWith(8); 118 | expect(y).toBeCalledWith(5); 119 | }); 120 | 121 | test('once', () => { 122 | const x = jest.fn(); 123 | const y = jest.fn(); 124 | 125 | const a = box(1); 126 | 127 | once(() => get(a), (v) => x(v)); 128 | once(a, (v) => y(v)); 129 | 130 | expect(x).not.toBeCalled(); 131 | expect(y).not.toBeCalled(); 132 | 133 | set(a, 3); 134 | expect(x).toBeCalledWith(3); 135 | expect(y).toBeCalledWith(3); 136 | 137 | update(a, (v) => v + 1); 138 | expect(get(a)).toBe(4); 139 | expect(x).toBeCalledTimes(1); 140 | expect(y).toBeCalledTimes(1); 141 | }); 142 | 143 | test('batch', () => { 144 | const spy = jest.fn(); 145 | const x = box(0); 146 | const y = box(0); 147 | 148 | on(() => get(x) + get(y), (v) => spy(v)); 149 | 150 | set(x, 1); 151 | set(y, 1); 152 | expect(spy).toBeCalledTimes(2); spy.mockReset(); 153 | 154 | batch(() => { 155 | set(x, 2); 156 | set(y, 2); 157 | }); 158 | expect(spy).toBeCalledTimes(1); spy.mockReset(); 159 | 160 | const fn = batch.fn((k: number) => { 161 | set(x, k); 162 | set(y, k); 163 | }); 164 | fn(5); 165 | expect(spy).toBeCalledTimes(1); spy.mockReset(); 166 | fn(0); 167 | expect(spy).toBeCalledTimes(1); spy.mockReset(); 168 | 169 | batch(spy, 10, 11, 12); 170 | expect(spy).toBeCalledTimes(1); 171 | expect(spy).toBeCalledWith(10, 11, 12); 172 | spy.mockReset(); 173 | }); 174 | 175 | test('untrack', () => { 176 | const spy = jest.fn(); 177 | const a = box(0); 178 | const b = box(0); 179 | const c = box(0); 180 | 181 | const a_fn = () => get(a); 182 | const b_fn = () => untrack(() => get(b)); 183 | const c_fn = untrack.fn(() => get(c)); 184 | 185 | sync(() => { 186 | a_fn(); 187 | b_fn(); 188 | c_fn(); 189 | return {}; 190 | }, spy); 191 | 192 | spy.mockReset(); 193 | set(a, 1); 194 | expect(spy).toBeCalledTimes(1); spy.mockReset(); 195 | set(b, 1); 196 | set(c, 1); 197 | expect(spy).toBeCalledTimes(0); 198 | 199 | untrack(spy, 10, 11, 12); 200 | expect(spy).toBeCalledTimes(1); 201 | expect(spy).toBeCalledWith(10, 11, 12); 202 | spy.mockReset(); 203 | }); 204 | }); 205 | 206 | -------------------------------------------------------------------------------- /tests/preact/component.test.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import { render, fireEvent, screen } from '@testing-library/preact'; 3 | import { box, get, set } from 'remini'; 4 | import { component } from 'remini/preact'; 5 | 6 | describe('should work preact', () => { 7 | 8 | test('observe', () => { 9 | const spy = jest.fn(); 10 | const b = box(0); 11 | 12 | const A = component(() => { 13 | spy(get(b)); 14 | return html`` 17 | )) 18 | ); 19 | 20 | describe('should work react', () => { 21 | 22 | test('observe', () => { 23 | const spy = jest.fn(); 24 | const h = box(0); 25 | 26 | const A = component(() => { 27 | spy(get(h)); 28 | return html`