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 |
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 | 
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 |
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 | \
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 |
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 |
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 |
18 |
19 | ---
20 |
21 | If you feel more comfortable watching step-by-step videos, the commented version of the example above
22 | was recorded just for you.
23 |
24 |
33 |
--------------------------------------------------------------------------------
/packages/docs/docs/examples/rpg-game.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # RPG Game
6 |
7 | A very basic RPG game with damage prediction using [Drafts](/getting-started/drafts).
8 |
9 | Features:
10 |
11 | - [React](https://reactjs.org/)
12 | - [Extensions](/advanced/extensions)
13 | - [Drafts](/getting-started/drafts)
14 |
15 | :::info
16 | This is intentionally a more advanced example and you may not fully understand what's going on. It's okay—perhaps [another example](/examples) would suit you best?
17 | :::
18 |
19 |
23 |
--------------------------------------------------------------------------------
/packages/docs/docs/examples/text-editor.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Text Editor
6 |
7 | A simple, persistent text editor.
8 |
9 | Features:
10 |
11 | - [React](https://reactjs.org/)
12 | - [Drafts](/getting-started/drafts)
13 | - [Automatic localStorage persistency](/advanced/local-storage)
14 |
15 |
19 |
--------------------------------------------------------------------------------
/packages/docs/docs/examples/todo.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import { Code } from '../../src/components/code'
6 |
7 | # Todo App
8 |
9 | A simple, persistent todo app.
10 |
11 | Features:
12 |
13 | - [React](https://reactjs.org/)
14 | - [Automatic localStorage persistency](/advanced/local-storage)
15 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/packages/docs/docs/getting-started/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Getting Started",
3 | "position": 2
4 | }
5 |
--------------------------------------------------------------------------------
/packages/docs/docs/getting-started/adopting.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | ---
4 |
5 | # Adopting in an existing application
6 |
7 | Integrating **superstate** in an existing application is easy and harmless. In fact, all you have to do is to
8 | [install superstate](/getting-started/installation) and wrap your next state within `superstate()`.
9 | [Learn how to create your first superstate →](/getting-started/first-state)
10 |
11 | :::info Tips and tricks
12 | You can use [Broadcasting](/advanced/broadcasting), [Extensions](/advanced/extensions) and/or [Middlewares](/advanced/middlewares)
13 | to update your legacy states when a **superstate** changes.
14 | :::
15 |
--------------------------------------------------------------------------------
/packages/docs/docs/getting-started/drafts.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Drafts
6 | One of the key and most opionionated feature of **superstate** is the built-in draft management.
7 | Drafts are preliminatory versions of your state that you are free to mutate without impacting
8 | the final user.
9 |
10 | To illustrate the concept of drafts to you: when you're typing a text message to someone, the said text is just
11 | a draft as long as you don't submit it. While the text is just a draft, you're free to change it as much as you like.
12 |
13 | ## Creating a draft
14 | To create a draft, call the `.sketch()` method:
15 |
16 | ```ts {3}
17 | const count = superstate(0)
18 |
19 | count.sketch(5)
20 | ```
21 |
22 | The `.sketch()` method will assign `5` to the `draft` of `count`.
23 |
24 | ### Updating an existing draft
25 | ```ts {4}
26 | const count = superstate(0)
27 |
28 | count.sketch(5) // draft is 5
29 | count.sketch(prev => prev + 5) // draft is 10
30 | ```
31 |
32 | :::warning
33 | Always prefer to [`.discard()`](#discarding-a-draft) a draft when you no longer want it instead of `.sketch(undefined)` because
34 | `.discard()` does more than just set the draft's value to `undefined`.
35 |
36 | However, for whatever reason, if you literally just want the `.draft()` to become `undefined`, feel free to `.sketch(undefined)`.
37 | :::
38 |
39 | ## Reading a draft
40 |
41 | To display the draft version of a superstate, you simply have to call `.draft()`:
42 |
43 | ```typescript {5}
44 | const count = superstate(0)
45 |
46 | count.sketch(20)
47 |
48 | console.log(count.draft()) // logs 20
49 | ```
50 |
51 |
52 | ## Publishing a draft
53 |
54 | When you change the value of a draft (by calling `.sketch(5)`, for example), this change is intentionally not propagated to `.now()` and only exists in `.draft()`.
55 | In order to publish it, guess what, you gotta call `.publish()`:
56 |
57 | ```ts {4}
58 | const count = superstate(0)
59 |
60 | count.sketch(20)
61 | count.publish()
62 | ```
63 |
64 | Or, if you want a shorter version, chaining is available:
65 |
66 | ```ts {3}
67 | const count = superstate(0)
68 |
69 | count.sketch(20).publish()
70 | ```
71 |
72 | After you call `.publish()`, **superstate** does a few things:
73 |
74 | 1. Assigns the value of the `draft` to `now`,
75 | 2. disposes the value of `.draft()`, making it become `undefined`,
76 | 3. broadcasts the change to all the subscribers. (See [Broadcasting](/advanced/broadcasting))
77 |
78 | If you want to skip the broadcast part, just pass `{ silent: true }` to `publish`:
79 |
80 | ```ts {3}
81 | const count = superstate(0)
82 |
83 | count.sketch(20).publish({ silent: true })
84 | ```
85 |
86 | ### Discarding a draft
87 | Let's say you made changes to your draft but in the end you changed your mind. Super fine—discarding drafts is simple:
88 |
89 | ```typescript {4}
90 | const count = superstate(0)
91 |
92 | count.sketch(10)
93 | count.discard() // Nah, 10 is not enough!
94 |
95 | count.sketch(20)
96 | count.publish() // Yeah, 20 it is!
97 | ```
98 |
99 | When discarding, changes will be broadcasted unless you pass `true` to the `silent` option:
100 |
101 | ```ts {3}
102 | const count = superstate(0)
103 |
104 | count.sketch(10).discard({ silent: true })
105 | ```
106 |
107 | :::info
108 | If you discard an `undefined` draft, nothing will happen.
109 | :::
110 |
111 | ## Monitoring draft changes
112 | If you like to do something whenever a draft changes, you can subscribe to your state's draft by passing `'draft'` as the second argument
113 | of the `.subscribe()` function:
114 |
115 | ```ts {5}
116 | const count = superstate(0)
117 |
118 | count.subscribe(value => {
119 | console.log(value)
120 | }, 'draft') // <--- here!
121 | ```
122 |
123 | :::info
124 | By default, `.subscribe()` will only listen to `.now()` changes.
125 | :::
126 |
127 | If you no longer want to listen to draft changes:
128 |
129 | ```typescript {2,4}
130 | const count = superstate(0)
131 | const unsubscribe = count.subscribe(console.log, 'draft')
132 |
133 | unsubscribe()
134 |
135 | // Now, you can safely mutate the draft that
136 | // no changes will be broadcasted.
137 | ```
138 |
139 | ### What changes are broadcasted?
140 |
141 | Changes are only broadcasted if the new value of the draft is different from the previous value. With that in mind:
142 |
143 | - Whenever `.sketch(value)` is called,
144 | - and/or whenever `.publish()` is called,
145 | - and/or whenever `.discard()` is called.
146 |
147 | :::info
148 | No matter how complex the value of `draft()` is, **superstate** will deep compare the previous value with the next value,
149 | to make sure changes are only broadcasted when needed.
150 | :::
151 |
152 | If you don't want changes to be broadcasted at all, most of the mutation functions allow you to pass the `{ silent: true }` option:
153 |
154 | ```ts {3-5}
155 | const count = superstate(0)
156 |
157 | count.sketch(5, { silent: true })
158 | count.discard({ silent: true })
159 | count.publish({ silent: true })
160 | ```
161 |
162 | ## `undefined` drafts
163 |
164 | If your state doesn't have a draft version, `undefined` will be returned when trying to read it through `.draft()`:
165 |
166 | ```typescript
167 | const count = superstate(0)
168 |
169 | console.log(count.draft()) // logs `undefined`
170 | ```
171 |
172 | You might be wondering why and/or when a draft is `undefined`. Well, the idea is that `superstate` aims to be as respectful
173 | as possible with your user's memory usage. In short, **superstate** only issues
174 | drafts when needed and disposes them when no longer needed.
175 |
176 | ### Draft disposal
177 |
178 | `.draft()` will return `undefined` when:
179 |
180 | - You haven't `.sketch()` anything,
181 | - after you call `.discard()` or
182 | - after you call `.publish()`.
183 |
--------------------------------------------------------------------------------
/packages/docs/docs/getting-started/first-state.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Your first superstate
6 |
7 | It's time to have fun. Let's create a superstate that stores a number:
8 |
9 | ```typescript
10 | import { superstate } from '@superstate/core'
11 |
12 | const count = superstate(0)
13 | ```
14 |
15 | To display what `count` looks like:
16 |
17 | ```typescript
18 | console.log(count.now()) // logs 0
19 | ```
20 |
21 | And to change what `count` should be:
22 |
23 | ```typescript
24 | count.set(5)
25 |
26 | console.log(count.now()) // logs 5
27 | ```
28 |
29 | ## A more real-world example
30 |
31 | ```ts
32 | const user = {
33 | state: superstate({
34 | firstName: 'John',
35 | lastName: 'Doe',
36 | role: 'user',
37 | posts: []
38 | }),
39 |
40 | rename: (firstName: string, lastName: string) => user.state.set({ firstName, lastName }),
41 | promote: (newRole: Role) => user.state.set({ role: newRole }),
42 | post: (newPost: Post) => user.state.set({ posts: [...user.state.now().posts, newPost] }),
43 |
44 | fullName: () => `${user.state.now().firstName} ${user.state.now().lastName}`,
45 | isAdmin: () => user.state.now().role === Role.Admin
46 | getPostsAmount: () => user.state.now().posts.length
47 | }
48 |
49 | enum Role {
50 | Admin = 'admin',
51 | Role = 'role'
52 | }
53 |
54 | interface User {
55 | firstName: string
56 | lastName: string
57 | role: Role
58 | posts: Post[]
59 | }
60 |
61 | interface Post {
62 | id: string
63 | title: string
64 | }
65 | ```
66 |
67 | :::info
68 | TypeScript in often used in the examples, but it's not required. If you're using vanilla JavaScript, just
69 | ignore all the interfaces, enums, etc.
70 | :::
71 |
72 | Now you can do things like:
73 |
74 | ```ts
75 | user.rename('Jane', 'Doe')
76 | user.promote(Role.Admin)
77 | user.post({ id: '1', title: 'Hello world' })
78 |
79 | console.log(user.fullName()) // logs `Jane Doe`
80 | console.log(user.isAdmin()) // logs `true`
81 | console.log(user.getPostsAmount()) // logs `1`
82 | ```
83 |
84 | And if you want to monitor when the state changes:
85 |
86 | ```ts
87 | user.state.subscribe((state) => {
88 | alert('Hey! The user state changed!')
89 | })
90 | ```
91 |
92 | :::info React?
93 | If you want to do something inside a React component when the state changes, please read our [React documentation](/getting-started/react).
94 | :::
95 |
96 | ## Technical Overview
97 |
98 | Under the hood, what **superstate** does is wrapping your state within a variable and
99 | enforcing queries and mutations to happen respectively through the `.now()` and `.set()` methods.
100 |
101 | This allows **superstate** to keep track of what's going on with your state, and to keep your
102 | app posted about changes made to it as well.
103 |
104 | ## An introductory video
105 |
106 | You are a better learner when watching videos? Perhaps you'd like to know there's a commented version of the [Notifications System example](/examples/notifications-system)
107 | available as well. Check it out:
108 |
109 |
118 |
--------------------------------------------------------------------------------
/packages/docs/docs/getting-started/installation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import Tabs from '@theme/Tabs'
6 | import TabItem from '@theme/TabItem'
7 |
8 | # Installation
9 |
10 | All you have to do is installing `@superstate/core` as any other package:
11 |
12 | _~1.1kb gzipped_
13 |
14 |
15 |
16 |
17 | ```bash
18 | npm install @superstate/core
19 | ```
20 |
21 |
22 |
23 |
24 | ```bash
25 | yarn add @superstate/core
26 | ```
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/packages/docs/docs/getting-started/react.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | import { Code } from '../../src/components/code'
6 | import Tabs from '@theme/Tabs'
7 | import TabItem from '@theme/TabItem'
8 |
9 | # React
10 | Integrating **superstate** with React is easy.
11 |
12 | First you have to install `@superstate/react`:
13 |
14 |
15 |
16 |
17 | ```bash
18 | npm install @superstate/react
19 | ```
20 |
21 |
22 |
23 |
24 | ```bash
25 | yarn add @superstate/react
26 | ```
27 |
28 |
29 |
30 |
31 | Then you'll be able to import the `useSuperState` hook:
32 |
33 |
39 | Count is {count.now()} \n
40 |
41 |
42 |
43 | )
44 | \}` }} />
45 |
46 | ## Targeting specific state version
47 |
48 | By default, the `useSuperState` hook will re-render your React component whenever either `now` or `draft` change.
49 | However, sometimes you want to monitor only changes made to the `draft` instead. To do so, all you have to do is
50 | specifying `draft` as a `useSuperState` target:
51 |
52 |