├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── codeql │ └── codeql-config.yml └── workflows │ ├── codeql-analysis.yml │ ├── demo-deploy.yml │ ├── npm-publish.yml │ ├── test.yml │ └── webpack.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── example ├── .env ├── .gitignore ├── README.md ├── build.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── components │ ├── App │ │ ├── App.css │ │ ├── App.jsx │ │ └── App.test.jsx │ └── Demo │ │ ├── Demo.css │ │ ├── Demo.jsx │ │ └── Demo.test.jsx │ ├── icons │ ├── github.svg │ └── npm.svg │ ├── index.css │ ├── index.jsx │ ├── reportWebVitals.js │ └── setupTests.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── .eslintrc │ ├── state.test.tsx │ └── trap.test.tsx ├── common.ts ├── compare.ts ├── context.ts ├── index.ts ├── react-app-env.d.ts ├── state.ts ├── trap.ts └── types.ts ├── tsconfig.json └── tsconfig.test.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier", 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "env": { 11 | "node": true 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2020, 15 | "ecmaFeatures": { 16 | "legacyDecorators": true, 17 | "jsx": true 18 | } 19 | }, 20 | "settings": { 21 | "react": { 22 | "version": "16" 23 | } 24 | }, 25 | "rules": { 26 | "space-before-function-paren": 0, 27 | "react/prop-types": 0, 28 | "react/jsx-handler-names": 0, 29 | "react/jsx-fragments": 0, 30 | "react/no-unused-prop-types": 0, 31 | "import/export": 0, 32 | "no-use-before-define": 0, 33 | "no-case-declarations": 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Gareneye] 4 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "Custom CodeQL Config" 2 | 3 | paths-ignore: 4 | - example 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '34 2 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | config-file: ./.github/codeql/codeql-config.yml 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | 53 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 54 | # queries: security-extended,security-and-quality 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/demo-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Demo deploy 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [16.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Prepare 24 | working-directory: . 25 | run: | 26 | npm install 27 | npm run build 28 | cd example && npm install 29 | 30 | - name: Deploy with gh-pages 31 | working-directory: ./example 32 | run: | 33 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 34 | npm run deploy -- -u "bit-about " 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [16.x] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Prepare 24 | run: | 25 | npm install 26 | - name: Test 27 | run: | 28 | npm run test 29 | - name: Codecov 30 | uses: codecov/codecov-action@v2.1.0 31 | with: 32 | directory: coverage 33 | 34 | publish-npm: 35 | needs: build 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-node@v3 40 | with: 41 | node-version: 16 42 | registry-url: https://registry.npmjs.org/ 43 | - run: npm ci 44 | - run: npm publish --access=public 45 | env: 46 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Test 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [ main ] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [16.x] 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Prepare 26 | run: | 27 | npm install 28 | - name: Test 29 | run: | 30 | npm run test 31 | -------------------------------------------------------------------------------- /.github/workflows/webpack.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Prepare 26 | run: | 27 | npm install 28 | 29 | - name: Test 30 | run: | 31 | npm run test 32 | 33 | - name: Codecov 34 | uses: codecov/codecov-action@v2.1.0 35 | with: 36 | directory: coverage 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "arrowParens": "always", 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Maciej Olejnik 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | Bundle size 5 | 6 |

