├── .changeset └── config.json ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── packages.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── .yarn └── releases │ └── yarn-3.2.2.cjs ├── .yarnrc.yml ├── README.md ├── babel.config.json ├── examples └── next-notifications │ ├── .eslintrc.json │ ├── index.d.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── pages │ ├── _app.tsx │ ├── index.tsx │ └── styles.css │ ├── postcss.config.js │ ├── project.json │ ├── tailwind.config.js │ └── tsconfig.json ├── jest.config.ts ├── jest.preset.js ├── nx.json ├── package.json ├── packages ├── adapters │ ├── .babelrc │ ├── .eslintrc.json │ ├── .npmrc │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.spec.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── yarn.lock ├── core │ ├── .babelrc │ ├── .eslintrc.json │ ├── .npmrc │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.spec.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── yarn.lock ├── docs │ ├── babel.config.js │ ├── blog │ │ ├── 2019-05-28-first-blog-post.md │ │ ├── 2019-05-29-long-blog-post.md │ │ ├── 2021-08-01-mdx-blog-post.mdx │ │ ├── 2021-08-26-welcome │ │ │ ├── docusaurus-plushie-banner.jpeg │ │ │ └── index.md │ │ └── authors.yml │ ├── docs │ │ ├── about │ │ │ ├── _category_.json │ │ │ ├── goals.mdx │ │ │ ├── license.mdx │ │ │ └── motivation.mdx │ │ ├── advanced │ │ │ ├── _category_.json │ │ │ ├── async.mdx │ │ │ ├── broadcasting.mdx │ │ │ ├── extensions.mdx │ │ │ ├── good-practices.mdx │ │ │ ├── local-storage.mdx │ │ │ ├── middlewares.mdx │ │ │ ├── nesting.mdx │ │ │ └── now-or-draft.mdx │ │ ├── api-reference │ │ │ ├── @superstate │ │ │ │ ├── _category_.json │ │ │ │ ├── core │ │ │ │ │ ├── _category_.json │ │ │ │ │ └── superstate.mdx │ │ │ │ └── react │ │ │ │ │ ├── _category_.json │ │ │ │ │ └── useSuperState.mdx │ │ │ └── _category_.json │ │ ├── examples │ │ │ ├── _category_.json │ │ │ ├── index.mdx │ │ │ ├── notifications-system.mdx │ │ │ ├── rpg-game.mdx │ │ │ ├── text-editor.mdx │ │ │ └── todo.mdx │ │ ├── getting-started │ │ │ ├── _category_.json │ │ │ ├── adopting.mdx │ │ │ ├── drafts.mdx │ │ │ ├── first-state.mdx │ │ │ ├── installation.mdx │ │ │ ├── react.mdx │ │ │ └── typescript.mdx │ │ └── intro.mdx │ ├── docusaurus.config.js │ ├── postcss.config.js │ ├── project.json │ ├── sidebars.js │ ├── src │ │ ├── components │ │ │ ├── code.tsx │ │ │ ├── intro-cards.module.css │ │ │ └── intro-cards.tsx │ │ └── css │ │ │ └── custom.css │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── docusaurus.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── logo.svg │ │ │ ├── logo_without_copy.svg │ │ │ └── tutorial │ │ │ ├── docsVersionDropdown.png │ │ │ └── localeDropdown.png │ ├── tailwind.config.js │ └── tsconfig.json ├── react │ ├── .babelrc │ ├── .eslintrc.json │ ├── .npmrc │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── yarn.lock └── util │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── is-running-on-server.ts │ │ └── messenger.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── tools ├── generators │ └── .gitkeep ├── scripts │ ├── build.ts │ └── release.ts └── tsconfig.tools.json ├── tsconfig.base.json ├── tw ├── colors.js ├── preset.js └── style.css ├── workspace.json └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/packages.yml: -------------------------------------------------------------------------------- 1 | name: Packages 2 | 3 | on: 4 | repository_dispatch: 5 | branches: 6 | - main 7 | 8 | push: 9 | paths: 10 | - packages/core/** 11 | - packages/react/** 12 | - packages/adapters/** 13 | 14 | branches: 15 | - main 16 | 17 | concurrency: ${{ github.workflow }}-${{ github.ref }} 18 | 19 | permissions: 20 | pull-requests: write 21 | issues: write 22 | contents: write 23 | actions: write 24 | packages: write 25 | deployments: write 26 | statuses: write 27 | 28 | jobs: 29 | packages: 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | matrix: 34 | node-version: [16.x] 35 | 36 | steps: 37 | - uses: actions/checkout@v3 38 | with: 39 | fetch-depth: 0 40 | persist-credentials: false 41 | 42 | - name: Setup Node.js environment 43 | uses: actions/setup-node@v3 44 | with: 45 | node-version: 16 46 | registry-url: https://registry.npmjs.org/ 47 | scope: '@superstate' 48 | cache: 'npm' 49 | 50 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 51 | uses: nrwl/nx-set-shas@v2.0.2 52 | 53 | - name: Install dependencies 54 | run: yarn install --frozen-lockfile 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | 58 | - name: Lint 59 | run: npx nx affected --target=lint --parallel=3 60 | 61 | - name: Test 62 | run: npx nx affected --target=test --parallel=3 63 | 64 | - name: Create Release Pull Request 65 | id: changesets 66 | uses: changesets/action@v1 67 | with: 68 | publish: npx nx affected --target=release --parallel=3 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 72 | YARN_TOKEN: ${{ secrets.YARN_TOKEN }} 73 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | /tmp 6 | /out-tsc 7 | .yarn/* 8 | !.yarn/releases 9 | 10 | # dependencies 11 | node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | 43 | # Remix files 44 | apps/**/build 45 | apps/**/.cache 46 | 47 | # Generated Docusaurus files 48 | .docusaurus/ 49 | .cache-loader/ 50 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | registry=https://registry.npmjs.org/ 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .prettierignore 2 | .docusaurus/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": true, 8 | "trailingComma": "es5", 9 | "useTabs": false, 10 | "endOfLine": "lf", 11 | "overrides": [ 12 | { 13 | "files": "*.md", 14 | "options": { 15 | "singleQuote": false, 16 | "quoteProps": "preserve" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "firsttris.vscode-jest-runner" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: 'node_modules' 2 | yarnPath: .yarn/releases/yarn-3.2.2.cjs 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | _Is it a bird? Is it a plane? No, it is **superstate**!_ 4 | 5 |
6 | 7 | ![superstate hero](https://i.imgur.com/EhecV7G.png) 8 | 9 |
10 | 11 | ### Behold your next favorite state management library for the XXI century. 12 | 13 | [Quick Start](https://superstate.dev/getting-started/first-state) · [Documentation](https://superstate.dev) · [Examples](https://superstate.dev/examples) · [API Reference](https://superstate.dev/api-reference/@superstate/core/superstate) 14 |
15 | 16 | --- 17 | 18 | ```shell 19 | yarn add @superstate/core 20 | ``` 21 | 22 | --- 23 | 24 | #### ✨ **Simple, sleek & elegant API** 25 | 26 | ```ts 27 | import { superstate } from "@superstate/core" 28 | 29 | const count = superstate(0) 30 | 31 | count.set(5) 32 | 33 | console.log(count.now()) // 5 34 | ``` 35 | 36 | _Yep, that simple. No PhD required._ 37 | 38 | #### 🤯 Mindlessly easy to share state across your entire app 39 | 40 | ```ts 41 | // count.ts 42 | import { superstate } from "@superstate/core" 43 | 44 | export const count = superstate(0) 45 | 46 | // calculator.ts 47 | import { count } from "./count.ts" 48 | 49 | count.set(5) 50 | 51 | // app.ts 52 | import { count } from "./count.ts" 53 | 54 | // When calculator.ts changes count, it'll call the callback below! :D 55 | count.subscribe((value) => { 56 | console.log(value) 57 | }) 58 | ``` 59 | 60 | _The trick is the `export` keyword: to share your state, all you have to do is exporting and importing it._ 61 | 62 | #### 📐 Designed with ergonomy and developer wellness in mind 63 | 64 | **superstate** puts the developer first—from the beginner to the veteran. No changes are made without thorough consideration and confidence that working with superstate is pure pleasure. 65 | 66 | #### 🧩 Fully extensible 67 | 68 | Instead of building **superstate** with lots of useless opinionated features, the idea is to give you the tools to expand it as much as you need—and only _when_ you need. Middlewares and Extensions are at your service. 69 | 70 | #### 📝 Built-in Drafts System 71 | 72 | Great UX doesn't have to be a burden to build. With the built-in Drafts System, you can give your users second chances without spending an extra day on it. 73 | 74 | ```ts 75 | const count = superstate(0) 76 | 77 | count.sketch(5) 78 | console.log(count.draft()) // 5 79 | console.log(count.now()) // 0 80 | 81 | count.publish() 82 | console.log(count.draft()) // undefined 83 | console.log(count.now()) // 5 84 | ``` 85 | 86 | #### 🤘 Simple enough for prototypes and scalable enough for the world's next big thing 87 | 88 | Regardless of the scale of your project, **superstate** _will_ fit. It is very compact for prototypes, but if your app hits big, **superstate** follows along. Have an ongoing application? It's all right—**superstate** can be incrementally adopted so you don't have to worry if the first rails are already built. 89 | 90 | #### ⌨️ Proudly written in TypeScript 91 | 92 | Building **superstate** without TypeScript was never an option. 93 | 94 | #### ⚛️ Deadly simple integration with React apps 95 | 96 | ```tsx 97 | import { superstate } from '@superstate/core' 98 | import { useSuperState } from '@superstate/react' 99 | 100 | const count = superstate(0) 101 | 102 | export const MyComponent = () => { 103 | useSuperState(count) 104 | 105 | return ( 106 |
107 |
Count: ${count}
108 | 109 |
110 | ) 111 | } 112 | ``` 113 | 114 | You read it right: calling the `useSuperState(count)` hook is all you need to make your component re-render when `count` increases. 115 | 116 | #### 🌐 Works in the browser or in Node environments 117 | 118 | Browser-only apps, hybrid apps (such as [Electron](https://www.electronjs.org/)), Node apps... check, check, check! 119 | 120 | --- 121 | 122 | #### Brought to you by 🇧🇷 [Guilherme "chiefGui" Oderdenge](https://github.com/chiefGui). 123 | 124 | --- 125 | 126 | The MIT License (MIT) 127 | 128 | Copyright (c) 2022 Guilherme Oderdenge 129 | 130 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 131 | 132 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 133 | 134 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 135 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/next-notifications/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nrwl/nx/react-typescript", 4 | "../../.eslintrc.json", 5 | "next", 6 | "next/core-web-vitals" 7 | ], 8 | "ignorePatterns": ["!**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": [ 14 | "error", 15 | "examples/next-notifications/pages" 16 | ] 17 | } 18 | }, 19 | { 20 | "files": ["*.ts", "*.tsx"], 21 | "rules": {} 22 | }, 23 | { 24 | "files": ["*.js", "*.jsx"], 25 | "rules": {} 26 | } 27 | ], 28 | "env": { 29 | "jest": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/next-notifications/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any 4 | export const ReactComponent: any 5 | export default content 6 | } 7 | -------------------------------------------------------------------------------- /examples/next-notifications/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next-notifications/next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withNx = require('@nrwl/next/plugins/with-nx') 3 | 4 | /** 5 | * @type {import('@nrwl/next/plugins/with-nx').WithNxOptions} 6 | **/ 7 | const nextConfig = { 8 | nx: { 9 | // Set this to true if you would like to to use SVGR 10 | // See: https://github.com/gregberge/svgr 11 | svgr: false, 12 | }, 13 | } 14 | 15 | module.exports = withNx(nextConfig) 16 | -------------------------------------------------------------------------------- /examples/next-notifications/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | 3 | import { AppProps } from 'next/app' 4 | import Head from 'next/head' 5 | 6 | function CustomApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | <> 9 | 10 | Welcome to next-notifications! 11 | 12 |
13 | 14 |
15 | 16 | ) 17 | } 18 | 19 | export default CustomApp 20 | -------------------------------------------------------------------------------- /examples/next-notifications/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { superstate } from '@superstate/core' 2 | import { useSuperState } from '@superstate/react' 3 | 4 | const notifications = { 5 | state: superstate([]), 6 | 7 | notify(message: string) { 8 | const id = Math.random().toString() 9 | 10 | notifications.state.set((prev) => [...prev, { id, message }]) 11 | 12 | setTimeout(() => notifications.destroy(id), 3000) 13 | }, 14 | 15 | destroy(id: string) { 16 | notifications.state.set((prev) => prev.filter((n) => n.id !== id)) 17 | }, 18 | 19 | get() { 20 | return notifications.state.now() 21 | }, 22 | } 23 | 24 | export default function Index() { 25 | useSuperState(notifications.state) 26 | 27 | return ( 28 |
29 |
30 | 35 |
36 | 37 | {notifications.get().length > 0 && ( 38 |
    39 | {notifications.get().map((n) => { 40 | return ( 41 |
  • 44 | {n.message} 45 |
  • 46 | ) 47 | })} 48 |
49 | )} 50 |
51 | ) 52 | } 53 | 54 | interface Notification { 55 | id: string 56 | message: string 57 | } 58 | -------------------------------------------------------------------------------- /examples/next-notifications/pages/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | @tailwind base; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | background: black; 8 | color: white; 9 | } 10 | -------------------------------------------------------------------------------- /examples/next-notifications/postcss.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: { 6 | config: join(__dirname, 'tailwind.config.js'), 7 | }, 8 | autoprefixer: {}, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /examples/next-notifications/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "examples/next-notifications", 4 | "projectType": "application", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/next:build", 8 | "outputs": ["{options.outputPath}"], 9 | "defaultConfiguration": "production", 10 | "options": { 11 | "root": "examples/next-notifications", 12 | "outputPath": "dist/examples/next-notifications" 13 | }, 14 | "configurations": { 15 | "development": {}, 16 | "production": {} 17 | } 18 | }, 19 | "serve": { 20 | "executor": "@nrwl/next:server", 21 | "defaultConfiguration": "development", 22 | "options": { 23 | "buildTarget": "next-notifications:build", 24 | "dev": true 25 | }, 26 | "configurations": { 27 | "development": { 28 | "buildTarget": "next-notifications:build:development", 29 | "dev": true 30 | }, 31 | "production": { 32 | "buildTarget": "next-notifications:build:production", 33 | "dev": false 34 | } 35 | } 36 | }, 37 | "export": { 38 | "executor": "@nrwl/next:export", 39 | "options": { 40 | "buildTarget": "next-notifications:build:production" 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/examples/next-notifications"], 46 | "options": { 47 | "jestConfig": "examples/next-notifications/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | }, 51 | "lint": { 52 | "executor": "@nrwl/linter:eslint", 53 | "outputs": ["{options.outputFile}"], 54 | "options": { 55 | "lintFilePatterns": ["examples/next-notifications/**/*.{ts,tsx,js,jsx}"] 56 | } 57 | } 58 | }, 59 | "tags": ["scope:examples"] 60 | } 61 | -------------------------------------------------------------------------------- /examples/next-notifications/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { createGlobPatternsForDependencies } = require('@nrwl/react/tailwind') 2 | const { join } = require('path') 3 | 4 | module.exports = { 5 | content: [ 6 | join( 7 | __dirname, 8 | '{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}' 9 | ), 10 | ...createGlobPatternsForDependencies(__dirname), 11 | ], 12 | presets: [require('../../tw/preset.js')], 13 | theme: { 14 | extend: {}, 15 | }, 16 | plugins: [], 17 | } 18 | -------------------------------------------------------------------------------- /examples/next-notifications/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "types": ["jest", "node"] 15 | }, 16 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], 17 | "exclude": ["node_modules", "jest.config.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nrwl/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "nx/presets/core.json", 3 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 4 | "npmScope": "superstate", 5 | "affected": { 6 | "defaultBase": "main" 7 | }, 8 | "cli": { 9 | "defaultCollection": "@nrwl/react" 10 | }, 11 | "tasksRunnerOptions": { 12 | "default": { 13 | "runner": "nx/tasks-runners/default", 14 | "options": { 15 | "cacheableOperations": ["build", "lint", "test", "e2e"] 16 | } 17 | } 18 | }, 19 | "generators": { 20 | "@nrwl/react": { 21 | "application": { 22 | "style": "none", 23 | "linter": "eslint", 24 | "babel": true 25 | }, 26 | "component": { 27 | "style": "none" 28 | }, 29 | "library": { 30 | "style": "none", 31 | "linter": "eslint" 32 | } 33 | }, 34 | "@nrwl/next": { 35 | "application": { 36 | "style": "css", 37 | "linter": "eslint" 38 | } 39 | } 40 | }, 41 | "targetDependencies": { 42 | "dev": [ 43 | { 44 | "target": "build", 45 | "projects": "dependencies" 46 | } 47 | ], 48 | "start": [ 49 | { 50 | "target": "build", 51 | "projects": "self" 52 | } 53 | ] 54 | }, 55 | "defaultProject": "docs" 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@superstate/monorepo", 3 | "description": "superstate monorepo", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "publishConfig": { 7 | "registry": "https://registry.npmjs.org" 8 | }, 9 | "scripts": { 10 | "build:core": "nx build core --configuration=production", 11 | "build:adapters": "nx build adapters --configuration=production", 12 | "test:core": "nx test core" 13 | }, 14 | "alias": { 15 | "@superstate/core": "@superstate/core/src/index.ts", 16 | "@superstate/react": "@superstate/react/src/index.ts" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@codesandbox/sandpack-react": "^1.1.3", 21 | "@docusaurus/core": "^2.0.0-rc.1", 22 | "@docusaurus/plugin-google-gtag": "^2.0.0-rc.1", 23 | "@docusaurus/preset-classic": "^2.0.0-rc.1", 24 | "@fontsource/plus-jakarta-sans": "^4.5.8", 25 | "@mdx-js/loader": "^2.1.1", 26 | "@mdx-js/react": "^1.6.21", 27 | "@next/mdx": "^12.1.6", 28 | "clsx": "^1.1.1", 29 | "core-js": "^3.6.5", 30 | "next": "12.1.5", 31 | "next-seo": "^5.4.0", 32 | "prism-react-renderer": "^1.2.1", 33 | "react": "18.1.0", 34 | "react-div-100vh": "^0.7.0", 35 | "react-dom": "18.1.0", 36 | "react-scroll": "^1.8.7", 37 | "react-syntax-highlighter": "^15.5.0", 38 | "regenerator-runtime": "0.13.7", 39 | "tailwind-merge": "^1.2.1", 40 | "tslib": "^2.3.0" 41 | }, 42 | "devDependencies": { 43 | "@changesets/cli": "^2.22.0", 44 | "@docusaurus/module-type-aliases": "2.0.0-beta.13", 45 | "@nrwl/cli": "^14.2.4", 46 | "@nrwl/cypress": "^14.2.4", 47 | "@nrwl/eslint-plugin-nx": "^14.2.4", 48 | "@nrwl/jest": "^14.2.4", 49 | "@nrwl/js": "^14.2.4", 50 | "@nrwl/linter": "^14.2.4", 51 | "@nrwl/next": "^14.2.4", 52 | "@nrwl/node": "^14.4.2", 53 | "@nrwl/react": "^14.3.1", 54 | "@nrwl/storybook": "^14.2.4", 55 | "@nrwl/web": "^14.2.4", 56 | "@nrwl/workspace": "^14.2.4", 57 | "@nx-plus/docusaurus": "^14.0.0", 58 | "@testing-library/react": "13.1.1", 59 | "@types/edit-json-file": "^1.7.0", 60 | "@types/jest": "27.4.1", 61 | "@types/lodash": "^4.14.182", 62 | "@types/node": "^18.0.3", 63 | "@types/react": "18.0.8", 64 | "@types/react-dom": "18.0.3", 65 | "@types/react-scroll": "^1.8.3", 66 | "@types/react-syntax-highlighter": "^15.5.1", 67 | "@types/rimraf": "^3.0.2", 68 | "@typescript-eslint/eslint-plugin": "~5.18.0", 69 | "@typescript-eslint/parser": "~5.18.0", 70 | "autoprefixer": "^10.4.7", 71 | "babel-jest": "27.5.1", 72 | "chalk": "^5.0.1", 73 | "concurrently": "^7.2.1", 74 | "cypress": "^9.1.0", 75 | "edit-json-file": "^1.7.0", 76 | "eslint": "~8.12.0", 77 | "eslint-config-next": "12.1.5", 78 | "eslint-config-prettier": "8.1.0", 79 | "eslint-plugin-cypress": "^2.10.3", 80 | "eslint-plugin-import": "2.26.0", 81 | "eslint-plugin-jsx-a11y": "6.5.1", 82 | "eslint-plugin-react": "7.29.4", 83 | "eslint-plugin-react-hooks": "4.5.0", 84 | "jest": "27.5.1", 85 | "microbundle": "^0.15.0", 86 | "npm-run-all": "^4.1.5", 87 | "nx": "^14.2.2", 88 | "postcss": "^8.4.14", 89 | "postcss-flexbugs-fixes": "^5.0.2", 90 | "postcss-preset-env": "^7.7.1", 91 | "prettier": "^2.5.1", 92 | "react-test-renderer": "18.1.0", 93 | "release-it": "^15.0.0", 94 | "rimraf": "^3.0.2", 95 | "tailwindcss": "^3.1.4", 96 | "ts-jest": "27.1.4", 97 | "ts-node": "^10.8.1", 98 | "typescript": "^4.7.4" 99 | }, 100 | "workspaces": [ 101 | "packages/*" 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /packages/adapters/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/web/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/adapters/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/adapters/.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | registry=https://registry.npmjs.org/ 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /packages/adapters/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @superstate/core 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - d1689a3: Add fallback for SSR applications with proper developer messaging. 8 | 9 | ## 0.0.8 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies [cefee5b] 14 | - @superstate/core@0.0.13 15 | 16 | ## 0.0.7 17 | 18 | ### Patch Changes 19 | 20 | - 5a53d99: remove draft ls after discard 21 | 22 | ## 0.0.6 23 | 24 | ### Patch Changes 25 | 26 | - e9d5f0c: Change the way ls is exported 27 | 28 | ## 0.0.5 29 | 30 | ### Patch Changes 31 | 32 | - 58d770c: Fix localStorage adapter not updating draft upon sketch 33 | 34 | ## 0.0.4 35 | 36 | ### Patch Changes 37 | 38 | - a1196e4: Add draft support to ls adapter 39 | 40 | ## 0.0.3 41 | 42 | ### Patch Changes 43 | 44 | - 8c8ef89: Several API changes 45 | - Updated dependencies [8c8ef89] 46 | - @superstate/core@0.0.12 47 | 48 | ## 0.0.2 49 | 50 | ### Patch Changes 51 | 52 | - 5012709: Approach RC 53 | - Updated dependencies [5012709] 54 | - @superstate/core@0.0.11 55 | 56 | ## 0.0.9 57 | 58 | ### Patch Changes 59 | 60 | - cb780e6: test 61 | 62 | ## 0.0.8 63 | 64 | ### Patch Changes 65 | 66 | - Document IDraft 67 | 68 | ## 0.0.7 69 | 70 | ### Patch Changes 71 | 72 | - 3a8d304: Approach release once more 73 | 74 | ## 0.0.6 75 | 76 | ### Patch Changes 77 | 78 | - e68dd22: Another approach to release candidate 79 | 80 | ## 0.0.5 81 | 82 | ### Patch Changes 83 | 84 | - 4ec3eea: Introduce release candidate version 85 | -------------------------------------------------------------------------------- /packages/adapters/README.md: -------------------------------------------------------------------------------- 1 | # adapters 2 | -------------------------------------------------------------------------------- /packages/adapters/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'adapters', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/packages/adapters', 15 | } 16 | -------------------------------------------------------------------------------- /packages/adapters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@superstate/adapters", 3 | "version": "0.1.2", 4 | "description": "Adapters to use with superstate", 5 | "main": "src/index.ts", 6 | "publishConfig": { 7 | "access": "public", 8 | "directory": "../../dist/packages/adapters", 9 | "registry": "https://registry.npmjs.org" 10 | }, 11 | "license": "MIT", 12 | "author": { 13 | "name": "Guilherme \"chiefGui\" Oderdenge" 14 | }, 15 | "bugs": "https://github.com/chiefGui/superstate", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/chiefGui/superstate.git", 19 | "directory": "packages/adapters" 20 | }, 21 | "peerDependencies": { 22 | "@superstate/core": "^0.1.1" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^18.0.3" 26 | }, 27 | "keywords": [ 28 | "state", 29 | "state management", 30 | "global state", 31 | "data", 32 | "react state management", 33 | "react state" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/adapters/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "packages/adapters/src", 4 | "projectType": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/workspace:run-commands", 8 | "options": { 9 | "command": "yarn ts-node tools/scripts/build.ts adapters" 10 | } 11 | }, 12 | "release": { 13 | "executor": "@nrwl/workspace:run-commands", 14 | "options": { 15 | "command": "yarn ts-node tools/scripts/release.ts adapters" 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nrwl/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/adapters/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nrwl/jest:jest", 27 | "outputs": ["coverage/packages/adapters"], 28 | "options": { 29 | "jestConfig": "packages/adapters/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | } 33 | }, 34 | "tags": [] 35 | } 36 | -------------------------------------------------------------------------------- /packages/adapters/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { superstate } from '@superstate/core' 2 | 3 | import { ls } from '.' 4 | 5 | describe('ls', () => { 6 | afterEach(() => localStorage.clear()) 7 | 8 | test('stores to localStorage when publish', () => { 9 | const count = superstate(0).use([ls('count')]) 10 | 11 | count.sketch(5).publish() 12 | 13 | expect(localStorage.getItem('count')).toBe('5') 14 | }) 15 | 16 | test('stores to localStorage when set', () => { 17 | const count = superstate(0).use([ls('count')]) 18 | 19 | count.set(5) 20 | 21 | expect(localStorage.getItem('count')).toBe('5') 22 | }) 23 | 24 | test('stores to localStorage more complex, nested objects', () => { 25 | const user = superstate({ 26 | id: 'john', 27 | posts: [{ title: 'hello world' }], 28 | }).use([ls('user')]) 29 | 30 | user 31 | .sketch((prev) => ({ 32 | ...prev, 33 | posts: [...prev.posts, { title: 'goodbye world' }], 34 | })) 35 | .publish() 36 | 37 | expect( 38 | JSON.parse(localStorage.getItem('user') as string).posts 39 | ).toHaveLength(2) 40 | 41 | expect( 42 | JSON.parse(localStorage.getItem('user') as string).posts[1].title 43 | ).toBe('goodbye world') 44 | }) 45 | 46 | test('sets now from localStorage', () => { 47 | localStorage.setItem('count', '5') 48 | 49 | const count = superstate(0).use([ls('count')]) 50 | 51 | expect(count.now()).toBe(5) 52 | }) 53 | 54 | test('stores draft on localStorage', () => { 55 | const count = superstate(0).use([ls('count', { draft: true })]) 56 | 57 | count.sketch(5) 58 | 59 | expect(localStorage.getItem('count__draft')).toBe('5') 60 | }) 61 | 62 | test('removes localStorage draft item upon discard', () => { 63 | const count = superstate(0).use([ls('count', { draft: true })]) 64 | 65 | count.sketch(5) 66 | expect(localStorage.getItem('count__draft')).toBe('5') 67 | 68 | count.discard() 69 | expect(localStorage.getItem('count__draft')).toBe(null) 70 | }) 71 | 72 | test('removes localStorage draft item upon publish', () => { 73 | const count = superstate(0).use([ls('count', { draft: true })]) 74 | 75 | count.sketch(5) 76 | expect(localStorage.getItem('count__draft')).toBe('5') 77 | 78 | count.publish() 79 | expect(localStorage.getItem('count__draft')).toBe(null) 80 | }) 81 | 82 | test('sets draft from localStorage', () => { 83 | localStorage.setItem('count__draft', '10') 84 | 85 | const count = superstate(0).use([ls('count', { draft: true })]) 86 | 87 | expect(count.draft()).toBe(10) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/adapters/src/index.ts: -------------------------------------------------------------------------------- 1 | import { IMiddlewareInput } from '@superstate/core' 2 | import { isRunningOnServer, panic } from '@/util' 3 | 4 | const LS_DRAFT_SUFFIX = '__draft' 5 | 6 | /** 7 | * superstate's localStorage adapter. A middleware 8 | * to automatically load from/write to localStorage. 9 | * 10 | * @param key The `localStorage` key to save to. 11 | */ 12 | export function ls(key: string, options?: ILSOptions) { 13 | return ({ eventType, now, set, sketch, draft }: IMiddlewareInput) => { 14 | if (eventType === 'init') { 15 | if (isRunningOnServer()) { 16 | return panic({ 17 | message: { 18 | what: 'Currently, the `ls` adapter is not supported on the server or server-side rendered applications.', 19 | solutions: ['Remove the `ls()` middleware from your superstate.'], 20 | references: ['https://superstate.dev/advanced/local-storage'], 21 | }, 22 | }) 23 | } 24 | 25 | const foundNowAtLocalStorage = localStorage.getItem(key) 26 | 27 | if (foundNowAtLocalStorage) { 28 | set(JSON.parse(foundNowAtLocalStorage)) 29 | } 30 | 31 | if (options?.draft) { 32 | const foundDraftAtLocalStorage = localStorage.getItem( 33 | `${key}${LS_DRAFT_SUFFIX}` 34 | ) 35 | 36 | if (!foundDraftAtLocalStorage) { 37 | return 38 | } 39 | 40 | sketch(JSON.parse(foundDraftAtLocalStorage)) 41 | } 42 | 43 | return 44 | } 45 | 46 | if (eventType === 'after:change') { 47 | localStorage.setItem(key, JSON.stringify(now())) 48 | } 49 | 50 | if (options?.draft && eventType === 'after:sketch') { 51 | localStorage.setItem(`${key}${LS_DRAFT_SUFFIX}`, JSON.stringify(draft())) 52 | } 53 | 54 | if (options?.draft && eventType === 'after:discard') { 55 | localStorage.removeItem(`${key}${LS_DRAFT_SUFFIX}`) 56 | } 57 | } 58 | } 59 | 60 | interface ILSOptions { 61 | /** 62 | * Whether to persist the draft to localStorage or not. 63 | */ 64 | draft?: boolean 65 | } 66 | -------------------------------------------------------------------------------- /packages/adapters/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "../../dist", 6 | "module": "commonjs", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false 13 | }, 14 | "files": [], 15 | "include": [], 16 | "references": [ 17 | { 18 | "path": "./tsconfig.lib.json" 19 | }, 20 | { 21 | "path": "./tsconfig.spec.json" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/adapters/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/adapters/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/adapters/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | fast-deep-equal@^3.1.3: 6 | version "3.1.3" 7 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 8 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 9 | -------------------------------------------------------------------------------- /packages/core/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/web/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | registry=https://registry.npmjs.org/ 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @superstate/core 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - cefee5b: Improve computed values support on Extensions 8 | 9 | ## 0.0.12 10 | 11 | ### Patch Changes 12 | 13 | - 8c8ef89: Several API changes 14 | 15 | ## 0.0.11 16 | 17 | ### Patch Changes 18 | 19 | - 5012709: Approach RC 20 | 21 | ## 0.0.10 22 | 23 | ### Patch Changes 24 | 25 | - dac7153: test 26 | 27 | ## 0.0.9 28 | 29 | ### Patch Changes 30 | 31 | - cb780e6: test 32 | 33 | ## 0.0.8 34 | 35 | ### Patch Changes 36 | 37 | - Document IDraft 38 | 39 | ## 0.0.7 40 | 41 | ### Patch Changes 42 | 43 | - 3a8d304: Approach release once more 44 | 45 | ## 0.0.6 46 | 47 | ### Patch Changes 48 | 49 | - e68dd22: Another approach to release candidate 50 | 51 | ## 0.0.5 52 | 53 | ### Patch Changes 54 | 55 | - 4ec3eea: Introduce release candidate version 56 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # core 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test core` to execute the unit tests via [Jest](https://jestjs.io). 8 | 9 | ## Running lint 10 | 11 | Run `nx lint core` to execute the lint via [ESLint](https://eslint.org/). 12 | -------------------------------------------------------------------------------- /packages/core/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'core', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/packages/core', 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@superstate/core", 3 | "version": "0.1.1", 4 | "description": "The essential package for superstate, the state management library.", 5 | "main": "src/index.ts", 6 | "publishConfig": { 7 | "access": "public", 8 | "directory": "../../dist/packages/core", 9 | "registry": "https://registry.npmjs.org" 10 | }, 11 | "license": "MIT", 12 | "author": { 13 | "name": "Guilherme \"chiefGui\" Oderdenge" 14 | }, 15 | "bugs": "https://github.com/chiefGui/superstate", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/chiefGui/superstate.git", 19 | "directory": "packages/core" 20 | }, 21 | "dependencies": { 22 | "fast-deep-equal": "^3.1.3" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^18.0.3" 26 | }, 27 | "keywords": [ 28 | "state", 29 | "state management", 30 | "global state", 31 | "data", 32 | "react state management", 33 | "react state" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "packages/core/src", 4 | "projectType": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/workspace:run-commands", 8 | "options": { 9 | "command": "yarn ts-node tools/scripts/build.ts core" 10 | } 11 | }, 12 | "release": { 13 | "executor": "@nrwl/workspace:run-commands", 14 | "options": { 15 | "command": "yarn ts-node tools/scripts/release.ts core" 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nrwl/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["packages/core/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nrwl/jest:jest", 27 | "outputs": ["coverage/packages/core"], 28 | "options": { 29 | "jestConfig": "packages/core/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | } 33 | }, 34 | "tags": [] 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { IMiddlewareInput, superstate } from '.' 2 | 3 | describe('superstate', () => { 4 | test('supports nested superstates', () => { 5 | const hp = superstate(100) 6 | const attributes = superstate({ hp }) 7 | const hero = superstate({ attributes }) 8 | 9 | expect(hero.now().attributes.now().hp.now()).toBe(100) 10 | }) 11 | 12 | describe('now', () => { 13 | test('returns initial value', () => { 14 | const count = superstate(0) 15 | 16 | expect(count.now()).toBe(0) 17 | }) 18 | }) 19 | 20 | describe('set', () => { 21 | test('assigns a new value to `now`', () => { 22 | const count = superstate(0) 23 | 24 | count.set(5) 25 | 26 | expect(count.now()).toBe(5) 27 | }) 28 | }) 29 | 30 | describe('sketch', () => { 31 | test('assigns input value to draft', () => { 32 | const count = superstate(0) 33 | 34 | count.sketch(5) 35 | 36 | expect(count.draft()).toBe(5) 37 | }) 38 | 39 | test('assigns input value to draft based on the previous `draft`', () => { 40 | const count = superstate(0) 41 | 42 | count.sketch(5) 43 | count.sketch((prev) => prev + 5) 44 | 45 | expect(count.draft()).toBe(10) 46 | }) 47 | 48 | test('assigns input value to draft based on the previous `now`', () => { 49 | const count = superstate(10) 50 | 51 | count.sketch((prev) => prev + 10) 52 | 53 | expect(count.draft()).toBe(20) 54 | }) 55 | 56 | test('assigns 0 to the value of the draft', () => { 57 | const count = superstate(10) 58 | 59 | count.sketch(0) 60 | 61 | expect(count.draft()).toBe(0) 62 | }) 63 | 64 | test('assigns the value to the draft without broadcasting changes', () => { 65 | const count = superstate(0) 66 | 67 | const subscriber = jest.fn() 68 | count.subscribe(subscriber, 'draft') 69 | 70 | count.sketch(5, { silent: false }) 71 | expect(subscriber).toHaveBeenCalledTimes(1) 72 | 73 | count.sketch(10, { silent: false }) 74 | expect(subscriber).toHaveBeenCalledTimes(2) 75 | 76 | count.sketch(5, { silent: true }) 77 | expect(subscriber).toHaveBeenCalledTimes(2) 78 | }) 79 | 80 | test('assigns input value to draft and publish in chain', () => { 81 | const count = superstate(0) 82 | 83 | count.sketch(5).publish() 84 | 85 | expect(count.now()).toBe(5) 86 | }) 87 | 88 | test('assigns input value to draft and discard in chain', () => { 89 | const count = superstate(0) 90 | 91 | count.sketch(5).discard() 92 | 93 | expect(count.draft()).toBeUndefined() 94 | expect(count.now()).toBe(0) 95 | }) 96 | }) 97 | 98 | describe('publish', () => { 99 | test('discards the draft after publish', () => { 100 | const count = superstate(0) 101 | 102 | count.sketch(5) 103 | expect(count.draft()).toBe(5) 104 | 105 | count.publish() 106 | expect(count.draft()).toBeUndefined() 107 | }) 108 | 109 | test('sets `now` to be the same value as `draft`', () => { 110 | const count = superstate(0) 111 | 112 | count.sketch(5) 113 | expect(count.now()).toBe(0) 114 | 115 | count.publish() 116 | expect(count.now()).toBe(5) 117 | }) 118 | }) 119 | 120 | describe('discard', () => { 121 | test('discards the draft', () => { 122 | const count = superstate(0) 123 | 124 | count.sketch(5) 125 | expect(count.draft()).toBe(5) 126 | 127 | count.discard() 128 | expect(count.draft()).toBeUndefined() 129 | }) 130 | }) 131 | 132 | describe('subscribe', () => { 133 | test('reacts to `now` changes via publish', () => { 134 | const count = superstate(0) 135 | 136 | const sub = jest.fn() 137 | 138 | count.subscribe(sub) 139 | 140 | count.sketch(5) 141 | count.publish() 142 | 143 | expect(sub).toHaveBeenCalledTimes(1) 144 | }) 145 | 146 | test('reacts to `now` changes via `set`', () => { 147 | const count = superstate(0) 148 | 149 | const sub = jest.fn() 150 | count.subscribe(sub) 151 | 152 | count.set(5) 153 | 154 | expect(sub).toHaveBeenCalledTimes(1) 155 | }) 156 | 157 | test('reacts to `now` when sets 0', () => { 158 | const count = superstate(0) 159 | 160 | const sub = jest.fn() 161 | count.subscribe(sub) 162 | 163 | count.set(1) 164 | count.set(2) 165 | count.set(3) 166 | count.set(0) 167 | 168 | expect(sub).toHaveBeenCalledTimes(4) 169 | }) 170 | 171 | test('reacts to `draft` changes', () => { 172 | const count = superstate(0) 173 | 174 | const sub = jest.fn() 175 | 176 | count.subscribe(sub, 'draft') 177 | 178 | count.sketch(5) 179 | 180 | expect(sub).toHaveBeenCalledTimes(1) 181 | }) 182 | 183 | test('reacts to `draft` when set is 0', () => { 184 | const count = superstate(0) 185 | 186 | const sub = jest.fn() 187 | count.subscribe(sub, 'draft') 188 | 189 | count.sketch(1) 190 | count.sketch(2) 191 | count.sketch(3) 192 | count.sketch(0) 193 | 194 | expect(sub).toHaveBeenCalledTimes(4) 195 | }) 196 | 197 | test('stop reacting when unsubscribe', () => { 198 | const count = superstate(0) 199 | 200 | const sub = jest.fn() 201 | 202 | const unsub = count.subscribe(sub, 'draft') 203 | count.sketch(5) 204 | expect(sub).toHaveBeenCalledTimes(1) 205 | 206 | unsub() 207 | count.sketch(5) 208 | expect(sub).toHaveBeenCalledTimes(1) 209 | }) 210 | }) 211 | 212 | describe('extensions', () => { 213 | test('sets a new value to `now`', () => { 214 | const count = superstate(5).extend({ 215 | sum: ({ set }, num: number) => set((prev) => prev + num), 216 | }) 217 | 218 | count.sum(10) 219 | 220 | expect(count.now()).toBe(15) 221 | }) 222 | 223 | test('changes the draft based on prev draft', () => { 224 | const count = superstate(5).extend({ 225 | sum: ({ sketch }, num: number) => sketch((prev) => prev + num), 226 | }) 227 | 228 | count.sketch(10) 229 | count.sum(5) 230 | 231 | expect(count.draft()).toBe(15) 232 | }) 233 | 234 | test('returns a computed value', () => { 235 | const user = superstate({ firstName: 'John', lastName: 'Doe' }).extend({ 236 | fullName: ({ now }) => `${now().firstName} ${now().lastName}`, 237 | }) 238 | 239 | expect(user.fullName()).toBe('John Doe') 240 | }) 241 | }) 242 | 243 | describe('middlewares', () => { 244 | test('calls a middleware before set', () => { 245 | const mockFn = jest.fn() 246 | 247 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 248 | if (eventType === 'before:set') { 249 | mockFn() 250 | } 251 | } 252 | 253 | const count = superstate(0).use([fakeMiddleware]) 254 | 255 | count.set(10) 256 | 257 | expect(mockFn).toHaveBeenCalledTimes(1) 258 | }) 259 | 260 | test('calls a middleware after set', () => { 261 | const mockFn = jest.fn() 262 | 263 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 264 | if (eventType === 'after:set') { 265 | mockFn() 266 | } 267 | } 268 | 269 | const count = superstate(0).use([fakeMiddleware]) 270 | 271 | count.set(10) 272 | 273 | expect(mockFn).toHaveBeenCalledTimes(1) 274 | }) 275 | 276 | test('calls a middleware before sketch', () => { 277 | const mockFn = jest.fn() 278 | 279 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 280 | if (eventType === 'before:sketch') { 281 | mockFn() 282 | } 283 | } 284 | 285 | const count = superstate(0).use([fakeMiddleware]) 286 | 287 | count.sketch(10) 288 | 289 | expect(mockFn).toHaveBeenCalledTimes(1) 290 | }) 291 | 292 | test('calls a middleware after sketch', () => { 293 | const mockFn = jest.fn() 294 | 295 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 296 | if (eventType === 'after:sketch') { 297 | mockFn() 298 | } 299 | } 300 | 301 | const count = superstate(0).use([fakeMiddleware]) 302 | 303 | count.sketch(10) 304 | 305 | expect(mockFn).toHaveBeenCalledTimes(1) 306 | }) 307 | 308 | test('calls a middleware before publish', () => { 309 | const mockFn = jest.fn() 310 | 311 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 312 | if (eventType === 'before:publish') { 313 | mockFn() 314 | } 315 | } 316 | 317 | const count = superstate(0).use([fakeMiddleware]) 318 | 319 | count.sketch(10).publish() 320 | 321 | expect(mockFn).toHaveBeenCalledTimes(1) 322 | }) 323 | 324 | test('calls a middleware after publish', () => { 325 | const mockFn = jest.fn() 326 | 327 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 328 | if (eventType === 'after:sketch') { 329 | mockFn() 330 | } 331 | } 332 | 333 | const count = superstate(0).use([fakeMiddleware]) 334 | 335 | count.sketch(10).publish() 336 | 337 | expect(mockFn).toHaveBeenCalledTimes(1) 338 | }) 339 | 340 | test('calls a middleware before change (via set)', () => { 341 | const mockFn = jest.fn() 342 | 343 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 344 | if (eventType === 'before:change') { 345 | mockFn() 346 | } 347 | } 348 | 349 | const count = superstate(0).use([fakeMiddleware]) 350 | 351 | count.set(10) 352 | 353 | expect(mockFn).toHaveBeenCalledTimes(1) 354 | }) 355 | 356 | test('calls a middleware after change (via set)', () => { 357 | const mockFn = jest.fn() 358 | 359 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 360 | if (eventType === 'after:change') { 361 | mockFn() 362 | } 363 | } 364 | 365 | const count = superstate(0).use([fakeMiddleware]) 366 | 367 | count.set(10) 368 | 369 | expect(mockFn).toHaveBeenCalledTimes(1) 370 | }) 371 | 372 | test('calls a middleware before change (via publish)', () => { 373 | const mockFn = jest.fn() 374 | 375 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 376 | if (eventType === 'before:change') { 377 | mockFn() 378 | } 379 | } 380 | 381 | const count = superstate(0).use([fakeMiddleware]) 382 | 383 | count.sketch(10).publish() 384 | 385 | expect(mockFn).toHaveBeenCalledTimes(1) 386 | }) 387 | 388 | test('calls a middleware after change (via publish)', () => { 389 | const mockFn = jest.fn() 390 | 391 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 392 | if (eventType === 'after:change') { 393 | mockFn() 394 | } 395 | } 396 | 397 | const count = superstate(0).use([fakeMiddleware]) 398 | 399 | count.sketch(10).publish() 400 | 401 | expect(mockFn).toHaveBeenCalledTimes(1) 402 | }) 403 | 404 | test('calls a middleware before discard', () => { 405 | const mockFn = jest.fn() 406 | 407 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 408 | if (eventType === 'before:discard') { 409 | mockFn() 410 | } 411 | } 412 | 413 | const count = superstate(0).use([fakeMiddleware]) 414 | 415 | count.sketch(10) 416 | count.discard() 417 | 418 | expect(mockFn).toHaveBeenCalledTimes(1) 419 | }) 420 | 421 | test('calls a middleware after discard', () => { 422 | const mockFn = jest.fn() 423 | 424 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 425 | if (eventType === 'after:discard') { 426 | mockFn() 427 | } 428 | } 429 | 430 | const count = superstate(0).use([fakeMiddleware]) 431 | 432 | count.sketch(10) 433 | count.discard() 434 | 435 | expect(mockFn).toHaveBeenCalledTimes(1) 436 | }) 437 | 438 | test('calls a middleware before broadcast now', () => { 439 | const mockFn = jest.fn() 440 | 441 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 442 | if (eventType === 'before:broadcast:now') { 443 | mockFn() 444 | } 445 | } 446 | 447 | const count = superstate(0).use([fakeMiddleware]) 448 | 449 | count.set(30) 450 | 451 | expect(mockFn).toHaveBeenCalledTimes(1) 452 | }) 453 | 454 | test('calls a middleware after broadcast now', () => { 455 | const mockFn = jest.fn() 456 | 457 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 458 | if (eventType === 'after:broadcast:now') { 459 | mockFn() 460 | } 461 | } 462 | 463 | const count = superstate(0).use([fakeMiddleware]) 464 | 465 | count.set(25) 466 | 467 | expect(mockFn).toHaveBeenCalledTimes(1) 468 | }) 469 | 470 | test('calls a middleware before broadcast draft', () => { 471 | const mockFn = jest.fn() 472 | 473 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 474 | if (eventType === 'before:broadcast:draft') { 475 | mockFn() 476 | } 477 | } 478 | 479 | const count = superstate(0).use([fakeMiddleware]) 480 | 481 | count.sketch(30) 482 | 483 | expect(mockFn).toHaveBeenCalledTimes(1) 484 | }) 485 | 486 | test('calls a middleware after broadcast draft', () => { 487 | const mockFn = jest.fn() 488 | 489 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 490 | if (eventType === 'after:broadcast:draft') { 491 | mockFn() 492 | } 493 | } 494 | 495 | const count = superstate(0).use([fakeMiddleware]) 496 | 497 | count.sketch(25) 498 | 499 | expect(mockFn).toHaveBeenCalledTimes(1) 500 | }) 501 | 502 | test('dont call middleware if silent before broadcast now', () => { 503 | const mockFn = jest.fn() 504 | 505 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 506 | if (eventType === 'before:broadcast:now') { 507 | mockFn() 508 | } 509 | } 510 | 511 | const count = superstate(0).use([fakeMiddleware]) 512 | 513 | count.set(30, { silent: true }) 514 | 515 | expect(mockFn).toHaveBeenCalledTimes(0) 516 | }) 517 | 518 | test('dont call middleware if silent after broadcast now', () => { 519 | const mockFn = jest.fn() 520 | 521 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 522 | if (eventType === 'after:broadcast:now') { 523 | mockFn() 524 | } 525 | } 526 | 527 | const count = superstate(0).use([fakeMiddleware]) 528 | 529 | count.set(25, { silent: true }) 530 | 531 | expect(mockFn).toHaveBeenCalledTimes(0) 532 | }) 533 | 534 | test('dont call middleware if silent before broadcast draft', () => { 535 | const mockFn = jest.fn() 536 | 537 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 538 | if (eventType === 'before:broadcast:draft') { 539 | mockFn() 540 | } 541 | } 542 | 543 | const count = superstate(0).use([fakeMiddleware]) 544 | 545 | count.sketch(30, { silent: true }) 546 | 547 | expect(mockFn).toHaveBeenCalledTimes(0) 548 | }) 549 | 550 | test('dont call middleware if silent after broadcast draft', () => { 551 | const mockFn = jest.fn() 552 | 553 | const fakeMiddleware = ({ eventType }: IMiddlewareInput) => { 554 | if (eventType === 'after:broadcast:draft') { 555 | mockFn() 556 | } 557 | } 558 | 559 | const count = superstate(0).use([fakeMiddleware]) 560 | 561 | count.sketch(25, { silent: true }) 562 | 563 | expect(mockFn).toHaveBeenCalledTimes(0) 564 | }) 565 | }) 566 | }) 567 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal' 2 | 3 | /** 4 | * The superstate function. This is the function that you should use to create a superstate. 5 | * 6 | * @param initialState The initial state. 7 | * @returns Methods (`ISuperState`) to work with your state. 8 | */ 9 | export function superstate(initialState: S): ISuperState { 10 | let _now = initialState 11 | let _draft: IDraft = undefined 12 | 13 | let _subscribers: ISubscriber[] = [] 14 | let _draftSubscribers: ISubscriber>[] = [] 15 | 16 | let _middlewares: IMiddleware[] = [] 17 | 18 | const _draftMethods: ISuperStateDraftMethods = { 19 | draft, 20 | sketch, 21 | publish, 22 | discard, 23 | } 24 | 25 | const _methods: ISuperState = { 26 | ..._draftMethods, 27 | 28 | set, 29 | now, 30 | subscribe, 31 | extend, 32 | use, 33 | unsubscribeAll, 34 | } 35 | 36 | function now() { 37 | return _now 38 | } 39 | 40 | function draft() { 41 | return _draft 42 | } 43 | 44 | function set( 45 | input: IMutateInput, 46 | options?: IMutateOptions 47 | ): ISuperState { 48 | if (typeof input === 'undefined' && typeof _draft === 'undefined') { 49 | return _methods 50 | } 51 | 52 | const prevNow = _now 53 | const newNow = _mutate(_now, input) 54 | 55 | if (deepEqual(prevNow, newNow)) { 56 | return _methods 57 | } 58 | 59 | _executeMiddlewares({ eventType: 'before:set' }) 60 | _overrideNow(newNow) 61 | _executeMiddlewares({ eventType: 'after:set' }) 62 | 63 | if (!options?.silent) { 64 | _broadcast() 65 | 66 | return _methods 67 | } 68 | 69 | return _methods 70 | } 71 | 72 | function publish(options?: IMutateOptions): ISuperState { 73 | if (typeof _draft === 'undefined') { 74 | return _methods 75 | } 76 | 77 | const prevNow = _now 78 | const newNow = _draft 79 | 80 | if (deepEqual(prevNow, newNow)) { 81 | return _methods 82 | } 83 | 84 | _executeMiddlewares({ eventType: 'before:publish' }) 85 | _overrideNow(newNow) 86 | _executeMiddlewares({ eventType: 'after:publish' }) 87 | 88 | discard() 89 | 90 | if (!options?.silent) { 91 | _broadcast() 92 | 93 | return _methods 94 | } 95 | 96 | return _methods 97 | } 98 | 99 | function sketch( 100 | input: IMutateInput, 101 | options?: IMutateOptions 102 | ): ISuperState { 103 | const prevDraft = _draft 104 | const newDraft = _mutate(_draft ?? _now, input) 105 | 106 | if (deepEqual(prevDraft, newDraft)) { 107 | return _methods 108 | } 109 | 110 | _executeMiddlewares({ eventType: 'before:sketch' }) 111 | _draft = newDraft 112 | _executeMiddlewares({ eventType: 'after:sketch' }) 113 | 114 | if (!options?.silent) { 115 | _broadcastDraft() 116 | 117 | return _methods 118 | } 119 | 120 | return _methods 121 | } 122 | 123 | function subscribe( 124 | subscriber: ISubscriber, 125 | target?: 'draft' | 'now' 126 | ): IUnsubscribe { 127 | if (target === 'draft') { 128 | return _subscribeDraft(subscriber as ISubscriber>) 129 | } 130 | 131 | const nextIndex = _subscribers.length 132 | 133 | _subscribers.push(subscriber) 134 | 135 | return () => 136 | (_subscribers = [ 137 | ..._subscribers.slice(0, nextIndex), 138 | ..._subscribers.slice(nextIndex + 1, _subscribers.length), 139 | ]) 140 | } 141 | 142 | function discard(options?: IMutateOptions): ISuperState { 143 | if (typeof _draft === 'undefined') { 144 | return _methods 145 | } 146 | 147 | _executeMiddlewares({ eventType: 'before:discard' }) 148 | _draft = undefined 149 | _executeMiddlewares({ eventType: 'after:discard' }) 150 | 151 | if (!options?.silent) { 152 | _broadcastDraft() 153 | 154 | return _methods 155 | } 156 | 157 | return _methods 158 | } 159 | 160 | function unsubscribeAll() { 161 | _subscribers = [] 162 | _draftSubscribers = [] 163 | } 164 | 165 | function use(middlewares: IMiddleware[]): ISuperState { 166 | _executeMiddlewares({ eventType: 'init' }, middlewares) 167 | 168 | _middlewares = [..._middlewares, ...middlewares] 169 | 170 | return _methods 171 | } 172 | 173 | function extend>( 174 | extensions: E 175 | ): ISuperState & IExtensionMethods { 176 | return { ..._methods, ..._prepareExtensions(extensions) } 177 | } 178 | 179 | function _overrideNow(newNow: S) { 180 | _executeMiddlewares({ eventType: 'before:change' }) 181 | _now = newNow 182 | _executeMiddlewares({ eventType: 'after:change' }) 183 | } 184 | 185 | function _mutate(value: S | IDraft, input: IMutateInput) { 186 | const clone = _cloneObj(value) 187 | 188 | return _isMutator(input) ? input(clone) : input 189 | } 190 | 191 | function _executeMiddlewares( 192 | { eventType }: Pick, 'eventType'>, 193 | middlewares?: IMiddleware[] 194 | ) { 195 | const targetMiddlewares = middlewares || _middlewares 196 | 197 | if (!targetMiddlewares.length) { 198 | return 199 | } 200 | 201 | targetMiddlewares.forEach((m) => { 202 | m({ eventType, ..._methods }) 203 | }) 204 | } 205 | 206 | function _prepareExtensions>( 207 | extensions: E 208 | ): IExtensionMethods { 209 | return Object.keys(extensions).reduce((prev, extensionKey) => { 210 | return { 211 | ...prev, 212 | 213 | [extensionKey]: (...params: IExtensionUserParams) => { 214 | const result = extensions[extensionKey]( 215 | _getExtensionProps(), 216 | ...params 217 | ) 218 | 219 | if (typeof result === 'undefined') { 220 | return 221 | } 222 | 223 | return result 224 | }, 225 | } 226 | }, {} as IExtensionMethods) 227 | } 228 | 229 | function _getExtensionProps(): IExtensionPropsBag { 230 | return _methods 231 | } 232 | 233 | function _broadcast() { 234 | _executeMiddlewares({ eventType: 'before:broadcast:now' }) 235 | _subscribers.forEach((s) => s(_now)) 236 | _executeMiddlewares({ eventType: 'after:broadcast:now' }) 237 | } 238 | 239 | function _broadcastDraft() { 240 | _executeMiddlewares({ eventType: 'before:broadcast:draft' }) 241 | _draftSubscribers.forEach((ds) => ds(_draft)) 242 | _executeMiddlewares({ eventType: 'after:broadcast:draft' }) 243 | } 244 | 245 | function _subscribeDraft(subscriber: ISubscriber>) { 246 | const nextIndex = _draftSubscribers.length 247 | 248 | _draftSubscribers.push(subscriber) 249 | 250 | return () => 251 | (_draftSubscribers = [ 252 | ..._draftSubscribers.slice(0, nextIndex), 253 | ..._draftSubscribers.slice(nextIndex + 1, _draftSubscribers.length), 254 | ]) 255 | } 256 | 257 | return _methods 258 | } 259 | 260 | function _cloneObj(inputState: S) { 261 | if (inputState === undefined) { 262 | return undefined 263 | } 264 | 265 | if (inputState instanceof Map) { 266 | return new Map(inputState) 267 | } 268 | 269 | if (inputState instanceof Set) { 270 | return new Set(inputState) 271 | } 272 | 273 | return JSON.parse(JSON.stringify(inputState)) 274 | } 275 | 276 | function _isMutator(value: IMutateInput): value is (prev: S) => S { 277 | return typeof value === 'function' 278 | } 279 | 280 | export interface ISuperState extends ISuperStateDraftMethods { 281 | /** 282 | * @returns The current value of the state. 283 | */ 284 | now: () => S 285 | 286 | /** 287 | * Mutates the value of `now`. If the input is `undefined`, it uses the `draft` value instead. 288 | * 289 | * It won't broadcast any changes if the previous `now` would be equal the new `now`. 290 | */ 291 | set: ISetFn 292 | 293 | /** 294 | * Starts monitoring changes to the state. 295 | * 296 | * @param subscriber A function that will be called when `now` or `draft` changes (depends on `target`). 297 | * @param target Can be either `now` or `draft` (default: `now`) 298 | * @returns A function to unsubscribe. 299 | */ 300 | subscribe: ( 301 | subscriber: ISubscriber, 302 | target?: 'draft' | 'now' | undefined 303 | ) => IUnsubscribe 304 | 305 | /** 306 | * Unsubscribes all `now` and `draft` subscribers. 307 | * 308 | * After you call this method, changes will no longer be 309 | * broadcasted. 310 | */ 311 | unsubscribeAll: () => void 312 | 313 | /** 314 | * Extends superstate with additional methods. 315 | * 316 | * Note: Currently only extensions that mutates the draft are supported. 317 | * 318 | * @param IExtension The extensions to add. 319 | * @returns The extended superstate. 320 | */ 321 | extend: >( 322 | extensions: E 323 | ) => ISuperState & IExtensionMethods 324 | 325 | /** 326 | * Adds a middleware to superstate. 327 | */ 328 | use: IUseFn 329 | } 330 | 331 | export interface ISuperStateDraftMethods { 332 | /** 333 | * @retur The draft version of your state. May be `undefined` if no draft is available. To set the value of the draft, call `.set()`. 334 | */ 335 | draft: IDraftFn 336 | 337 | /** 338 | * Overrides the value of `now` with the value of `draft`. 339 | * 340 | * If `draft` is `undefined`, nothing will happen. 341 | * 342 | * If `draft` is equals to `now`, nothing will happen either. 343 | */ 344 | publish: () => ISuperState 345 | 346 | /** 347 | * Discards the draft, setting its value to `undefined`. 348 | * Upon calling this function, a draft broadcast will occur. 349 | */ 350 | discard: IDiscardFn 351 | 352 | /** 353 | * Assigns the value passed via `input` to `draft`. 354 | * 355 | * @param input It can be a raw value or a function whose first and sole argument is the value of the previous `draft`. In case there is no previous draft, the first argument will be the previous value of `now`. 356 | * @param options.silent (default: `false`) Whether to broadcast the change. 357 | */ 358 | sketch: ISketchFn 359 | } 360 | 361 | export type IExtensionPropsBag = ISuperState 362 | 363 | /** 364 | * The type of the draft. 365 | * Almost the same thing as the state, 366 | * except it can be `undefined`. 367 | */ 368 | export type IDraft = S | undefined 369 | 370 | type IMutateInput = ((prev: S) => S) | S 371 | type IMutateOptions = { 372 | /** 373 | * Whether to broadcast the change or not. 374 | * 375 | * `true`: broadcast the change, 376 | * 377 | * `false`: don't broadcast the change. 378 | */ 379 | silent?: boolean 380 | } 381 | type ISubscriber = (newState: S) => void 382 | 383 | type IExtensionUserParams = any[] 384 | type IExtensionAllParams = [IExtensionPropsBag, ...IExtensionUserParams] 385 | type IExtension = (...params: IExtensionAllParams) => O 386 | type IExtensions = Record> 387 | type IExtensionMethods> = { 388 | [key in keyof E]: ( 389 | ...params: DropFirst> 390 | ) => ReturnType 391 | } 392 | 393 | type IUnsubscribe = () => void 394 | 395 | type ISketchFn = ( 396 | input: IMutateInput, 397 | options?: IMutateOptions 398 | ) => ISuperState 399 | type ISetFn = ( 400 | input: IMutateInput, 401 | options?: IMutateOptions 402 | ) => ISuperState 403 | type IDiscardFn = () => ISuperState 404 | type IDraftFn = () => IDraft 405 | type IUseFn = (middlewares: IMiddleware[]) => ISuperState 406 | 407 | type DropFirst = T extends [any, ...infer U] ? U : never 408 | 409 | export type IMiddlewareInput = ISuperState & { 410 | eventType: IMiddlewareEventType 411 | } 412 | type IMiddleware = (input: IMiddlewareInput) => void 413 | type IMiddlewareEventType = 414 | | 'init' 415 | | 'before:set' 416 | | 'after:set' 417 | | 'before:sketch' 418 | | 'after:sketch' 419 | | 'before:publish' 420 | | 'after:publish' 421 | | 'before:change' 422 | | 'after:change' 423 | | 'before:discard' 424 | | 'after:discard' 425 | | 'before:broadcast:now' 426 | | 'after:broadcast:now' 427 | | 'before:broadcast:draft' 428 | | 'after:broadcast:draft' 429 | | 'before:register' 430 | | 'after:register' 431 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "../../dist", 6 | "module": "commonjs", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false 13 | }, 14 | "files": [], 15 | "include": [], 16 | "references": [ 17 | { 18 | "path": "./tsconfig.lib.json" 19 | }, 20 | { 21 | "path": "./tsconfig.spec.json" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | fast-deep-equal@^3.1.3: 6 | version "3.1.3" 7 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 8 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 9 | -------------------------------------------------------------------------------- /packages/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | } 4 | -------------------------------------------------------------------------------- /packages/docs/blog/2019-05-28-first-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: first-blog-post 3 | title: First Blog Post 4 | authors: 5 | name: Gao Wei 6 | title: Docusaurus Core Team 7 | url: https://github.com/wgao19 8 | image_url: https://github.com/wgao19.png 9 | tags: [hola, docusaurus] 10 | --- 11 | 12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 13 | -------------------------------------------------------------------------------- /packages/docs/blog/2019-05-29-long-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: long-blog-post 3 | title: Long Blog Post 4 | authors: endi 5 | tags: [hello, docusaurus] 6 | --- 7 | 8 | This is the summary of a very long blog post, 9 | 10 | Use a `` comment to limit blog post size in the list view. 11 | 12 | 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 17 | 18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 21 | 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 23 | 24 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 25 | 26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 27 | 28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 29 | 30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 31 | 32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 33 | 34 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 35 | 36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 37 | 38 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 39 | 40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 41 | 42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 43 | 44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 45 | -------------------------------------------------------------------------------- /packages/docs/blog/2021-08-01-mdx-blog-post.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: mdx-blog-post 3 | title: MDX Blog Post 4 | authors: [slorber] 5 | tags: [docusaurus] 6 | --- 7 | 8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). 9 | 10 | :::tip 11 | 12 | Use the power of React to create interactive blog posts. 13 | 14 | ```js 15 | 16 | ``` 17 | 18 | 19 | 20 | ::: 21 | -------------------------------------------------------------------------------- /packages/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiefGui/superstate/2fc54384a242d763833459a60a51ab0a87e7a35e/packages/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg -------------------------------------------------------------------------------- /packages/docs/blog/2021-08-26-welcome/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | authors: [slorber, yangshun] 5 | tags: [facebook, hello, docusaurus] 6 | --- 7 | 8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). 9 | 10 | Simply add Markdown files (or folders) to the `blog` directory. 11 | 12 | Regular blog authors can be added to `authors.yml`. 13 | 14 | The blog post date can be extracted from filenames, such as: 15 | 16 | - `2019-05-30-welcome.md` 17 | - `2019-05-30-welcome/index.md` 18 | 19 | A blog post folder can be convenient to co-locate blog post images: 20 | 21 | ![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) 22 | 23 | The blog supports tags as well! 24 | 25 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. 26 | -------------------------------------------------------------------------------- /packages/docs/blog/authors.yml: -------------------------------------------------------------------------------- 1 | endi: 2 | name: Endilie Yacop Sucipto 3 | title: Maintainer of Docusaurus 4 | url: https://github.com/endiliey 5 | image_url: https://github.com/endiliey.png 6 | 7 | yangshun: 8 | name: Yangshun Tay 9 | title: Front End Engineer @ Facebook 10 | url: https://github.com/yangshun 11 | image_url: https://github.com/yangshun.png 12 | 13 | slorber: 14 | name: Sébastien Lorber 15 | title: Docusaurus maintainer 16 | url: https://sebastienlorber.com 17 | image_url: https://github.com/slorber.png 18 | -------------------------------------------------------------------------------- /packages/docs/docs/about/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "About", 3 | "position": 6 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/about/goals.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Goals 6 | 7 | In short: 8 | 9 | - Sleek and comfortable API design 10 | - Small bundle footprint and impact 11 | - Extensible 12 | - Do one thing, and do it well 13 | - Always plug and play 14 | - Explicitness over implicitness 15 | 16 | --- 17 | 18 | **The ultimate goal of superstate is to make it easy reacting to variable changes.** In order to achieve that, **superstate** has to be compact 19 | and cannot take decisions for you. It wants _you_ to be the chosen one; the admiral; the captain; the one in charge. 20 | 21 | **superstate** is not opinionated either. It favors explicitness over implicitness because debugging time is pure waste. 22 | 23 | **superstate** wants to be a tool. A fun one. Not a chore. 24 | -------------------------------------------------------------------------------- /packages/docs/docs/about/license.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # License 6 | 7 | Copyright (c) 2022 Guilherme Oderdenge 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /packages/docs/docs/about/motivation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Motivation 6 | 7 | In theory, state management should be something simple to deal with. You declare a variable and assign it a value that 8 | we expect to change during the life of our application. 9 | 10 | In practice though, if we think about it, most of the issues our applications have as they grow are somehow related 11 | to bad state management—and this is the main reason why **superstate** was born. 12 | 13 | To me, the author of **superstate**, it feels like the current solutions have gaps. Sometimes they relate to overly 14 | complex solutions for small, simple problems; sometimes they relate to too much unnecessary boilerplate and verbosity. 15 | 16 | What I dislike the most though is the excess of complicated words: context, reducer, store, selector, 17 | derived state, state machines, etc. 18 | 19 | It's too much pressure. And pressure leads to lack of motivation, that leads to 20 | bad code, that leads to unsustainable codebase. A domino effect. 21 | 22 | **superstate** however aims to be accessible. No boilerplate nor complicated words. Designed to be adored by beginners and 23 | to don't put veterans down after a good, deserved vacation time. 24 | 25 | **superstate** is an attempt to fill these mentioned gaps; it features an elegant and sleek API, and is 26 | just enough opinionated to make you as productive as you can be. 27 | 28 | Perhaps you should give it a try. :) 29 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Advanced", 3 | "position": 3 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/async.mdx: -------------------------------------------------------------------------------- 1 | # Async 2 | 3 | Currently, **superstate** does not have anything built-in to automate the work with asynchronous data. 4 | This is an intentional decision because I believe it may not be needed at all. 5 | 6 | Allow me to clarify. I don't think this: 7 | 8 | ```ts 9 | const count = superstate(0) 10 | 11 | async function init() { 12 | const response = await API.get('/count') 13 | 14 | count.set(response.count) 15 | } 16 | 17 | init() 18 | ``` 19 | 20 | Is worse than this: 21 | 22 | ```ts 23 | // For those who skipped the text on this page: 24 | // the snippet below DOES NOT work! 25 | // Please, read the page to understand why. 26 | 27 | const count = superstate(async () => await API.get('count')) 28 | ``` 29 | 30 | And if you go a little creative, you can have a wonderful, explicit API: 31 | 32 | ```ts 33 | const count = { 34 | state: superstate(0), 35 | 36 | sync() { 37 | const response = await API.get('/count') 38 | 39 | count.state.set(response.count) 40 | }, 41 | } 42 | 43 | function init() { 44 | count.sync() 45 | } 46 | 47 | init() 48 | ``` 49 | 50 | You know, I don't think something is being lost by skipping built-in async state. Of course we have more lines of code, 51 | but is it really that bad? Also, it may be worth mentioning that a long-term goal of **superstate** is 52 | [to always favor explicitness over implicitness](/about/goals), and this often means more lines of code. 53 | But that's not necessarily a bad thing though—in this case, having full control and understanding of the code 54 | around your state totally pays off. 55 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/broadcasting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Broadcasting 6 | 7 | If you want to do something when your state changes, broadcasting is the way to go. 8 | 9 | ## Subscribing to changes 10 | 11 | To start listening to a **superstate**, you have to subscribe to it: 12 | 13 | ```ts {5-7} 14 | import { superstate } from '@superstate/core' 15 | 16 | const count = superstate(0) 17 | 18 | count.subscribe((newCount) => { 19 | console.log(newCount) 20 | }) 21 | ``` 22 | 23 | And then, if you set something: 24 | 25 | ```ts 26 | count.set(1) 27 | ``` 28 | 29 | The `console.log(newCount)` will be called. 30 | 31 | By default, your subscriptions will target changes made to `now`. If you want to listen for changes made to the [draft](/getting-started/drafts): 32 | 33 | ```ts {3} 34 | count.subscribe((newDraft) => { 35 | console.log(newDraft) 36 | }, 'draft') // pay attention to the 'draft' here 37 | ``` 38 | 39 | In case you'd like to listen to changes made to both states, `now` and `draft`: 40 | 41 | ```ts {3} 42 | count.subscribe((newDraft) => { 43 | console.log(newDraft) 44 | }, 'all') // pay attention to the 'all' here 45 | ``` 46 | 47 | :::info Remember 48 | If your subscription is targeting a `draft`, only changes made via `.sketch()` will be caught. [Learn more →](/getting-started/drafts) 49 | ::: 50 | 51 | ## Unsubscribing 52 | 53 | If you are not interested in changes anymore, you can call the function returned from `subscribe`: 54 | 55 | ```ts 56 | const unsubscribe = count.subscribe((newCount) => { 57 | console.log(newCount) 58 | }) 59 | 60 | // do whatever you want 61 | 62 | unsubscribe() // from now on, the previous subscriber is no more a subscriber 63 | ``` 64 | 65 | If you're feeling bold and want to unsubscribe all subscribers, you can call `unsubscribeAll`: 66 | 67 | ```ts 68 | count.unsubscribeAll() 69 | ``` 70 | 71 | :::caution 72 | When you call `unsubscribeAll()`, it's literally unsubscribe all—no subscriber is safe. Just use it if you know 73 | what you are doing. 74 | ::: 75 | 76 | ## Exceptions 77 | 78 | There are two exceptions for when changes won't be broadcasted. 79 | One is if the next state value is the same as the previous one—**superstate** understands that nothing has changed, 80 | hence there's no reason to broadcast. 81 | 82 | The other exception is when you explictly specify `{ silent: true }` to your mutation methods. For example: 83 | 84 | ```ts 85 | count.set(1, { silent: true }) // will not broadcast 86 | count.sketch(1, { silent: true }) // will not broadcast 87 | count.publish({ silent: true }) // will not broadcast 88 | count.discard({ silent: true }) // will not broadcast 89 | ``` 90 | 91 | ## React 92 | 93 | If you want your components to re-render when your state changes, it's better to use the `useSuperState()` hook: 94 | 95 | ```tsx {7} 96 | import { superstate } from '@superstate/core' 97 | import { useSuperState } from '@superstate/react' 98 | 99 | const count = superstate(0) 100 | 101 | function MyComponent() { 102 | useSuperState(count) 103 | 104 | // Below it's just a sample effect 105 | // that will publish a change after 1 second 106 | // to demonstrate that this component will re-render 107 | // once the change is published. 108 | useEffect(() => { 109 | setTimeout(() => { 110 | count.publish(10) 111 | }, 1000) 112 | }, []) 113 | 114 | return
{count.now}
115 | } 116 | ``` 117 | 118 | [You can learn more here.](/getting-started/react) 119 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/extensions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Extensions 6 | 7 | When you feel like you're repeating yourself too much or just want to have standard ways to change the value of a 8 | **superstate**, then it's time for extensions! 9 | 10 | Let's suppose you have a `count` superstate: 11 | 12 | ```typescript 13 | const count = superstate(0) 14 | ``` 15 | 16 | Overriding the value of `count` is very straightforward: 17 | 18 | ```typescript 19 | count.set(1) 20 | ``` 21 | 22 | Increasing the value of it is very straightforward as well: 23 | 24 | ```typescript 25 | count.set((prev) => prev + 1) 26 | ``` 27 | 28 | But wouldn't it be magical if you could do the following?: 29 | 30 | ```typescript 31 | count.sum(1) 32 | ``` 33 | 34 | The example above has a few benefits: 35 | 36 | - It's easier to read, 37 | - It's easier to write, 38 | - It's easier to test, 39 | - It's easier to maintain, 40 | - It's less verbose, 41 | - Separates business logic from the implementation. 42 | 43 | ## Creating an extension 44 | 45 | From the example above, to create an extension that sums your `count`: 46 | 47 | ```ts {1-5} 48 | const count = superstate(0).extend({ 49 | sum({ set }, amount: number) { 50 | set((prev) => prev + amount) 51 | }, 52 | }) 53 | 54 | count.sum(1) // `count.now()` will log 1 55 | count.sum(1) // `count.now()` will log 2 56 | count.sum(5) // `count.now()` will log 7 57 | ``` 58 | 59 | Let's understand what's happening here: 60 | 61 | ```ts 62 | sum({ set }, amount: number) { 63 | publish(prev => prev + amount) 64 | } 65 | ``` 66 | 67 | > `sum` 68 | 69 | is the name of your extension. 70 | 71 | > `{ set }` 72 | 73 | is a [`superstate` object](/api-reference/@superstate/core/superstate), which exposes the `set` method. 74 | 75 | :::danger 76 | The first parameter of an extension should always be reserved to the `superstate` object. 77 | ::: 78 | 79 | > `amount: number` 80 | 81 | a custom parameter of the `.sum()` function. This is completely up to us to define and we can have as many parameters 82 | as we like. For example: 83 | 84 | ```ts {1} 85 | sum({ set }, x: number, y: number, z: number) { 86 | set(x + y + z) 87 | } 88 | ``` 89 | 90 | :::info 91 | The "`: number`" piece is TypeScript. You can omit it when writing vanilla JavaScript. 92 | ::: 93 | 94 | > `set(prev => prev + amount)` 95 | 96 | The regular [`set`](/api-reference/@superstate/core/superstate#set) function exposed by a `superstate` object. 97 | 98 | ## Computed values 99 | 100 | Given the example below, 101 | 102 | ```typescript 103 | const user = superstate({ firstName: 'John', lastName: 'Doe' }) 104 | ``` 105 | 106 | It's exhaustive to always be concatenating the first and last name of the user: 107 | 108 | ```typescript 109 | // logs John Doe 110 | console.log(`${user.now().firstName} ${user.now().lastName}`) 111 | ``` 112 | 113 | Extensions got you covered by allowing computed values. Let's see how it works: 114 | 115 | ```typescript 116 | const user = superstate({ firstName: 'John', lastName: 'Doe' }).extend({ 117 | fullName, 118 | }) 119 | 120 | function fullName({ now }) { 121 | const _user = now() 122 | 123 | return `${_user.firstName} ${_user.lastName}` 124 | } 125 | 126 | console.log(user.fullName()) // logs John Doe 127 | ``` 128 | 129 | Just to give you another use-case for computed values: 130 | 131 | ```typescript 132 | const money = superstate(100).extend({ formatted }) 133 | 134 | function formatted({ now, draft }) { 135 | return `$${now()}` 136 | } 137 | 138 | console.log(money.formatted()) // logs $100 139 | ``` 140 | 141 | ## Side effects 142 | 143 | You can also handle side effects within your extension's scope: 144 | 145 | ```typescript 146 | const count = superstate(0).extend({ 147 | sum({ set, now }, amount: number) { 148 | const next = now() + amount 149 | set(next) 150 | 151 | // Feel free to do whatever you like. 152 | alert(`count has changed to ${next}.`) 153 | }, 154 | }) 155 | ``` 156 | 157 | Note that by doing the above, whenever you call `.sum()`, the `alert()` will be called as well—meaning you are the one in charge 158 | for managing your side effect(s) lifecycle. 159 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/good-practices.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Good Practices 6 | 7 | ## Mutating the state based on its previous value 8 | 9 | You don't want to use `count.now()` when referring to the previous value of your state when 10 | mutating it because depending on how you program your application, that value may be inaccurate, i.e. 11 | in asynchronous contexts. 12 | 13 | The safest way to update your state based on its previous value is passing a function to `.set()`, where 14 | the first argument is the current value of your state and making its return to be the next value you want 15 | your state to be. 16 | 17 | ❌ **Don't:** 18 | 19 | ```typescript 20 | const count = superstate(0) 21 | 22 | count.set(count.now() + 1) 23 | ``` 24 | 25 | ✅ **Do:** 26 | 27 | ```typescript 28 | const count = superstate(0) 29 | 30 | count.set((prev) => prev + 1) 31 | ``` 32 | 33 | ## Abstract complex logic with Extensions 34 | 35 | It's tempting to just rush and use the built-in methods to handle your state. They work. However, it may become unsustainable at some point if 36 | you don't abstract complex, not obvious logic into small, organized pieces. [Extensions](/advanced/extensions) are here for you. 37 | 38 | ❌ **Don't:** 39 | 40 | ```typescript 41 | const user = superstate({ 42 | firstName: 'John', 43 | lastName: 'Doe', 44 | slug: 'john-doe', 45 | }) 46 | 47 | user.set((prev) => ({ 48 | ...prev, 49 | 50 | firstName: 'Hello', 51 | lastName: 'World', 52 | slug: slugify('Hello World'), 53 | })) 54 | ``` 55 | 56 | ✅ **Do:** 57 | 58 | ```typescript 59 | const user = { 60 | state: superstate({ firstName: 'John', lastName: 'Doe', slug: 'john-doe' }), 61 | 62 | rename({ firstName, lastName }) { 63 | user.state.set(prev => { 64 | ...prev, 65 | 66 | firstName, 67 | lastName, 68 | slug: slugify(`${firstName} ${lastName}`) 69 | }) 70 | } 71 | } 72 | 73 | user.rename({ firstName: 'Hello', lastName: 'World' }) 74 | ``` 75 | 76 | :::info Types! 77 | Extensions are seamlessly integrated with TypeScript. They show up in your editor's autocomplete as well as their arguments. Take advantage of it! 78 | ::: 79 | 80 | ## Keep your state serializable 81 | 82 | Don't populate your superstates with non-serializable data, such as function, classes, etc. 83 | **superstate** expects its contents to be as agnostic as possible, so data can be handled 84 | in an unopinionated fashion. For example, [localStorage persistency](/advanced/local-storage) wouldn't be 85 | possible out of the box if data was not serializable. [Learn more.](https://en.wikipedia.org/wiki/Serialization) 86 | 87 | ❌ **Don't:** 88 | 89 | ```ts {5-7} 90 | const user = superstate({ 91 | firstName: 'John', 92 | lastName: 'Doe', 93 | slug: 'john-doe', 94 | save() { 95 | // Your save function. 96 | }, 97 | }) 98 | ``` 99 | 100 | ✅ **Do:** 101 | 102 | ```ts {8-10} 103 | const user = { 104 | state: superstate({ 105 | firstName: 'John', 106 | lastName: 'Doe', 107 | slug: 'john-doe', 108 | }), 109 | 110 | save() { 111 | // Your save function. 112 | }, 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/local-storage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import Tabs from '@theme/Tabs' 6 | import TabItem from '@theme/TabItem' 7 | 8 | # Automatic localStorage persistency 9 | 10 | :::danger SSR not supported! 11 | The localStorage adapter currently does not support server-side rendered applications and will throw an 12 | exception if executed on the server. 13 | ::: 14 | 15 | Install the `@superstate/adapters` package: 16 | 17 | 18 | 19 | 20 | ```bash 21 | npm install @superstate/adapters 22 | ``` 23 | 24 | 25 | 26 | 27 | ```bash 28 | yarn add @superstate/adapters 29 | ``` 30 | 31 | 32 | 33 | 34 | And plug the `ls` adapter into your **superstate**: 35 | 36 | ```typescript {4-6} 37 | import { superstate } from '@superstate/core' 38 | import { ls } from '@superstate/adapters' 39 | 40 | const count = superstate(0).use([ 41 | ls('count'), // 'count' is the localStorage key 42 | ]) 43 | ``` 44 | 45 | Now, whenever `now` changes, its value will be persisted against the `localStorage`: 46 | 47 | ```typescript 48 | count.set(5) 49 | 50 | console.log(localStorage.getItem('count')) // logs 5 51 | ``` 52 | 53 | :::danger Limitation 54 | Currently, only data serializeable by `JSON.stringify()` are supported. 55 | ::: 56 | 57 | ## Initial value 58 | 59 | When `ls` is plugged into your state, its initial value will be whatever is stored in the `localStorage` under the key you specified. 60 | For example: 61 | 62 | ```ts 63 | localStorage.setItem('count', 5) 64 | 65 | const count = superstate(0).use([ 66 | ls('count'), // 'count' is the key 67 | ]) 68 | 69 | console.log(count.now()) // logs 5 70 | ``` 71 | 72 | ## Persisting drafts 73 | 74 | If you'd like to persist your drafts alongside the published (`now`) value of your state, you can do so by passing `true` to the `draft` option of `ls`: 75 | 76 | ```ts 77 | const count = superstate(0).use([ls('count', { draft: true })]) 78 | ``` 79 | 80 | This way, every time your `draft` changes, it'll be persisted against the localStorage as well. 81 | 82 | :::info 83 | The key of drafts on localStorage are preffixed by `__draft`. If your key is `count`, then the draft version of it 84 | on localStorage would be `count__draft` 85 | ::: 86 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/middlewares.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Middlewares 6 | 7 | Middlewares are implicit methods to deal with side effects of your superstates. 8 | 9 | Let's say we want to display an `alert` whenever a `superstate` changes. One simple way to achieve it would be through 10 | a simple Middleware: 11 | 12 | ```ts 13 | function myMiddleware({ eventType }) { 14 | if (eventType === 'after:change') { 15 | alert('hello world!') 16 | 17 | return 18 | } 19 | } 20 | 21 | const count = superstate(0).use([myMiddleware]) 22 | 23 | count.set(10) // here your `alert()` would be called 24 | ``` 25 | 26 | And honestly, Middlewares are that simple! 27 | 28 | ## Event Types 29 | 30 | One of the most important aspects of a Middleware is the `eventType` property `superstate` passes to it. The `eventType` 31 | helps you decide what to do and when. 32 | 33 | Here's a list of all the available event types: 34 | 35 | ``` 36 | init 37 | before:set 38 | after:set 39 | before:sketch 40 | after:sketch 41 | before:publish 42 | after:publish 43 | before:change 44 | after:change 45 | before:discard 46 | after:discard 47 | before:broadcast:now 48 | after:broadcast:now 49 | before:broadcast:draft 50 | after:broadcast:draft 51 | ``` 52 | 53 | ### The `init` event type 54 | 55 | The `init` event type is executed once when a Middleware is successfully attached to a **superstate**: 56 | 57 | ```typescript 58 | function initMiddleware({ eventType }) { 59 | if (eventType === 'init') { 60 | alert('initMiddleware was installed successfully!') 61 | 62 | return 63 | } 64 | } 65 | 66 | const count = superstate(0).use([initMiddleware]) // the `alert()` would be called here. 67 | ``` 68 | 69 | May be important to note that `init` won't ever be called again despite this very first time. 70 | 71 | ### `before:change` and `after:change` 72 | 73 | Just like `init`, these `change` events are special, because it has two different instigators. 74 | 75 | It'll be triggered when... 76 | - you `.publish()` successfully and/or, 77 | - you `.set()` successfully. 78 | 79 | :::info 80 | Note that the `change` event will not trigger for changes made to the draft. If you are interested on draft events, 81 | please use `before:sketch` and `after:sketch`. 82 | ::: 83 | 84 | ### Before/After events 85 | 86 | As you may have noticed, some events are prefixed with `before:` or `after:`. 87 | 88 | These events will only trigger when an action was successfully achieved, and only before/after the action itself. 89 | 90 | To illustrate an example, `before:publish` occurs when you call `.publish()`, but *before* the publish method has published 91 | your changes; at the same time, `after:publish` also occurs when you call `.publish()`, except it will be executed *after* 92 | the publish method has published your changes. Both `before:publish` and `after:publish` will only occur if publishing was successful. 93 | 94 | ## Advanced Middlewares 95 | 96 | `eventType` is not the only property exposed to Middlewares. Actually, Middlewares have access to the exact same functions 97 | a `superstate` object has, such as `publish`, `set`, `discard`, etc [(Learn more)](/api-reference/@superstate/core/superstate). To illustrate you an example: 98 | 99 | ```typescript 100 | function myMiddleware({ eventType, set, now }) { 101 | if (eventType === 'after:publish') { 102 | sketch(now()) 103 | } 104 | } 105 | 106 | const count = superstate(0).use([myMiddleware]) 107 | 108 | count.publish(5) 109 | ``` 110 | 111 | The Middleware above will assign your draft with the value of `now()`, which is `5`, every time you `.publish()` something. 112 | 113 | :::danger Infinite loop! 114 | If you call `publish()` inside a Middleware that does something after you `publish()`, expect an infinite loop. That said, don't do that! 115 | ::: 116 | 117 | ## When Middlewares make sense 118 | 119 | From [Wikipedia](https://en.wikipedia.org/wiki/Middleware): 120 | 121 | > Middleware is a type of computer software that provides services to software applications beyond those available from the operating system. It can be described as "software glue". 122 | > 123 | > Middleware makes it easier for software developers to implement communication and input/output, so they can focus on the specific purpose of their application. 124 | 125 | To name you a practical example of where a Middleware can be useful, the [localStorage adapter](/advanced/local-storage) is a Middleware. 126 | Instead of having to manually write/read to/from localStorage, a Middleware was written to do that for us. 127 | 128 | If you need to automate something in your superstates, Middlewares can be the answer. 129 | 130 | ## TypeScript 131 | 132 | ```typescript 133 | function myMiddleware(input: IMiddlewareInput): void { 134 | // ... your function 135 | } 136 | 137 | const count = superstate(0).use([myMiddleware]) 138 | ``` 139 | 140 | *Feel free to change `T = number` to whatever you need.* 141 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/nesting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Nesting 6 | 7 | Nesting states is perfectly doable and encouraged. Let's say you are building a game where you 8 | have a `hero`, and this hero have `attributes`: 9 | 10 | ```tsx 11 | import { superstate } from '@superstate/core' 12 | 13 | const hp = superstate(100) 14 | const hero = superstate({ attributes: { hp } }) 15 | 16 | hero.now().attributes.hp.set((prev) => prev - 50) 17 | ``` 18 | 19 | To make your life easier, you can create helper functions: 20 | 21 | ```tsx 22 | const attributes = { hp: attribute(100) } 23 | 24 | function attribute(initial: number) { 25 | const state = superstate(initial) 26 | 27 | function decrease(amount: number) { 28 | state.set((prev) => prev - amount) 29 | }, 30 | 31 | function increase(amount: number) { 32 | state.set((prev) => prev + amount) 33 | } 34 | 35 | function now() { 36 | return state.now() 37 | } 38 | 39 | return { decrease, increase, now } 40 | } 41 | 42 | const hero = { 43 | state: superstate({ attributes }), 44 | 45 | getAttribute(key: keyof typeof attributes) { 46 | return hero.state.now().attributes[key] 47 | } 48 | 49 | heal(amount: number) { 50 | hero.getAttribute('hp').increase(amount) 51 | }, 52 | 53 | takeDamage(amount: number) { 54 | hero.getAttribute('hp').decrease(amount) 55 | } 56 | } 57 | 58 | hero.takeDamage(50) 59 | hero.heal(25) 60 | 61 | console.log(hero.get('hp')) // 75 62 | ``` 63 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/now-or-draft.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | import { Code } from '../../src/components/code' 6 | 7 | # "now" or "draft"? 8 | 9 | At this point you may have heard of [Drafts](/getting-started/drafts), but you may not understand why exactly 10 | they exist and why (or when) you should use them. Let's go over the basics. 11 | 12 | **superstate**'s API is, in fact, very simple: 13 | 14 | ```typescript 15 | const count = superstate(0) 16 | 17 | count.set(5) 18 | ``` 19 | 20 | The above is what you would expect of any state management library: you define a state, set its initial value (`0`), 21 | and mutate it (to `5`) when needed. 22 | 23 | However, this is not always enough when it comes to real-life applications. 24 | 25 | An ordinary example of a real-life application is when you want to confirm a user's action before you actually submit it. 26 | A good example may be editting a user's profile: 27 | 28 |
29 | 37 | \
38 | \ Hello {name}.
39 | \ @{slugify(name, { lower: true })}
40 | \
\n 41 | \ 42 | \ {isConfirming && ( 43 | \
44 | \

45 | \ You sure? Your name will become {draftName} and 46 | \ your slug will be @{slugify(draftName, { lower: true })}. 47 | \

\n 48 | \ 49 | \
50 | \ 51 | \ 60 | \
61 | \
62 | \ )}\n 63 | \ 64 | \
{ 65 | \ e.preventDefault() 66 | \ 67 | \ setDraftName(e.target.elements.name.value) 68 | \ setIsConfirming(true) 69 | \ }}> 70 | \ \n 71 | \ 72 | \ 73 | \
74 | \
75 | \ ) 76 | \}` }} /> 77 | 78 | 79 | Although there are plenty of ways to achieve the above with less verbosity and complexity, 80 | it'd be better if you wouldn't have to worry about it at all. 81 | 82 | Let's see the same example with **superstate**: 83 | 84 |
85 | 95 |
96 | Hello {_user.name}.
97 | @{slugify(_user.name, { lower: true })} 98 |
\n 99 | \ 100 | {draft && ( 101 |
102 |

103 | You sure? Your name will become {draft.name} and 104 | your slug will be @{slugify(draft.name, { lower: true })}. 105 |

\n 106 | \ 107 |
108 | 109 | 110 |
111 |
112 | )}\n 113 | \ 114 |
{ 115 | e.preventDefault() 116 | \ 117 | user.sketch(prev => ({ ...prev, name: e.target.elements.name.value })) 118 | }}> 119 | \n 120 | 121 |
122 |
123 | ) 124 | \}` }} /> 125 | 126 | 127 | We can observe a few things from the example above: 128 | 129 | #### Less lines of code 130 | The same functionality was achieved with both codes. However, with **superstate** it required less lines of code and it felt 131 | more ergonomic. 132 | 133 | #### Easy to keep it consistent 134 | If your app has several places where you need user's confirmation before making a change go live, you don't have to worry 135 | about abstracting things and/or consistency. **superstate** takes care of it for you. 136 | 137 | #### Instant better UX 138 | Have you tried to submit the same name twice? In the example without **superstate**, it allows users to submit the same 139 | name no matter what—you have to implement your own diff algorithm to prevent this. 140 | 141 | In the other hand, **superstate** is smarter. It won't give false hopes to the user nor stress your API if a change wouldn't 142 | make sense. 143 | 144 | ### The veredict 145 | 146 | `now` and `draft` serve different purposes. `now` is meant to express the official version of your state; `draft` is meant 147 | to represent a "work-in-progress" version of your state that may change before going live. 148 | 149 | If your application doesn't care about giving users second chances, thus improving its usability, then don't bother 150 | with `draft` at all. Otherwise, `draft` will be at your service. 151 | -------------------------------------------------------------------------------- /packages/docs/docs/api-reference/@superstate/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "@superstate", 3 | "position": 1 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/api-reference/@superstate/core/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "core", 3 | "position": 1 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/api-reference/@superstate/core/superstate.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # superstate 6 | 7 | The superstate wrapper. This is the function that you should use to create a superstate. 8 | 9 | Examples: 10 | 11 | ```typescript 12 | // Primitives 13 | const greet = superstate('Hello world!') 14 | const count = superstate(0) 15 | const isLoggedIn = superstate(false) 16 | 17 | // Objects 18 | const person = superstate({ id: '0', name: 'John' }) 19 | 20 | // Nested objects 21 | const theme = superstate({ 22 | header: { color: 'red' }, 23 | footer: { color: 'blue' }, 24 | links: [ 25 | { href: '#', text: 'Home' }, 26 | { href: '#', text: 'About' }, 27 | ], 28 | }) 29 | 30 | // Collections 31 | const people = superstate([ 32 | { id: '0', name: 'John' }, 33 | { id: '1', name: 'Doe' }, 34 | ]) 35 | 36 | // Maps 37 | const numbers = superstate( 38 | new Map([ 39 | ['one', 1], 40 | ['two', 2], 41 | ]) 42 | ) 43 | 44 | // Sets 45 | const colors = superstate(new Set(['Red', 'Yellow'])) 46 | ``` 47 | 48 | ## now 49 | 50 | Returns the current value of your state. 51 | 52 | ```ts 53 | const count = superstate(0) 54 | 55 | console.log(count.now()) // 0 56 | 57 | count.set(10) 58 | 59 | console.log(count.now()) // 10 60 | ``` 61 | 62 | ## draft 63 | 64 | The draft version of your state. Will be `undefined` if no draft is available. To set the value of the draft, call `.sketch()`. 65 | 66 | ```ts 67 | const count = superstate(0) 68 | 69 | console.log(count.draft()) // undefined 70 | 71 | count.sketch(10) 72 | 73 | console.log(count.draft()) // 10 74 | ``` 75 | 76 | :::info 77 | If you don't plan to use [Drafts](/getting-started/drafts), always use [now()](#now) instead to get the value of your state. 78 | ::: 79 | 80 | ## set 81 | 82 | Overrides the value of `now`, which is the current value of your state: 83 | 84 | ```typescript 85 | const count = superstate(0) 86 | 87 | count.set(1) // now === 1 88 | ``` 89 | 90 | If you want to use the previous value as a reference: 91 | 92 | ```typescript 93 | const count = superstate(0) 94 | 95 | count.set((prev) => prev + 1) // now === 1 96 | count.set((prev) => prev + 1) // now === 2 97 | ``` 98 | 99 | ### Broadcasting 100 | 101 | Whenever you `.set()`, changes will be broadcasted unless the new `now` is equal to the previous one, 102 | or you pass `true` to the `silent` option: 103 | 104 | ```typescript 105 | const count = superstate(0) 106 | 107 | count.set(1, { silent: true }) // won't be broadcasted 108 | ``` 109 | 110 | ## sketch 111 | 112 | Overrides the value of `draft`: 113 | 114 | ```ts 115 | const count = superstate(0) 116 | 117 | count.sketch(1) // `draft` === 1 118 | ``` 119 | 120 | And just like [`.set`](#set), you can use the previous `draft` as a reference: 121 | 122 | ```ts 123 | const count = superstate(0) 124 | 125 | count.sketch(1) // `draft` === 1 126 | count.sketch((prev) => prev + 1) // `draft` === 2 127 | ``` 128 | 129 | :::info 130 | If you don't plan to use [Drafts](/getting-started/drafts), prefer to mutate your state with [set](#set) instead. 131 | ::: 132 | 133 | ### Broadcasting 134 | 135 | Whenever you `.sketch()`, changes will be broadcasted unless the new `draft` is equal to the previous one, 136 | or you pass `true` to the `silent` option: 137 | 138 | ```ts 139 | const count = superstate(0) 140 | 141 | count.sketch(1, { silent: true }) 142 | ``` 143 | 144 | Note that draft changes will only be broadcasted to draft subscribers. [Learn more →](/advanced/broadcasting#subscribing-to-changes) 145 | 146 | ## publish 147 | 148 | When you have a `draft` ready to go, call `.publish()` to make it the new `now` value: 149 | 150 | ```ts 151 | const count = superstate(0) 152 | 153 | count.sketch(1) 154 | console.log(count.draft()) // logs 1 155 | 156 | count.publish() 157 | console.log(count.draft()) // logs `undefined` 158 | console.log(count.now()) // logs 1 159 | ``` 160 | 161 | :::info 162 | Don't worry about publishing if you're not using [Drafts](/getting-started/drafts). 163 | ::: 164 | 165 | ### Broadcasting 166 | 167 | Whenever you `.publish()`, changes will be broadcasted unless the new `now` is equal to the previous one, 168 | or you pass `true` to the `silent` option: 169 | 170 | ```ts {4} 171 | const count = superstate(0) 172 | 173 | count.sketch(1) 174 | count.publish({ silent: true }) // won't be broadcasted 175 | ``` 176 | 177 | ## discard 178 | 179 | Discards the draft, setting its value to `undefined`. 180 | 181 | Upon calling this function, a draft broadcast will occur. 182 | 183 | ```ts 184 | const count = superstate(0) 185 | 186 | count.set(1) // draft === 1 187 | 188 | count.discard() // draft === undefined 189 | ``` 190 | 191 | If there's no draft to discard, nothing is going to happen. 192 | 193 | ### Broadcasting 194 | 195 | Whenever you `.discard()`, changes will be broadcasted unless the new `draft` is equal to the previous one, 196 | or you pass `true` to the `silent` option: 197 | 198 | ```ts {4} 199 | const count = superstate(0) 200 | 201 | count.sketch(1) 202 | count.discard({ silent: true }) // won't be broadcasted 203 | ``` 204 | 205 | ## subscribe 206 | 207 | Starts monitoring changes to the state. 208 | 209 | ```ts 210 | const count = superstate(0) 211 | 212 | count.subscribe((value) => console.log(value)) 213 | 214 | count.set(10) // this will trigger the subscribe callback 215 | ``` 216 | 217 | You can also subscribe to changes made to the `draft`: 218 | 219 | ```ts {3-4} 220 | const count = superstate(0) 221 | 222 | // Note the 'draft' at the line below 223 | count.subscribe((value) => console.log(value), 'draft') 224 | 225 | count.sketch(10) // this will trigger the subscribe callback 226 | ``` 227 | 228 | ### unsubscribe 229 | 230 | The `.subscribe()` method returns a unsubscribe function: 231 | 232 | ```ts 233 | const count = superstate(0) 234 | 235 | const unsubscribe = count.subscribe((value) => console.log(value)) 236 | 237 | count.set(10) // this will trigger the subscribe callback 238 | 239 | unsubscribe() // stop listening to changes 240 | 241 | count.set(10) // this wont trigger the subscribe callback 242 | ``` 243 | 244 | ## unsubscribeAll 245 | 246 | Unsubscribes all `now` and `draft` subscribers. 247 | After you call this method, changes will no longer be broadcasted. 248 | 249 | ```ts 250 | const count = superstate(0) 251 | 252 | count.subscribe((value) => console.log(value)) 253 | 254 | count.publish(10) // this will trigger the subscribe callback 255 | 256 | count.unsubscribeAll() // stop listening to changes 257 | 258 | count.publish(10) // this wont trigger the subscribe callback 259 | ``` 260 | 261 | :::caution 262 | Only call this function if you know what you're doing. If possible, always prefer to unsubscribe 263 | to [particular subscriptions](#unsubscribe). 264 | ::: 265 | 266 | ## extend 267 | 268 | Read more about [Extensions](/advanced/extensions). 269 | 270 | ## use 271 | 272 | Read more about [Middlewares](/advanced/middlewares). 273 | -------------------------------------------------------------------------------- /packages/docs/docs/api-reference/@superstate/react/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "react", 3 | "position": 2 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/api-reference/@superstate/react/useSuperState.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # useSuperState 6 | 7 | A React hook that re-renders the component when a `superstate` changes. 8 | 9 | ```tsx 10 | import { superstate } from '@superstate/core' 11 | import { useSuperState } from '@superstate/react' 12 | 13 | const count = superstate(0) 14 | 15 | export function MyComponent() { 16 | useSuperState(count) 17 | 18 | return
{count.now()}
19 | } 20 | ``` 21 | 22 | You can also re-render only when a specific state changes: 23 | 24 | *Defaults to `all`* 25 | 26 | 27 | ```ts 28 | useSuperState(count, { target: 'now' }) 29 | useSuperState(count, { target: 'draft' }) 30 | useSuperState(count, { target: 'all' }) 31 | ``` 32 | -------------------------------------------------------------------------------- /packages/docs/docs/api-reference/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "API Reference", 3 | "position": 5 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/examples/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Examples", 3 | "position": 4 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/examples/index.mdx: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Todo App 4 | 5 | A simple, persistent todo app. 6 | 7 | Features: 8 | 9 | - [React](https://reactjs.org/) 10 | - [Automatic localStorage persistency](/advanced/local-storage) 11 | 12 | **[Click here to see it →](/examples/todo)** 13 | 14 | --- 15 | 16 | ## Text Editor 17 | 18 | A simple, persistent text editor. 19 | 20 | Features: 21 | 22 | - [React](https://reactjs.org/) 23 | - [Drafts](/getting-started/drafts) 24 | - [Automatic localStorage persistency](/advanced/local-storage) 25 | 26 | **[Click here to see it →](/examples/text-editor)** 27 | 28 | --- 29 | 30 | ## Notifications System 31 | 32 | A globally-accessible notifications system. 33 | 34 | Features: 35 | 36 | - [React](https://reactjs.org/) 37 | - [Extensions](/advanced/extensions) 38 | 39 | **[Click here to see it →](/examples/notifications-system)** 40 | 41 | --- 42 | 43 | ## RPG Game 44 | 45 | A very basic RPG game with damage prediction using [Drafts](/getting-started/drafts). 46 | 47 | Features: 48 | 49 | - [React](https://reactjs.org/) 50 | - [Extensions](/advanced/extensions) 51 | - [Drafts](/getting-started/drafts) 52 | 53 | **[Click here to see it →](/examples/rpg-game)** 54 | -------------------------------------------------------------------------------- /packages/docs/docs/examples/notifications-system.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Notifications System 6 | 7 | A globally-accessible notifications system. 8 | 9 | Features: 10 | 11 | - [React](https://reactjs.org/) 12 | - [Extensions](/advanced/extensions) 13 | 14 |