7 | 8 | ## Install 9 | 10 | ```bash 11 | npm i @bit-about/state 12 | ``` 13 | 14 | ## Features 15 | 16 | - 100% **Idiomatic React** 17 | - 100% Typescript with state types deduction 18 | - Efficient **sub-states selectors** 19 | - Get state from a hook... 20 | - ...or utilise static access 21 | - No centralized state provider 22 | - Tiny - only **1.4kB** 23 | - **Just works** ™ 24 | 25 | ### ➡️ [Check demo](https://bit-about.github.io/state/) 26 | 27 | ## Usage 28 | 29 | ```tsx 30 | import { useState } from 'react' 31 | import { state } from '@bit-about/state' 32 | 33 | // 1️⃣ Create a hook-based store 34 | const [Provider, useStore] = state(() => { 35 | const [alice, setAlice] = useState('Alice') 36 | const [bob, setBob] = useState('Bob') 37 | 38 | return { alice, setAlice, bob, setBob } 39 | }) 40 | 41 | // 2️⃣ Wrap tree with Provider 42 | const App = () => ( 43 | 44 | 45 | 46 | ) 47 | ``` 48 | 49 | and then 50 | ```tsx 51 | // 3️⃣ Use the selector hook in component 52 | const Child = () => { 53 | const alice = useStore(state => state.alice) 54 | 55 | return

{alice}

56 | } 57 | ``` 58 | 59 | ## State selectors 60 | 61 | Access fine-grained control to the specific part of your state to re-render **only when necessary**. 62 | 63 | ```tsx 64 | // 👍 Re-render when anything changed 65 | const { alice, bob } = useStore() 66 | 67 | // 💪 Re-render when alice changed 68 | const alice = useStore(state => state.alice) 69 | 70 | // 🤌 Re-render when alice or bob changed 71 | const [alice, bob] = useStore(state => [state.alice, state.bob]) 72 | 73 | // or 74 | const { alice, bob } = useStore( 75 | state => ({ alice: state.alice, bob: state.bob }) 76 | ) 77 | ``` 78 | 79 | > NOTE: **Values** in objects and arrays created on the fly are shallow compared. 80 | 81 | ## Static store 82 | 83 | The third element of the `state()` result tuple is a `store` object. Store is a static helper which provides access to the state **without a hook**. 84 | 85 | ```tsx 86 | const [Provider, useStore, store] = state(/* ... */) 87 | ``` 88 | 89 | and then 90 | ```tsx 91 | // 👍 Get whole state 92 | const { alice } = store.get() 93 | 94 | // 💪 Get substate 95 | const alice = store 96 | .select(state => state.alice) 97 | .get() 98 | 99 | // 🤌 Subscribe to the store and listen for changes 100 | const subscription = store 101 | .select(state => state.alice) 102 | .subscribe(alice => console.log(alice)) 103 | 104 | // remember to unsubscribe! 105 | subscription.unsubscribe() 106 | ``` 107 | 108 | ## State props 109 | 110 | The state hook allows you to pass any arguments into the context. It can be some initial state or you could even return it and pass it through to the components. All state prop changes will update the context and trigger component re-rendering **only when necessary**. 111 | 112 | ```tsx 113 | import { useState } from 'react' 114 | import { getUserById } from '../utils' 115 | 116 | const [UserProvider, useUser] = state(props => { 117 | const [user] = useState(() => getUserById(props.id)) 118 | return user 119 | }) 120 | 121 | const UserProfile = () => ( 122 | 123 | {/* ... */} 124 | 125 | ) 126 | ``` 127 | 128 | ## 👉 Functions in state 129 | 130 | Please remember that functions defined without `React.useCallback` create themselves from scratch every time - which results in incorrect comparisons and components think the state has changed so they re-render themselves. 131 | 132 | ```tsx 133 | import { useState, useCallback } from 'react' 134 | 135 | const [Provider, useStore] = state(() => { 136 | const [counter, setCounter] = useState(0); 137 | 138 | // ✖️ It will re-render components every time 139 | // const incrementCounter = () => setCounter(value => value + 1) 140 | 141 | const incrementCounter = useCallback( 142 | () => setCounter(value => value + 1), 143 | [setCounter] 144 | ) 145 | 146 | return { counter, incrementCounter } 147 | }) 148 | ``` 149 | 150 | ## BitAboutState 💛 [BitAboutEvent](https://github.com/bit-about/event) 151 | 152 | Are you tired of sending logic to the related components?
153 | Move your bussiness logic to the hook-based state using `@bit-about/state` + `@bit-about/event`.
154 | 155 | Now you've got **completely type-safe side-effects**. Isn't that cool? 156 | 157 | ```tsx 158 | import { useState } from 'react' 159 | import { state } from '@bit-about/state' 160 | import { useEvent } from './auth-events' // @bit-about/event hook 161 | import User from '../models/user' 162 | 163 | const [UserProvider, useUser] = state(() => { 164 | const [user, setUser] = useState(null) 165 | 166 | useEvent({ 167 | userLogged: (user: User) => setUser(user), 168 | userLoggout: () => setUser(null) 169 | }) 170 | 171 | return user 172 | }) 173 | ``` 174 | 175 | ## BitAboutState 💛 [React Query](https://github.com/tannerlinsley/react-query) 176 | 177 | ```tsx 178 | import { useQuery } from 'react-query' 179 | import { fetchUser } from './user' 180 | 181 | const useUserQuery = (id) => useQuery(['user', id], () => fetchUser(id)) 182 | 183 | const [UserProvider, useUser] = state(props => { 184 | const { data: user } = useUserQuery(props.id) 185 | return user 186 | }) 187 | 188 | const UserProfile = () => ( 189 | 190 | {/* ... */} 191 | 192 | ) 193 | 194 | // 🧠 Re-render ONLY when user avatar changed (no matter if isLoading changes) 195 | const avatar = useUser(state => state.user.avatar) 196 | ``` 197 | 198 | ## Partners 199 | wayfdigital.com 200 | 201 | ## Credits 202 | - [Constate](https://github.com/diegohaz/constate) - approach main inspiration 203 | - [use-context-selector](https://github.com/dai-shi/use-context-selector) & [FluentUI](https://github.com/microsoft/fluentui) - fancy re-render avoiding tricks and code main inspiration 204 | 205 | ## License 206 | MIT © [Maciej Olejnik 🇵🇱](https://github.com/macoley) 207 | 208 | ## Support me 209 | 210 | Support me! 211 | 212 | If you use my library and you like it...
213 | it would be nice if you put the name `BitAboutState` in the work experience section of your resume.
214 | Thanks 🙇🏻! 215 | 216 | 217 | --- 218 |

🇺🇦 Slava Ukraini

219 | -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Demo @bit-about/state 2 | 3 | Live preview: [https://bit-about.github.io/state](https://bit-about.github.io/state/). 4 | 5 | --- 6 | 7 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 8 | 9 | ## Available Scripts 10 | 11 | In the project directory, you can run: 12 | 13 | ### `npm start` 14 | 15 | Runs the app in the development mode.\ 16 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 17 | 18 | The page will reload when you make changes.\ 19 | You may also see any lint errors in the console. 20 | 21 | ### `npm test` 22 | 23 | Launches the test runner in the interactive watch mode.\ 24 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 25 | 26 | ### `npm run build` 27 | 28 | Builds the app for production to the `build` folder.\ 29 | It correctly bundles React in production mode and optimizes the build for the best performance. 30 | 31 | The build is minified and the filenames include the hashes.\ 32 | Your app is ready to be deployed! 33 | 34 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. -------------------------------------------------------------------------------- /example/build.js: -------------------------------------------------------------------------------- 1 | const rewire = require('rewire') 2 | const defaults = rewire('react-scripts/scripts/build.js') 3 | const config = defaults.__get__('config') 4 | 5 | /** 6 | * Do not mangle component names in production 7 | * @link https://kentcdodds.com/blog/profile-a-react-app-for-performance#disable-function-name-mangling 8 | */ 9 | config.optimization.minimizer[0].options.extractComments = false 10 | config.optimization.minimizer[0].options.minimizer.options.keep_classnames = true 11 | config.optimization.minimizer[0].options.minimizer.options.keep_fnames = true 12 | config.optimization.minimizer[0].options.minimizer.options.output.comments = true 13 | 14 | // Force development env 15 | config.plugins[4].definitions['process.env'].NODE_ENV = '"development"' 16 | config.mode = 'development' 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bit-about/state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@bit-about/state": "file:..", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "@types/react": "^16.9.27", 11 | "@types/react-dom": "^16.9.7", 12 | "gh-pages": "^3.2.3", 13 | "react": "file:../node_modules/react", 14 | "react-dom": "file:../node_modules/react-dom", 15 | "react-scripts": "^5.0.0", 16 | "react-transition-group": "^4.4.2", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "predeploy": "npm run build", 21 | "deploy": "gh-pages -d build", 22 | "start": "react-scripts start", 23 | "build": "node ./build.js", 24 | "postbuild": "react-snap", 25 | "test": "react-scripts test" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "homepage": "https://bit-about.github.io/state", 46 | "devDependencies": { 47 | "react-snap": "^1.23.0", 48 | "rewire": "^6.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleystack/state/bdab3dc08940b47e89fa91fd14d75d0ff151975e/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | @bit-about/state 🚀 Tiny and powerful React hook-based state management library. 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleystack/state/bdab3dc08940b47e89fa91fd14d75d0ff151975e/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleystack/state/bdab3dc08940b47e89fa91fd14d75d0ff151975e/example/public/logo512.png -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "@bit-about/state", 3 | "name": "@bit-about/state 🚀 Tiny and powerful React hook-based state management library.", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#282c34", 24 | "background_color": "#282c34" 25 | } 26 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | text-align: center; 3 | padding: 1em; 4 | padding-bottom: 3em; 5 | display: flex; 6 | min-height: 100vh; 7 | align-items: center; 8 | justify-content: center; 9 | flex-direction: column; 10 | position: relative; 11 | } 12 | 13 | .app-container { 14 | width: 100%; 15 | max-width: 996px; 16 | } 17 | 18 | .app-header { 19 | background-color: #282c34; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: calc(10px + 2vmin); 25 | color: white; 26 | } 27 | 28 | .title { 29 | font-size: 2em; 30 | margin: 0; 31 | margin-bottom: 10px; 32 | } 33 | 34 | .desc { 35 | font-size: smaller; 36 | margin: 0; 37 | } 38 | 39 | ul.menu { 40 | list-style: none; 41 | margin: 0; 42 | padding: 0; 43 | margin-top: 1.5em; 44 | } 45 | 46 | ul.menu li { 47 | display: inline; 48 | margin: 0 0.5em; 49 | } 50 | 51 | .footer { 52 | position: absolute; 53 | bottom: 0; 54 | left: 0; 55 | right: 0; 56 | margin: 0; 57 | padding: 0; 58 | line-height: 3em; 59 | color: rgb(171, 171, 171); 60 | font-size: 0.8em; 61 | } 62 | 63 | @media only screen and (max-width: 600px) { 64 | .app-container { 65 | margin-top: 4em; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/src/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import Demo from '../Demo/Demo' 2 | import { ReactComponent as Github } from '../../icons/github.svg' 3 | import { ReactComponent as Npm } from '../../icons/npm.svg' 4 | import './App.css' 5 | 6 | function App() { 7 | return ( 8 |
9 |
10 |
11 |

@bit-about/state

12 |

13 | 🚀 Tiny and powerful React hook-based state management library. 14 |

15 | 47 |
48 | 49 |
50 |
51 | ) 52 | } 53 | 54 | export default App 55 | -------------------------------------------------------------------------------- /example/src/components/App/App.test.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import App from './App' 3 | 4 | it('renders without crashing', () => { 5 | const div = document.createElement('div') 6 | ReactDOM.render(, div) 7 | ReactDOM.unmountComponentAtNode(div) 8 | }) 9 | -------------------------------------------------------------------------------- /example/src/components/Demo/Demo.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | border: 1px solid #40434a; 4 | margin: 1em; 5 | padding: 1em; 6 | display: flex; 7 | flex: 1; 8 | } 9 | 10 | .demo { 11 | margin-top: 4em; 12 | margin-bottom: 0; 13 | } 14 | 15 | .container-title { 16 | font-size: 0.8em; 17 | font-weight: bold; 18 | position: absolute; 19 | margin-top: -1.2em; 20 | top: 0; 21 | left: 0.5em; 22 | 23 | background-color: #282c34; 24 | padding: 0.5em; 25 | color: rgb(171, 171, 171); 26 | text-transform: uppercase; 27 | } 28 | 29 | .container-info { 30 | font-size: 0.8em; 31 | position: absolute; 32 | margin-top: -1.2em; 33 | top: 0; 34 | right: 0.5em; 35 | 36 | background-color: #282c34; 37 | padding: 0.5em; 38 | color: rgb(171, 171, 171); 39 | } 40 | 41 | .container-info span { 42 | display: inline-block; 43 | margin-left: 0.5em; 44 | } 45 | 46 | .row { 47 | display: flex; 48 | flex-direction: row; 49 | } 50 | 51 | .column { 52 | display: flex; 53 | flex-direction: column; 54 | } 55 | 56 | .desktop-right { 57 | justify-content: flex-end; 58 | } 59 | 60 | .center { 61 | justify-content: center; 62 | } 63 | 64 | @media only screen and (max-width: 600px) { 65 | .row:not(.forced) { 66 | flex-direction: column; 67 | } 68 | 69 | .container ~ .container { 70 | margin-top: 0.5em; 71 | } 72 | 73 | .desktop-right { 74 | justify-content: center; 75 | } 76 | } 77 | 78 | .bump-enter { 79 | opacity: 0; 80 | transform: scale(2); 81 | } 82 | .bump-exit { 83 | opacity: 1; 84 | transform: scale(1); 85 | } 86 | .bump-enter-active { 87 | opacity: 1; 88 | transform: scale(1); 89 | } 90 | .bump-exit-active { 91 | opacity: 0; 92 | transform: scale(2); 93 | } 94 | .bump-enter-active, 95 | .bump-exit-active { 96 | transition: all 300ms; 97 | } 98 | -------------------------------------------------------------------------------- /example/src/components/Demo/Demo.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react' 2 | import { state } from '@bit-about/state' 3 | import { SwitchTransition, CSSTransition } from 'react-transition-group' 4 | import './Demo.css' 5 | import { useCallback } from 'react' 6 | 7 | /** 8 | * Extracted code from @bit-about/state store creator 9 | * for presenting useSideEffect purpose. 10 | * autoIncrementJohn is controlled by internal unexpected "force" 11 | * which is common called sideEffect. 12 | * 13 | * When you wrap this function using useSideEffect 14 | * you can control changes in Redux DevTools 15 | */ 16 | function useAutoIncrementJohn() { 17 | const [autoIncrementJohn, setAutoIncrementJohn] = useState(0) 18 | 19 | const incrementJohn = useCallback(() => setAutoIncrementJohn((value) => value + 1), [setAutoIncrementJohn]) 20 | 21 | useEffect(() => { 22 | const interval = setInterval(incrementJohn, 5000) 23 | return () => clearInterval(interval) 24 | }, [incrementJohn]) 25 | 26 | return autoIncrementJohn 27 | } 28 | 29 | /** 30 | * @bit-about/state 31 | */ 32 | const [StoreProvider, useStore] = state(({ alice: initialAlice }) => { 33 | const [alice, setAlice] = useState(initialAlice) 34 | const [bob, setBob] = useState(0) 35 | 36 | // Side effects example 37 | const autoIncrementJohn = useAutoIncrementJohn() 38 | 39 | return { 40 | alice, 41 | setAlice, 42 | bob, 43 | setBob, 44 | autoIncrementJohn 45 | } 46 | }) 47 | 48 | /** 49 | * Used for testing rerending component scenario 50 | */ 51 | const useForceUpdate = () => { 52 | const [, set] = useState() 53 | return () => set({}) 54 | } 55 | 56 | /** 57 | * Render counter 58 | */ 59 | const RenderCounter = () => { 60 | const renderCounter = useRef(1) 61 | 62 | useEffect(() => { 63 | renderCounter.current = renderCounter.current + 1 64 | }) 65 | 66 | return ( 67 | 68 | 🔄 69 | 70 | 73 | node.addEventListener('transitionend', done, false) 74 | } 75 | classNames='bump' 76 | > 77 | {renderCounter.current} 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | /** 85 | * COMPONENT_1 86 | */ 87 | function AliceBox() { 88 | const [alice, setAlice] = useStore((state) => [state.alice, state.setAlice]) 89 | 90 | return ( 91 |
92 | component_2 93 | 94 | 95 |

alice: {alice}

96 |
97 | 100 |
101 | ) 102 | } 103 | 104 | /** 105 | * COMPONENT_2 106 | */ 107 | function BobBox() { 108 | const [bob, setBob] = useStore((state) => [state.bob, state.setBob]) 109 | 110 | return ( 111 |
112 | component_2 113 | 114 | 115 |

bob: {bob}

116 |
117 | 120 |
121 | ) 122 | } 123 | 124 | /** Store preview */ 125 | function StorePreview() { 126 | const { alice, bob, autoIncrementJohn } = useStore() 127 | 128 | return ( 129 | 130 |

alice: {alice}

131 |

bob: {bob}

132 |

autoIncrementJohn: {autoIncrementJohn}

133 |
134 | ) 135 | } 136 | 137 | /** 138 | * main component aka APP 139 | */ 140 | function Demo() { 141 | const [alice, setAlice] = useState(10) 142 | const forceUpdate = useForceUpdate() 143 | 144 | return ( 145 |
146 | app 147 | 148 | 149 |
150 | 156 | 159 |
160 | 161 | 162 |
163 | store 164 | 165 | 166 |
167 | 168 | 169 |
170 |
171 |
172 |
173 | ) 174 | } 175 | 176 | export default Demo 177 | -------------------------------------------------------------------------------- /example/src/components/Demo/Demo.test.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import Demo from './Demo' 3 | 4 | it('renders without crashing', () => { 5 | const div = document.createElement('div') 6 | ReactDOM.render(, div) 7 | ReactDOM.unmountComponentAtNode(div) 8 | }) 9 | -------------------------------------------------------------------------------- /example/src/icons/github.svg: -------------------------------------------------------------------------------- 1 | GitHub -------------------------------------------------------------------------------- /example/src/icons/npm.svg: -------------------------------------------------------------------------------- 1 | npm -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #282c34; 9 | color: #fff; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | } 20 | 21 | .button { 22 | background: #1d8cf8; 23 | background-image: linear-gradient(to bottom left, #1d8cf8, #3358f4, #1d8cf8); 24 | background-size: 210% 210%; 25 | background-position: 100% 0; 26 | color: #fff; 27 | box-shadow: 2px 2px 6px rgb(0 0 0 / 40%); 28 | text-align: center; 29 | padding: 8px 17px; 30 | font-weight: 400; 31 | border-radius: 0.25rem; 32 | border: none; 33 | margin: 1em; 34 | cursor: pointer; 35 | } 36 | 37 | .code-preview { 38 | background-color: #1e2126; 39 | padding: 0.5em 1.5em; 40 | margin: 0.5em; 41 | text-align: left; 42 | color: #80899c; 43 | border: 1px solid #343740; 44 | } 45 | 46 | .code-preview p { 47 | margin: 0.3em 0; 48 | font-size: 0.7em; 49 | } 50 | -------------------------------------------------------------------------------- /example/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import reportWebVitals from './reportWebVitals' 4 | 5 | import App from './components/App/App' 6 | import './index.css' 7 | 8 | const rootElement = document.getElementById('root') 9 | if (rootElement.hasChildNodes()) { 10 | ReactDOM.hydrate(, rootElement) 11 | } else { 12 | ReactDOM.render(, rootElement) 13 | } 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(console.log) 19 | -------------------------------------------------------------------------------- /example/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /example/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bit-about/state", 3 | "version": "1.3.1", 4 | "description": "Tiny and powerfull state managment library.", 5 | "author": "Gareneye", 6 | "license": "MIT", 7 | "repository": "github:bit-about/state", 8 | "main": "dist/index.js", 9 | "module": "dist/index.modern.js", 10 | "source": "src/index.ts", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "build": "microbundle --compress", 16 | "start": "microbundle watch --no-compress", 17 | "prepare": "run-s build", 18 | "test": "run-s test:unit test:lint test:build", 19 | "test:build": "run-s build", 20 | "test:lint": "eslint ./src", 21 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom --coverage", 22 | "test:watch": "react-scripts test --env=jsdom" 23 | }, 24 | "peerDependencies": { 25 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0", 26 | "scheduler": "^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0" 27 | }, 28 | "devDependencies": { 29 | "@testing-library/jest-dom": "^5.16.5", 30 | "@testing-library/react": "^13.4.0", 31 | "@testing-library/user-event": "^14.4.3", 32 | "@types/jest": "^29.2.5", 33 | "@types/node": "^12.12.38", 34 | "@types/react": "^18.0.26", 35 | "@types/react-dom": "^18.0.10", 36 | "@typescript-eslint/eslint-plugin": "^5.17.0", 37 | "@typescript-eslint/parser": "^5.17.0", 38 | "babel-eslint": "^10.1.0", 39 | "cross-env": "^7.0.3", 40 | "eslint": "^7.12.1", 41 | "eslint-config-prettier": "^8.5.0", 42 | "eslint-config-standard": "^16.0.3", 43 | "eslint-config-standard-react": "^11.0.1", 44 | "eslint-plugin-import": "^2.25.4", 45 | "eslint-plugin-node": "^11.1.0", 46 | "eslint-plugin-prettier": "^4.0.0", 47 | "eslint-plugin-promise": "^5.0.0", 48 | "eslint-plugin-react": "^7.29.0", 49 | "eslint-plugin-standard": "^5.0.0", 50 | "microbundle": "^0.15.1", 51 | "npm-run-all": "^4.1.5", 52 | "prettier": "^2.6.1", 53 | "react": "^ 18.2.0", 54 | "react-dom": "^18.2.0", 55 | "react-scripts": "^5.0.0", 56 | "typescript": "^4.6.3" 57 | }, 58 | "files": [ 59 | "dist" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/state.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { fireEvent, render } from '@testing-library/react' 3 | import state from '../state' 4 | import { StateSelector } from '../types' 5 | 6 | // Counter Component 7 | const Counter = ({ role = 'counter' }: { role: string }) => { 8 | const renderCounter = React.useRef(0) 9 | renderCounter.current = renderCounter.current + 1 10 | return

{renderCounter.current}

11 | } 12 | 13 | test('Basic usage', () => { 14 | const VALUE = 'Alice' 15 | const [Provider, useStore] = state(() => { 16 | const [value] = React.useState(VALUE) 17 | return { value } 18 | }) 19 | 20 | const Child = () => { 21 | const { value } = useStore() 22 | return

{value}

23 | } 24 | 25 | const App = () => ( 26 | 27 | 28 | 29 | ) 30 | const { getByText } = render() 31 | expect(getByText(VALUE)).toBeDefined() 32 | }) 33 | 34 | test('Rerender', () => { 35 | const [Provider, useStore] = state(() => { 36 | const [value, setValue] = React.useState(0) 37 | return { value, setValue } 38 | }) 39 | 40 | const Button = () => { 41 | const { setValue } = useStore() 42 | return