├── .gitignore ├── packages ├── simple-shared-state │ ├── .gitignore │ ├── src │ │ ├── index.js │ │ ├── merge.js │ │ └── store.js │ ├── babel.config.js │ ├── jsdoc.json │ ├── test_bundle │ │ └── merge.js │ ├── webpack.config.js │ ├── package.json │ ├── README.md │ ├── dist │ │ ├── simple-shared-state.es6.umd.js │ │ └── simple-shared-state.es5.umd.js │ └── test │ │ └── main.test.js ├── redux-comparison │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── storeRedux.js │ │ ├── setupTests.js │ │ ├── app.test.js │ │ ├── lib │ │ │ ├── randomRGB.js │ │ │ └── stats.js │ │ ├── index.css │ │ ├── components │ │ │ ├── colorSquareRedux.js │ │ │ ├── colorSquareSSS.js │ │ │ ├── gridApp.js │ │ │ └── squareGrid.js │ │ ├── index.js │ │ ├── storeSSS.js │ │ ├── reducer.js │ │ ├── appSSS.js │ │ ├── appRedux.js │ │ └── serviceWorker.js │ ├── .env │ ├── .gitignore │ ├── package.json │ └── README.md ├── react-test-ground │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── setupTests.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── App.css │ │ ├── store.js │ │ ├── App.js │ │ ├── logo.svg │ │ └── serviceWorker.js │ ├── .gitignore │ ├── package.json │ └── README.md ├── use-simple-shared-state │ ├── index.js │ ├── package.json │ ├── webpack.config.js │ └── README.md └── enhanced-simple-shared-state │ ├── package.json │ └── index.js ├── package.json ├── examples └── include-via-script-tags │ └── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /packages/simple-shared-state/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | templates/ 3 | out/ 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /packages/redux-comparison/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/react-test-ground/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/simple-shared-state/src/index.js: -------------------------------------------------------------------------------- 1 | export { Store } from "./store"; 2 | export { deleted } from "./merge"; 3 | -------------------------------------------------------------------------------- /packages/redux-comparison/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-rf-etc/simple-shared-state/HEAD/packages/redux-comparison/public/favicon.ico -------------------------------------------------------------------------------- /packages/redux-comparison/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-rf-etc/simple-shared-state/HEAD/packages/redux-comparison/public/logo192.png -------------------------------------------------------------------------------- /packages/redux-comparison/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-rf-etc/simple-shared-state/HEAD/packages/redux-comparison/public/logo512.png -------------------------------------------------------------------------------- /packages/react-test-ground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-rf-etc/simple-shared-state/HEAD/packages/react-test-ground/public/favicon.ico -------------------------------------------------------------------------------- /packages/react-test-ground/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-rf-etc/simple-shared-state/HEAD/packages/react-test-ground/public/logo192.png -------------------------------------------------------------------------------- /packages/react-test-ground/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-rf-etc/simple-shared-state/HEAD/packages/react-test-ground/public/logo512.png -------------------------------------------------------------------------------- /packages/redux-comparison/src/storeRedux.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | import { createStore } from 'redux'; 3 | 4 | export default createStore(reducer); 5 | -------------------------------------------------------------------------------- /packages/redux-comparison/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_HOST_DOMAIN=//localhost:4000 2 | REACT_APP_TEST_UUID=00000000-0000-1000-9000-400000000000 3 | REACT_APP_PASSWORD=awesomepassword 4 | REACT_APP_USER_EMAIL=user@emails.com -------------------------------------------------------------------------------- /packages/simple-shared-state/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/app.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/simple-shared-state/jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "docdash": { 4 | "static": true 5 | }, 6 | "source": { 7 | "include": ["./src", "../../README.md"], 8 | "includePattern": ".+\\.js$", 9 | "excludePattern": "(node_modules/|docs)" 10 | }, 11 | "opts": { 12 | "template": "./node_modules/docdash" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/lib/randomRGB.js: -------------------------------------------------------------------------------- 1 | const randomRGB = () => ([ 2 | Math.floor(Math.random()*256), 3 | Math.floor(Math.random()*256), 4 | Math.floor(Math.random()*256) 5 | ]); 6 | 7 | export function randomRGBArray(arrayLength) { 8 | const rgbArray = []; 9 | for (let i=0; i < arrayLength; i++) { 10 | rgbArray.push(randomRGB); 11 | } 12 | return rgbArray; 13 | } 14 | 15 | export default randomRGB; 16 | -------------------------------------------------------------------------------- /packages/react-test-ground/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/redux-comparison/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/lib/stats.js: -------------------------------------------------------------------------------- 1 | 2 | export default (label, { n, min, max, mean, variance, standard_deviation }) => { 3 | let string = `${label}:\n`; 4 | string += `count: ${n}\n`; 5 | string += `min: ${min}\n`; 6 | string += `max: ${max}\n`; 7 | string += `variance: ${variance}\n`; 8 | string += `standard deviation: ${standard_deviation}\n`; 9 | string += `mean: ${mean}`; 10 | 11 | return string; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-shared-state", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "main": "index.js", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com-personal:rm-rf-etc/simple-shared-state.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/components/colorSquareRedux.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import styled from 'styled-components'; 4 | 5 | const ColorSquareContainer = styled.div` 6 | width: 100%; 7 | height: 100%; 8 | ` 9 | 10 | const ColorSquare = ({ index }) => { 11 | const [r,g,b] = useSelector(state => state.squareColors[index]); 12 | 13 | return ( 14 | 15 | ); 16 | }; 17 | 18 | export default ColorSquare; 19 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/components/colorSquareSSS.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from '../storeSSS'; 3 | import useSharedState from 'use-simple-shared-state'; 4 | import styled from 'styled-components'; 5 | 6 | const ColorSquareContainer = styled.div` 7 | width: 100%; 8 | height: 100%; 9 | `; 10 | 11 | const ColorSquare = ({ index }) => { 12 | const [[r,g,b]] = useSharedState(store, [s => s.squareColors[index]]); 13 | 14 | return ( 15 | 16 | ) 17 | } 18 | 19 | export default ColorSquare; 20 | -------------------------------------------------------------------------------- /packages/use-simple-shared-state/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export default (store, selectors) => { 4 | const [state, setState] = useState([]); 5 | 6 | useEffect(() => { 7 | const unwatch = store.watchBatch(selectors, (array) => { 8 | setState(array.slice()); 9 | }); 10 | return unwatch; 11 | }, []); 12 | 13 | if (state.length) return state; 14 | 15 | const storeState = store.getState(); 16 | return selectors.map((fn) => { 17 | let snapshot; 18 | try { 19 | snapshot = fn(storeState); 20 | } catch (_) {} 21 | 22 | return snapshot; 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/react-test-ground/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/redux-comparison/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/use-simple-shared-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-simple-shared-state", 3 | "version": "0.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "workspaces": true, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "hooks", 13 | "simple-shared-state", 14 | "redux", 15 | "mobx", 16 | "context api", 17 | "context" 18 | ], 19 | "externals": [ 20 | "react" 21 | ], 22 | "author": "", 23 | "license": "ISC", 24 | "peerDependencies": { 25 | "react": "^16.12.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/preset-env": "^7.7.7", 29 | "babel-loader": "^8.0.6", 30 | "webpack-cli": "^3.3.10" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/components/gridApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ColorSquareGrid from './squareGrid'; 3 | 4 | export default ({ clickStart, runOnce, changeSize, size, ColorSquare }) => ( 5 |
6 | 10 |
11 | 12 | 13 | 14 |
15 | 16 | 24 | {size} 25 |
26 | ); 27 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App1 from './appSSS'; 4 | import App2 from './appRedux'; 5 | import { Provider } from 'react-redux'; 6 | import reduxStore from './storeRedux'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | ReactDOM.render(, document.getElementById('root1')); 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root2'), 15 | ); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.unregister(); 21 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/components/squareGrid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const gridContainerSize = 300; 5 | 6 | const GridContainer = styled.div(({ count }) => ` 7 | display: grid; 8 | height: ${gridContainerSize}px; 9 | width: ${gridContainerSize}px; 10 | grid-gap: 2px; 11 | grid-template-columns: repeat(${count}, auto); 12 | grid-template-rows: repeat(${count}, auto); 13 | `); 14 | 15 | const ColorSquareGrid = function({ gridSize, ColorSquare }) { 16 | const squaresList = Array(gridSize * gridSize).fill(0); 17 | return ( 18 | 19 | {squaresList.map((_, index) => )} 20 | 21 | ) 22 | } 23 | 24 | export default ColorSquareGrid; 25 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/storeSSS.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'simple-shared-state'; 2 | 3 | const state = { 4 | gridSize: 1, 5 | squareColors: [ 6 | [0, 0, 0], 7 | ], 8 | example: { 9 | thing1: { 10 | a: 1, 11 | b: 2, 12 | } 13 | } 14 | }; 15 | 16 | const actions = () => ({ 17 | changeGridSize: (gridSize) => ({ 18 | gridSize, 19 | squareColors: Array(gridSize * gridSize).fill([0,0,0]), 20 | }), 21 | changeColors: (squareColors) => ({ 22 | squareColors, 23 | }), 24 | changeExample: () => ({ 25 | example: { 26 | thing1: { 27 | a: `changed! ${Math.random()}`, 28 | b: `changed! ${Math.random()}`, 29 | } 30 | } 31 | }), 32 | }); 33 | 34 | // export default new Store(state, actions, window.__REDUX_DEVTOOLS_EXTENSION__); 35 | export default new Store(state, actions); 36 | -------------------------------------------------------------------------------- /packages/use-simple-shared-state/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = [ 4 | { 5 | entry: "./src/index.js", 6 | mode: "production", 7 | output: { 8 | library: "useSharedState", 9 | libraryTarget: "umd", 10 | filename: "use-shared-state.es6.umd.js", 11 | path: path.resolve(__dirname, "dist"), 12 | globalObject: 'Function("return this")()', 13 | }, 14 | }, 15 | { 16 | entry: "./src/index.js", 17 | mode: "production", 18 | output: { 19 | library: "useSharedState", 20 | libraryTarget: "umd", 21 | filename: "use-shared-state.es5.umd.js", 22 | path: path.resolve(__dirname, "dist"), 23 | globalObject: 'Function("return this")()', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.m?js$/, 29 | exclude: /(node_modules)/, 30 | use: { 31 | loader: "babel-loader", 32 | options: { 33 | presets: ["@babel/preset-env"], 34 | } 35 | } 36 | } 37 | ] 38 | } 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /packages/react-test-ground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-test-ground", 3 | "version": "0.1.0", 4 | "private": true, 5 | "workspaces": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-scripts": "3.3.0", 13 | "use-simple-shared-state": "0.0.1", 14 | "simple-shared-state": "2.1.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/redux-comparison/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "workspaces": true, 6 | "dependencies": { 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-redux": "^7.1.3", 12 | "react-scripts": "3.3.0", 13 | "simple-shared-state": "^5.0.0", 14 | "stats-incremental": "^1.2.1", 15 | "styled-components": "^5.0.0", 16 | "use-simple-shared-state": "0.0.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/reducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | gridSize: 1, 3 | squareColors: [ 4 | [0,0,0], 5 | ], 6 | example: { 7 | thing1: { 8 | a: 1, 9 | b: 2, 10 | } 11 | } 12 | }; 13 | 14 | function reducer(state = initialState, action) { 15 | switch(action.type) { 16 | case 'CHANGE_EXAMPLE': { 17 | const newState = { ...state }; 18 | newState.example.thing1.a = `changed! ${Math.random()}`; 19 | newState.example.thing1.b = `changed! ${Math.random()}`; 20 | return newState; 21 | } 22 | case 'CHANGE_COLORS': { 23 | return { 24 | ...state, 25 | squareColors: { 26 | ...state.squareColors, 27 | ...action.newColors, 28 | }, 29 | }; 30 | } 31 | case 'CHANGE_GRID_SIZE': { 32 | return { 33 | ...state, 34 | gridSize: action.newSize, 35 | squareColors: Array(action.newSize * action.newSize) 36 | .fill([0,0,0]), 37 | }; 38 | } 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | export default reducer; 45 | -------------------------------------------------------------------------------- /packages/enhanced-simple-shared-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enhanced-simple-shared-state", 3 | "version": "0.0.1", 4 | "description": "", 5 | "workspaces": true, 6 | "module": "index.js", 7 | "main": "dist/simple-derived-state.es6.umd.js", 8 | "unpkg": "dist/simple-derived-state.es6.umd.js", 9 | "homepage": "https://simplesharedstate.com", 10 | "scripts": { 11 | "lint": "eslint .", 12 | "test": "jest --watchAll", 13 | "build": "webpack ./src/index.js" 14 | }, 15 | "keywords": [ 16 | "shared state", 17 | "redux", 18 | "mobx", 19 | "react", 20 | "context", 21 | "context api" 22 | ], 23 | "author": "Rob Christian ", 24 | "license": "ISC", 25 | "dependencies": {}, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/rm-rf-etc/simple-shared-state" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.0.0", 32 | "@babel/preset-env": "^7.7.7", 33 | "babel-loader": "^8.0.6", 34 | "esm": "^3.2.25", 35 | "jest": "^24.9.0", 36 | "webpack": "^4.41.5", 37 | "webpack-cli": "^3.3.10" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/use-simple-shared-state/README.md: -------------------------------------------------------------------------------- 1 | # SimpleSharedState React Hook 2 | 3 | Redux is verbose. SimpleSharedState is brief. 4 | 5 | - Docs: [https://simplesharedstate.com](https://simplesharedstate.com) 6 | - Repo: [https://github.com/rm-rf-etc/simple-shared-state](https://github.com/rm-rf-etc/simple-shared-state) 7 | - Example app: https://simple-shared-state.stackblitz.io/ 8 | - Edit online: https://stackblitz.com/edit/simple-shared-state 9 | 10 | 11 | ## Get It 12 | 13 | ``` 14 | npm install use-simple-shared-state 15 | ``` 16 | 17 | ## Basic Use 18 | 19 | Assuming you already have a store made with `simple-shared-state`: 20 | ```javascript 21 | import React from "react"; 22 | import useSharedState from "use-simple-shared-state"; 23 | import store from "./store.js"; 24 | 25 | const selectors = [ 26 | (state) => state.counter1, 27 | (state) => state.examples.user, 28 | // put as many selectors here as you need 29 | ]; 30 | 31 | export const MyComponent = () => { 32 | const [count1, someObject] = useSharedState(store, selectors); 33 | return ( 34 |
35 |

Hello World

36 | {count1} 37 |
{JSON.stringify(someObject)}
38 |
39 | ) 40 | }; 41 | ``` 42 | 43 | That's all there is to it. 44 | -------------------------------------------------------------------------------- /examples/include-via-script-tags/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Page 7 | 8 | 9 | 10 | 16 | 17 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/store.js: -------------------------------------------------------------------------------- 1 | import { Store, partialArray } from "simple-shared-state"; 2 | 3 | const initialState = { 4 | counters: { 5 | count1: 0, 6 | count2: 0, 7 | }, 8 | todos: [ 9 | { id: 0, label: 1, key: "a" }, 10 | { id: 1, label: 2, key: "b" }, 11 | { id: 2, label: 3, key: "c" }, 12 | ], 13 | }; 14 | 15 | const actions = (getState) => ({ 16 | setCounter1: (value) => ({ 17 | counters: { count1: +value }, 18 | }), 19 | incrementCounter1: () => ({ 20 | counters: { count1: getState(s => s.counters.count1) + 1 }, 21 | }), 22 | decrementCounter1: () => ({ 23 | counters: { count1: getState(s => s.counters.count1) - 1 }, 24 | }), 25 | pushTodo: (label) => { 26 | const len = getState(s => s.todos.length); 27 | return { 28 | todos: partialArray(len, { 29 | label, 30 | key: Math.random().toString().split(".")[1], 31 | }), 32 | }; 33 | }, 34 | popTodo: () => ({ 35 | todos: [].pop, 36 | }), 37 | removeTodo: (key) => { 38 | const todos = getState(s => s.todos); 39 | const idx = todos.findIndex((todo) => todo.key === key); 40 | const newArray = todos.slice(0, idx).concat(todos.slice(idx + 1, todos.length)); 41 | 42 | return { 43 | todos: newArray, 44 | }; 45 | }, 46 | }); 47 | 48 | export default new Store(initialState, actions, window.__REDUX_DEVTOOLS_EXTENSION__); 49 | -------------------------------------------------------------------------------- /packages/enhanced-simple-shared-state/index.js: -------------------------------------------------------------------------------- 1 | 2 | export default (Store) => { 3 | Store.prototype.newDerivedState = newDerivedState; 4 | Store.prototype.addPartialState = addPartialState; 5 | }; 6 | 7 | function newDerivedState({ label, selectors, getPartial, putPartial, actions }) { 8 | const partialStateStore = new Store({}); 9 | const actionsObject = actions(partialStateStore); 10 | 11 | // Create a lifted version of each developer-provided action. 12 | Object.keys(actionsObject).forEach((actionName) => { 13 | const actionLabel = `${label}.${actionName}()`; 14 | 15 | partialStateStore.actions[actionName] = (...args) => { 16 | const branch = actionsObject[actionName](...args); 17 | this.dispatchTyped(actionLabel, putPartial(args, branch)); 18 | }; 19 | }); 20 | 21 | const unwatch = this.watchBatch(selectors, (args) => { 22 | partialStateStore.dispatchTyped("derived change", getPartial(args)); 23 | }); 24 | 25 | partialStateStore.destroy = () => unwatch(); 26 | 27 | return partialStateStore; 28 | }; 29 | 30 | function addPartialState({ label, partialStateStore }) { 31 | this.dispatchTyped("state received", { 32 | [label]: partialStateStore.getState(), 33 | }); 34 | partialStateStore.watchDispatch(() => { 35 | this.stateTree[label] = partialStateStore.getState(); 36 | }); 37 | this.watch(s => s[label], (state) => { 38 | partialStateStore.stateTree = Object.assign({}, state); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/simple-shared-state/src/merge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @member deleted 3 | * @memberof module:SimpleSharedState 4 | * @const {number} deleted - A globally unique object to reference when you want to delete 5 | * things from state. 6 | * 7 | * @example 8 | * // `deleted` is essentially just a Symbol, but works in IE. 9 | * const deleted = new Number(0); 10 | * deleted === 0; // false 11 | * deleted === deleted; // true 12 | * 13 | * @example 14 | * import { Store, deleted } from "simple-shared-state"; 15 | * 16 | * const actions = () => ({ 17 | * removeB: (prop) => ({ 18 | * [prop]: deleted, 19 | * }), 20 | * }); 21 | * const store = new Store({ a: 1, b: 2 }, actions); 22 | * console.log(store.getState()); // { a: 1, b: 2 } 23 | * 24 | * store.actions.removeB("b"); 25 | * 26 | * // state: { a: 1 } 27 | */ 28 | export const deleted = new Number(); 29 | 30 | const isArray = Array.isArray; 31 | 32 | export const merge = (tree, branch) => { 33 | if (isArray(branch)) { 34 | return branch; 35 | } 36 | if (tree && branch && typeof tree === "object") { 37 | Object.keys(branch).forEach((key) => { 38 | if (isArray(branch[key])) { 39 | tree[key] = branch[key]; 40 | return; 41 | } 42 | if (branch[key] === deleted) { 43 | delete tree[key]; 44 | return; 45 | } 46 | tree[key] = merge(tree[key], branch[key]); 47 | }); 48 | return Object.assign(isArray(tree) ? [] : {}, tree); 49 | } 50 | return branch; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/simple-shared-state/test_bundle/merge.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.merge=t():e.merge=t()}(Function("return this")(),(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t),r.d(t,"deleted",(function(){return n})),r.d(t,"merge",(function(){return u}));const n=new Number,o=Array.isArray,u=(e,t)=>o(t)?t:e&&t&&"object"==typeof e?(Object.keys(t).forEach(r=>{o(t[r])?e[r]=t[r]:t[r]!==n?e[r]=u(e[r],t[r]):delete e[r]}),Object.assign(o(e)?[]:{},e)):t}])})); -------------------------------------------------------------------------------- /packages/simple-shared-state/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = [ 4 | { 5 | entry: "./src/index.js", 6 | mode: "production", 7 | output: { 8 | library: "SimpleSharedState", 9 | libraryTarget: "umd", 10 | filename: "simple-shared-state.es6.umd.js", 11 | path: path.resolve(__dirname, "dist"), 12 | globalObject: 'Function("return this")()', 13 | }, 14 | }, 15 | { 16 | entry: "./src/index.js", 17 | mode: "production", 18 | output: { 19 | library: "SimpleSharedState", 20 | libraryTarget: "umd", 21 | filename: "simple-shared-state.es5.umd.js", 22 | path: path.resolve(__dirname, "dist"), 23 | globalObject: 'Function("return this")()', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.m?js$/, 29 | use: { 30 | loader: "babel-loader", 31 | options: { 32 | presets: ["@babel/preset-env"], 33 | plugins: ["@babel/plugin-transform-object-assign"] 34 | } 35 | } 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | entry: "./src/merge.js", 42 | mode: "production", 43 | output: { 44 | library: "merge", 45 | libraryTarget: "umd", 46 | filename: "merge.js", 47 | path: path.resolve(__dirname, "test_bundle"), 48 | globalObject: 'Function("return this")()', 49 | }, 50 | module: { 51 | rules: [ 52 | { 53 | test: /src\/merge.js$/, 54 | exclude: /.*/, 55 | use: { 56 | loader: "babel-loader", 57 | options: { 58 | presets: ["@babel/preset-env"], 59 | } 60 | } 61 | } 62 | ] 63 | } 64 | }, 65 | ]; 66 | -------------------------------------------------------------------------------- /packages/simple-shared-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-shared-state", 3 | "version": "5.0.1", 4 | "description": "Easily share state between components using a no-frills observable object API", 5 | "workspaces": true, 6 | "module": "src/index.js", 7 | "main": "dist/simple-shared-state.es6.umd.js", 8 | "unpkg": "dist/simple-shared-state.es6.umd.js", 9 | "homepage": "https://simplesharedstate.com", 10 | "scripts": { 11 | "lint": "eslint .", 12 | "test": "jest --watchAll", 13 | "doc": "jsdoc -c ./jsdoc.json -t ../../node_modules/docdash", 14 | "build": "webpack", 15 | "size": "npx size-limit" 16 | }, 17 | "size-limit": [ 18 | { 19 | "path": "dist/simple-shared-state.es6.umd.js", 20 | "limit": "10 KB" 21 | } 22 | ], 23 | "keywords": [ 24 | "shared state", 25 | "redux", 26 | "mobx", 27 | "react", 28 | "context", 29 | "context api" 30 | ], 31 | "author": "Rob Christian ", 32 | "license": "ISC", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/rm-rf-etc/simple-shared-state" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.0.0", 39 | "@babel/plugin-transform-object-assign": "^7.7.4", 40 | "@babel/preset-env": "^7.7.7", 41 | "@size-limit/preset-small-lib": "^3.0.0", 42 | "babel-loader": "^8.0.6", 43 | "docdash": "^1.2.0", 44 | "esm": "^3.2.25", 45 | "jest": "^24.9.0", 46 | "jsdoc": "^3.6.3", 47 | "redux": "^4.0.5", 48 | "taffydb": "^2.7.3", 49 | "webpack": "^4.41.5", 50 | "webpack-cli": "^3.3.10" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./App.css"; 3 | import store from "./store"; 4 | import useSharedState from "use-simple-shared-state"; 5 | 6 | const selectors = [ 7 | (state) => state.counters.count1, 8 | (state) => state.todos, 9 | ]; 10 | 11 | const { 12 | setCounter1, 13 | incrementCounter1, 14 | decrementCounter1, 15 | pushTodo, 16 | popTodo, 17 | removeTodo, 18 | } = store.actions; 19 | 20 | const App = () => { 21 | const [count1, todos] = useSharedState(store, selectors); 22 | const [field1, setField1] = useState(0); 23 | const [field3, setField3] = useState(""); 24 | 25 | return ( 26 |
27 |
28 | {count1} 29 | 30 | 31 |
32 | setField1(+target.value)}> 33 | 34 |
35 |
36 |
37 | setField3(target.value)}> 38 | 39 | 40 |
41 |
42 |
    43 | {todos.map((todo) => ( 44 |
  • 45 | {todo.label} 46 | 47 |
  • 48 | ))} 49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /packages/react-test-ground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/redux-comparison/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Shared State 2 | 3 | Redux is verbose. SimpleSharedState is brief. 4 | 5 | - Docs: [https://simplesharedstate.com](https://simplesharedstate.com) 6 | - Repo: [https://github.com/rm-rf-etc/simple-shared-state](https://github.com/rm-rf-etc/simple-shared-state) 7 | - Example app: [https://simple-shared-state.stackblitz.io/](https://simple-shared-state.stackblitz.io/) 8 | - Edit online: [https://stackblitz.com/edit/simple-shared-state](https://stackblitz.com/edit/simple-shared-state) 9 | 10 | 11 | ## Status 12 | 13 | SimpleSharedState is still relatively experimental. Versions are following semver, but the version being greater 14 | than 1.0 doesn't mean it's considered production ready just yet. Please review the project source and tests to 15 | determine if it's viable for your project. More elaborate tests are needed to test SimpleSharedState performance 16 | against Redux. 17 | 18 | 19 | ## Get It 20 | 21 | ``` 22 | npm install simple-shared-state 23 | ``` 24 | 25 | If using script tags: 26 | ```html 27 | 28 | 31 | ``` 32 | 33 | ### Support for Internet Explorer 34 | 35 | Use `dist/simple-shared-state.es5.umd.js`. For example: 36 | ```html 37 | 38 | ``` 39 | 40 | 41 | ## Basic Use 42 | 43 | First create a store. 44 | ```javascript 45 | import { Store } from "simple-shared-state"; 46 | 47 | const initialState = { 48 | user: { 49 | name: "Alice", 50 | slogan: "simple-shared-state makes apps fun again", 51 | }, 52 | }; 53 | const actions = () => ({ 54 | changeSlogan: (newSlogan) => ({ 55 | user: { 56 | slogan: newSlogan, 57 | }, 58 | }), 59 | }); 60 | const store = new Store(initialState, actions); 61 | ``` 62 | Then create a watcher. Don't worry about error handling in selectors, just return 63 | the state that you want. 64 | ```javascript 65 | const selector = (state) => state.user; 66 | 67 | store.watch(selector, (state) => { 68 | console.log("user snapshot:", state); 69 | }); 70 | ``` 71 | Then call your action to update state and trigger the watch handler. 72 | ```javascript 73 | store.actions.changeSlogan("simple-shared-state is better than cat memes"); 74 | // 'user snapshot:' { name: 'Alice', slogan: 'simple-shared-state is better than cat memes' } 75 | ``` 76 | 77 | 78 | ## Redux Devtools 79 | 80 | Works with devtools, of course. Just pass in the reference like this: 81 | ```javascript 82 | const store = new Store(initialState, actions, window.__REDUX_DEVTOOLS_EXTENSION__); 83 | ``` 84 | 85 | 86 | ## React Hooks 87 | 88 | [useSimpleSharedState](https://npmjs.com/package/use-simple-shared-state) 89 | 90 | 91 | ## Future Work 92 | 93 | - Fix redux devtools integration 94 | - Add support for async/await in action creators 95 | - Support typescript types 96 | - Explore potential to optimize selector processing for very large state trees 97 | -------------------------------------------------------------------------------- /packages/simple-shared-state/README.md: -------------------------------------------------------------------------------- 1 | # Simple Shared State 2 | 3 | Redux is verbose. SimpleSharedState is brief. 4 | 5 | - Docs: [https://simplesharedstate.com](https://simplesharedstate.com) 6 | - Repo: [https://github.com/rm-rf-etc/simple-shared-state](https://github.com/rm-rf-etc/simple-shared-state) 7 | - Example app: [https://simple-shared-state.stackblitz.io/](https://simple-shared-state.stackblitz.io/) 8 | - Edit online: [https://stackblitz.com/edit/simple-shared-state](https://stackblitz.com/edit/simple-shared-state) 9 | 10 | 11 | ## Status 12 | 13 | SimpleSharedState is still relatively experimental. Versions are following semver, but the version being greater 14 | than 1.0 doesn't mean it's considered production ready just yet. Please review the project source and tests to 15 | determine if it's viable for your project. More elaborate tests are needed to test SimpleSharedState performance 16 | against Redux. 17 | 18 | 19 | ## Get It 20 | 21 | ``` 22 | npm install simple-shared-state 23 | ``` 24 | 25 | If using script tags: 26 | ```html 27 | 28 | 31 | ``` 32 | 33 | ### Support for Internet Explorer 34 | 35 | Use `dist/simple-shared-state.es5.umd.js`. For example: 36 | ```html 37 | 38 | ``` 39 | 40 | 41 | ## Basic Use 42 | 43 | First create a store. 44 | ```javascript 45 | import { Store } from "simple-shared-state"; 46 | 47 | const initialState = { 48 | user: { 49 | name: "Alice", 50 | slogan: "simple-shared-state makes apps fun again", 51 | }, 52 | }; 53 | const actions = () => ({ 54 | changeSlogan: (newSlogan) => ({ 55 | user: { 56 | slogan: newSlogan, 57 | }, 58 | }), 59 | }); 60 | const store = new Store(initialState, actions); 61 | ``` 62 | Then create a watcher. Don't worry about error handling in selectors, just return 63 | the state that you want. 64 | ```javascript 65 | const selector = (state) => state.user; 66 | 67 | store.watch(selector, (state) => { 68 | console.log("user snapshot:", state); 69 | }); 70 | ``` 71 | Then call your action to update state and trigger the watch handler. 72 | ```javascript 73 | store.actions.changeSlogan("simple-shared-state is better than cat memes"); 74 | // 'user snapshot:' { name: 'Alice', slogan: 'simple-shared-state is better than cat memes' } 75 | ``` 76 | 77 | 78 | ## Redux Devtools 79 | 80 | Works with devtools, of course. Just pass in the reference like this: 81 | ```javascript 82 | const store = new Store(initialState, actions, window.__REDUX_DEVTOOLS_EXTENSION__); 83 | ``` 84 | 85 | 86 | ## React Hooks 87 | 88 | [useSimpleSharedState](https://npmjs.com/package/use-simple-shared-state) 89 | 90 | 91 | ## Future Work 92 | 93 | - Fix redux devtools integration 94 | - Add support for async/await in action creators 95 | - Support typescript types 96 | - Explore potential to optimize selector processing for very large state trees 97 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/appSSS.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridApp from './components/gridApp'; 3 | import ColorSquare from './components/colorSquareSSS'; 4 | import randomRGB from './lib/randomRGB'; 5 | import useSharedState from 'use-simple-shared-state'; 6 | import store from './storeSSS'; 7 | import styled from 'styled-components'; 8 | import getStats from './lib/stats'; 9 | import Stats from 'stats-incremental'; 10 | 11 | let stats = Stats(); 12 | let running = false; 13 | 14 | 15 | const TwoCol = styled.div` 16 | display: grid; 17 | grid-gap: 20px; 18 | grid-template-columns: 0fr auto; 19 | `; 20 | const Col = styled.div(({ start }) => ` 21 | grid-column: ${start}; 22 | grid-row: 1; 23 | `); 24 | const Pre = styled.pre` 25 | width: 100%; 26 | `; 27 | 28 | const { changeColors, changeGridSize, changeExample } = store.actions; 29 | 30 | const resetScores = () => stats = Stats(); 31 | 32 | const changeSize = (e) => changeGridSize(e.target.value); 33 | 34 | const App = () => { 35 | const [gridSize, example] = useSharedState(store, [s => s.gridSize, s => s.example]); 36 | const [scoreState, setScoreState] = React.useState(''); 37 | 38 | const scoreIt = React.useCallback(() => { 39 | const t1 = performance.now(); 40 | 41 | const changes = {}; 42 | for(let i=0; i < (gridSize * gridSize); i++) { 43 | changes[i] = randomRGB(); 44 | } 45 | changeColors(changes); 46 | 47 | const t2 = performance.now(); 48 | stats.update(t2 - t1); 49 | 50 | setScoreState(getStats('SSS', stats.getAll())); 51 | }, [gridSize]); 52 | 53 | const handleClick = React.useCallback(() => { 54 | if (running) { 55 | running = false; 56 | return; 57 | } 58 | running = true; 59 | loop(); 60 | 61 | function loop() { 62 | if (running) { 63 | scoreIt(); 64 | setTimeout(loop, 500); 65 | } 66 | } 67 | }, [scoreIt]); 68 | 69 | return ( 70 | <> 71 |

Simple Shared State

72 | 73 | 74 | 77 |
 78 |             Metrics:
 79 |             {' '}
 80 |             {scoreState}
 81 |           
82 | 83 |
 84 |             {JSON.stringify(example, null, '\t')}
 85 |           
86 |
 87 |             example.thing1.a: {example.thing1.a}
 88 |           
89 |
 90 |             example.thing1.b: {example.thing1.b}
 91 |           
92 | 93 | 94 | 101 | 102 |
103 | 104 | ); 105 | } 106 | 107 | export default App; 108 | -------------------------------------------------------------------------------- /packages/react-test-ground/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /packages/redux-comparison/src/appRedux.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridApp from './components/gridApp'; 3 | import ColorSquareGrid from './components/squareGrid'; 4 | import ColorSquare from './components/colorSquareRedux'; 5 | import randomRGB from './lib/randomRGB'; 6 | import { useSelector } from 'react-redux'; 7 | import store from './storeRedux'; 8 | import styled from 'styled-components'; 9 | import getStats from './lib/stats'; 10 | import Stats from 'stats-incremental'; 11 | 12 | let stats = Stats(); 13 | 14 | let running = false; 15 | 16 | const TwoCol = styled.div` 17 | display: grid; 18 | grid-gap: 20px; 19 | grid-template-columns: 0fr auto; 20 | `; 21 | const Col = styled.div(({ start }) => ` 22 | grid-column: ${start}; 23 | grid-row: 1; 24 | `); 25 | const Pre = styled.pre` 26 | width: 100%; 27 | `; 28 | 29 | const resetScores = () => stats = Stats(); 30 | 31 | const changeSize = (e) => store.dispatch({ 32 | type: 'CHANGE_GRID_SIZE', 33 | newSize: e.target.value, 34 | }); 35 | 36 | const changeExample = () => store.dispatch({ 37 | type: 'CHANGE_EXAMPLE', 38 | }); 39 | 40 | const App = () => { 41 | const gridSize = useSelector(state => state.gridSize); 42 | const example = useSelector(state => state.example); 43 | const [scoreState, setScoreState] = React.useState(''); 44 | // useSelector(state => state.example.thing1.a); 45 | 46 | const scoreIt = React.useCallback(() => { 47 | const t1 = performance.now(); 48 | 49 | const changes = {}; 50 | for(let i=0; i < gridSize * gridSize; i++) { 51 | changes[i] = randomRGB(); 52 | } 53 | store.dispatch({ 54 | type: 'CHANGE_COLORS', 55 | newColors: changes, 56 | }); 57 | 58 | const t2 = performance.now(); 59 | stats.update(t2 - t1); 60 | 61 | setScoreState(getStats('Redux', stats.getAll())); 62 | }, [gridSize]); 63 | 64 | const handleClick = React.useCallback(() => { 65 | if (running) { 66 | running = false; 67 | return; 68 | } 69 | running = true; 70 | loop(); 71 | 72 | function loop() { 73 | if (running) { 74 | scoreIt(); 75 | setTimeout(loop, 500); 76 | } 77 | } 78 | }, [scoreIt]); 79 | 80 | return ( 81 | <> 82 |

Redux

83 | 84 | 85 | 88 |
 89 |             Metrics:
 90 |             {' '}
 91 |             {scoreState}
 92 |           
93 | 94 |
 95 |             {JSON.stringify(example, null, '\t')}
 96 |           
97 |
 98 |             example.thing1.a: {example && example.thing1.a}
 99 |           
100 |
101 |             example.thing1.b: {example && example.thing1.b}
102 |           
103 | 104 | 105 | 113 | 114 |
115 | 116 | ); 117 | } 118 | 119 | export default App; 120 | -------------------------------------------------------------------------------- /packages/redux-comparison/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## TODO 4 | 1. Write a component that is a colored square which changes color randomly with a button press 5 | 2. Break this component out into it's own file and configure App to generate a grid of them 6 | 3. Make color randomization per component 7 | * How do we do this? 8 | * store colorsquares in a list in state 9 | * on button press, loop through this list and dispatch an action for each square which updates it's color 10 | * so state will be: 11 | state = { 12 | ...state, 13 | colorSquares: [[r,g,b], ...] 14 | } 15 | 16 | handleClick will: state.colorSquares.forEach(() => dispatch(updateSquare)) 17 | 4. Make grid size configurable 18 | 5. Make color change on a timer 19 | 6. Make tempo configurable 20 | 21 | ## Available Scripts 22 | 23 | In the project directory, you can run 24 | 25 | ### `yarn start` 26 | 27 | Runs the app in the development mode.
28 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 29 | 30 | The page will reload if you make edits.
31 | You will also see any lint errors in the console. 32 | 33 | ### `yarn test` 34 | 35 | Launches the test runner in the interactive watch mode.
36 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 37 | 38 | ### `yarn build` 39 | 40 | Builds the app for production to the `build` folder.
41 | It correctly bundles React in production mode and optimizes the build for the best performance. 42 | 43 | The build is minified and the filenames include the hashes.
44 | Your app is ready to be deployed! 45 | 46 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 47 | 48 | ### `yarn eject` 49 | 50 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 51 | 52 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 53 | 54 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 55 | 56 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 57 | 58 | ## Learn More 59 | 60 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 61 | 62 | To learn React, check out the [React documentation](https://reactjs.org/). 63 | 64 | ### Code Splitting 65 | 66 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 67 | 68 | ### Analyzing the Bundle Size 69 | 70 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 71 | 72 | ### Making a Progressive Web App 73 | 74 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 75 | 76 | ### Advanced Configuration 77 | 78 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 79 | 80 | ### Deployment 81 | 82 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 83 | 84 | ### `yarn build` fails to minify 85 | 86 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 87 | -------------------------------------------------------------------------------- /packages/simple-shared-state/dist/simple-shared-state.es6.umd.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SimpleSharedState=e():t.SimpleSharedState=e()}(Function("return this")(),(function(){return function(t){var e={};function n(s){if(e[s])return e[s].exports;var r=e[s]={i:s,l:!1,exports:{}};return t[s].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=t,n.c=e,n.d=function(t,e,s){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:s})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(s,r,function(e){return t[e]}.bind(null,r));return s},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";n.r(e);const s=new Number,r=Array.isArray,i=(t,e)=>r(e)?e:t&&e&&"object"==typeof t?(Object.keys(e).forEach(n=>{r(e[n])?t[n]=e[n]:e[n]!==s?t[n]=i(t[n],e[n]):delete t[n]}),Object.assign(r(t)?[]:{},t)):e,o=Array.isArray;class c{constructor(t={},e,n){if(this.devtool,this.stateTree=Object.assign({},t),this.dispatching=!1,this.listeners=new Map,this.snapshots=new Map,this.dispatchListeners=new Set,this.actions={},n&&n.connect&&"function"==typeof n.connect&&(this.devtool=n.connect(),this.devtool.subscribe(t=>{"DISPATCH"===t.type&&"JUMP_TO_STATE"===t.payload.type&&this._applyBranch(JSON.parse(t.state))}),this.devtool.init(this.stateTree)),e&&"function"==typeof e){const t=e(this.getState.bind(this));Object.keys(t).forEach(e=>{const n=this.devtool?`${e}()`:"unknown";this.actions[e]=(...s)=>{this.dispatchTyped(n,t[e].apply(null,s))}})}}_applyBranch(t){this.dispatching=!0,i(this.stateTree,t),this.listeners.forEach((e,n)=>{let r;const c=this.snapshots.get(n),a=t=>{this.snapshots.set(n,t),e(t)};try{switch(r=n(t),r){case c:return;case s:return void(void 0!==c&&a(void 0));case void 0:return void(r=n(this.stateTree))}}catch(t){try{return void n(this.stateTree)}catch(t){}}r!==c&&(o(r)&&!r.isPartial?a(r):a(i(c,r)))}),this.dispatchListeners.forEach(t=>t()),this.dispatching=!1}watch(t,e,n=!0){if("function"!=typeof t||"function"!=typeof e)throw new Error("selector and handler must be functions");if(this.listeners.has(t))throw new Error("Cannot reuse selector");let s;try{s=t(this.stateTree),n&&e(s)}catch(t){}return this.listeners.set(t,e),this.snapshots.set(t,s),()=>{this.listeners.delete(t),this.snapshots.delete(t)}}watchBatch(t,e){if(!t||"function"!=typeof t.forEach)throw new Error("selectors must be a list of functions");if("function"!=typeof e)throw new Error("handler is not a function");const n=[];let s=0,r=!1;t.forEach(e=>{if("function"!=typeof e)throw t.forEach(t=>this.listeners.delete(t)),new Error("selector must be a function");let i=s++;try{n[i]=e(this.stateTree)}catch(t){n[i]=void 0}this.watch(e,t=>{n[i]=t,r=!0},!1)});const i=()=>{r&&(e(n.slice()),r=!1)};return this.dispatchListeners.add(i),e(n.slice()),()=>{this.dispatchListeners.delete(i),t.forEach(t=>this.listeners.delete(t))}}watchDispatch(t){if("function"!=typeof t)throw new Error("handler must be a function");return this.dispatchListeners.add(t),()=>this.dispatchListeners.delete(t)}getState(t){if(t&&"function"==typeof t){let n;try{n=(e=t(this.stateTree))&&"object"==typeof e?Object.assign(o(e)?[]:{},e):e}catch(t){}return n}var e;return Object.assign({},this.stateTree)}dispatchTyped(t="unknown",e){if(this.dispatching)throw new Error("can't dispatch while dispatching");if(!e)throw new Error("can't dispatch invalid branch");if("function"==typeof e&&(e=e(this.getState())),"object"!=typeof e)throw new Error("dispatch got invalid branch");this._applyBranch(e),this.devtool&&this.devtool.send(t,this.getState())}dispatch(t){this.dispatchTyped("unknown",t)}}n.d(e,"Store",(function(){return c})),n.d(e,"deleted",(function(){return s}))}])})); -------------------------------------------------------------------------------- /packages/redux-comparison/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/react-test-ground/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/simple-shared-state/dist/simple-shared-state.es5.umd.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SimpleSharedState=e():t.SimpleSharedState=e()}(Function("return this")(),(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";function r(){return(r=Object.assign||function(t){for(var e=1;e0&&void 0!==arguments[0]?arguments[0]:{},r=arguments.length>1?arguments[1]:void 0,o=arguments.length>2?arguments[2]:void 0;if(f(this,t),this.devtool,this.stateTree=u({},n),this.dispatching=!1,this.listeners=new Map,this.snapshots=new Map,this.dispatchListeners=new Set,this.actions={},o&&o.connect&&"function"==typeof o.connect&&(this.devtool=o.connect(),this.devtool.subscribe((function(t){"DISPATCH"===t.type&&"JUMP_TO_STATE"===t.payload.type&&e._applyBranch(JSON.parse(t.state))})),this.devtool.init(this.stateTree)),r&&"function"==typeof r){var i=r(this.getState.bind(this));Object.keys(i).forEach((function(t){var n=e.devtool?"".concat(t,"()"):"unknown";e.actions[t]=function(){for(var r=arguments.length,o=new Array(r),s=0;s2&&void 0!==arguments[2])||arguments[2];if("function"!=typeof t||"function"!=typeof e)throw new Error("selector and handler must be functions");if(this.listeners.has(t))throw new Error("Cannot reuse selector");try{n=t(this.stateTree),o&&e(n)}catch(t){}return this.listeners.set(t,e),this.snapshots.set(t,n),function(){r.listeners.delete(t),r.snapshots.delete(t)}}},{key:"watchBatch",value:function(t,e){var n=this;if(!t||"function"!=typeof t.forEach)throw new Error("selectors must be a list of functions");if("function"!=typeof e)throw new Error("handler is not a function");var r=[],o=0,i=!1;t.forEach((function(e){if("function"!=typeof e)throw t.forEach((function(t){return n.listeners.delete(t)})),new Error("selector must be a function");var s=o++;try{r[s]=e(n.stateTree)}catch(t){r[s]=void 0}n.watch(e,(function(t){r[s]=t,i=!0}),!1)}));var s=function(){i&&(e(r.slice()),i=!1)};return this.dispatchListeners.add(s),e(r.slice()),function(){n.dispatchListeners.delete(s),t.forEach((function(t){return n.listeners.delete(t)}))}}},{key:"watchDispatch",value:function(t){var e=this;if("function"!=typeof t)throw new Error("handler must be a function");return this.dispatchListeners.add(t),function(){return e.dispatchListeners.delete(t)}}},{key:"getState",value:function(t){if(t&&"function"==typeof t){var e;try{e=(n=t(this.stateTree))&&"object"===a(n)?u(l(n)?[]:{},n):n}catch(t){}return e}var n;return u({},this.stateTree)}},{key:"dispatchTyped",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"unknown",e=arguments.length>1?arguments[1]:void 0;if(this.dispatching)throw new Error("can't dispatch while dispatching");if(!e)throw new Error("can't dispatch invalid branch");if("function"==typeof e&&(e=e(this.getState())),"object"!==a(e))throw new Error("dispatch got invalid branch");this._applyBranch(e),this.devtool&&this.devtool.send(t,this.getState())}},{key:"dispatch",value:function(t){this.dispatchTyped("unknown",t)}}])&&h(e.prototype,n),r&&h(e,r),t}();n.d(e,"Store",(function(){return p})),n.d(e,"deleted",(function(){return i}))}])})); -------------------------------------------------------------------------------- /packages/simple-shared-state/src/store.js: -------------------------------------------------------------------------------- 1 | import { merge, deleted } from "./merge"; 2 | 3 | const isArray = Array.isArray; 4 | 5 | /** 6 | * @module SimpleSharedState 7 | */ 8 | 9 | export class Store { 10 | /** 11 | * @description Create a new store instance. 12 | * 13 | * @constructor 14 | * @param {object} initialState - Any plain JS object (Arrays not allowed at the top level). 15 | * @param {function} [actions] - A function, which takes a reference to `store`, and returns an object of 16 | * actions for invoking changes to state. 17 | * @param {function} [devtool] - Provide a reference to `window.__REDUX_DEVTOOLS_EXTENSION__` to enable 18 | * redux devtools. 19 | */ 20 | constructor(initialState = {}, getActions, useDevtool) { 21 | this.devtool; 22 | this.stateTree = Object.assign({}, initialState); 23 | this.dispatching = false; 24 | this.listeners = new Map(); 25 | this.snapshots = new Map(); 26 | this.dispatchListeners = new Set(); 27 | this.actions = {}; 28 | 29 | if (useDevtool && useDevtool.connect && typeof useDevtool.connect === "function") { 30 | // this adapts SimpleSharedState to work with redux devtools 31 | this.devtool = useDevtool.connect(); 32 | this.devtool.subscribe((message) => { 33 | if (message.type === "DISPATCH" && message.payload.type === "JUMP_TO_STATE") { 34 | this._applyBranch(JSON.parse(message.state)); 35 | } 36 | }); 37 | this.devtool.init(this.stateTree); 38 | } 39 | 40 | if (getActions && typeof getActions === "function") { 41 | const actions = getActions(this.getState.bind(this)); 42 | 43 | Object.keys(actions).forEach((actionName) => { 44 | const actionType = this.devtool ? `${actionName}()` : "unknown"; 45 | 46 | this.actions[actionName] = (...args) => { 47 | this.dispatchTyped(actionType, actions[actionName].apply(null, args)); 48 | }; 49 | }); 50 | } 51 | } 52 | 53 | _applyBranch(branch) { 54 | this.dispatching = true; 55 | merge(this.stateTree, branch); 56 | 57 | this.listeners.forEach((handler, selector) => { 58 | let change; 59 | const snapshot = this.snapshots.get(selector); 60 | const submit = (value) => { 61 | this.snapshots.set(selector, value); 62 | handler(value); 63 | }; 64 | 65 | try { 66 | // attempt selector only on the branch 67 | change = selector(branch); 68 | 69 | switch(change) { 70 | case snapshot: 71 | return; 72 | case deleted: 73 | if (snapshot !== undefined) submit(undefined); 74 | return; 75 | case undefined: 76 | change = selector(this.stateTree); 77 | // If ^this line throws, then **current state is also not applicable**, 78 | // meaning something was deleted, so we should proceed to catch block. 79 | return; 80 | // If `return` runs, then selector didn't throw, so exit early. 81 | } 82 | } 83 | catch (_) { 84 | try { 85 | selector(this.stateTree); 86 | // If ^selector works on new state then exit early. 87 | return; 88 | } catch (_) {} 89 | } 90 | 91 | // This test also covers the scenario where both are undefined. 92 | if (change === snapshot) return; 93 | 94 | if (isArray(change) && !change.isPartial) { 95 | submit(change); 96 | } else { 97 | submit(merge(snapshot, change)); 98 | } 99 | }); 100 | 101 | this.dispatchListeners.forEach((callback) => callback()); 102 | this.dispatching = false; 103 | }; 104 | 105 | /** 106 | * @method module:SimpleSharedState.Store#watch 107 | * @param {function} selector - A pure function which takes state and returns a piece of that state. 108 | * @param {function} handler - The listener which will receive the piece of state when changes occur. 109 | * @param {boolean} [runNow=true] - Pass false to prevent `handler` from being called immediately 110 | * after watch is called. 111 | * @returns {function} Invoke this function to destroy the listener. 112 | * 113 | * @description Creates a state listener which is associated with the selector. Every selector must 114 | * be globally unique, as they're stored internally in a Set. If `watch` receives a selector which 115 | * has already been passed before, `watch` will throw. Refer to the tests for more examples. `watch` 116 | * returns a function which, when called, removes the watcher / listener. 117 | */ 118 | watch(selector, handler, runNow = true) { 119 | if (typeof selector !== "function" || typeof handler !== "function") { 120 | throw new Error("selector and handler must be functions"); 121 | } 122 | if (this.listeners.has(selector)) { 123 | throw new Error("Cannot reuse selector"); 124 | } 125 | 126 | let snapshot; 127 | try { 128 | snapshot = selector(this.stateTree); 129 | if (runNow) handler(snapshot); 130 | } catch (_) {} 131 | 132 | this.listeners.set(selector, handler); 133 | this.snapshots.set(selector, snapshot); 134 | 135 | return () => { 136 | this.listeners.delete(selector); 137 | this.snapshots.delete(selector); 138 | }; 139 | }; 140 | 141 | /** 142 | * @method module:SimpleSharedState.Store#watchBatch 143 | * @param {Array|Set} selectors - A Set or Array of selector functions. Refer to 144 | * [Store#watch]{@link module:SimpleSharedState.Store#watch} for details about selector functions. 145 | * @param {function} handler - The listener which will receive the Array of state snapshots. 146 | * @returns {function} A callback that removes the dispatch watcher and cleans up after itself. 147 | * 148 | * @description Creates a dispatch listener from a list of selectors. Each selector yields a snapshot, 149 | * which is stored in an array and updated whenever the state changes. When dispatch happens, your 150 | * `handler` function will be called with the array of snapshots, ***if*** any snapshots have changed. 151 | * 152 | * @example 153 | * import { Store, partialArray } from "simple-shared-state"; 154 | * 155 | * const store = new Store({ 156 | * people: ["Alice", "Bob"], 157 | * }); 158 | * 159 | * const unwatch = store.watchBatch([ 160 | * (state) => state.people[0], 161 | * (state) => state.people[1], 162 | * ], (values) => console.log(values)); 163 | * 164 | * store.dispatch({ people: partialArray(1, "John") }); 165 | * // [ 'Alice', 'John' ] 166 | * 167 | * store.dispatch({ people: [ "Janet", "Jake", "James" ] }); 168 | * // [ 'Janet', 'Jake' ] 169 | * // notice "James" is not present, that's because of our selectors 170 | * 171 | * console.log(store.getState(s => s.people)); 172 | * // [ 'Janet', 'Jake', 'James' ] 173 | * 174 | * unwatch(); 175 | * store.dispatch({ people: [ "Justin", "Josh", store.deleted ] }); 176 | * // nothing happens, the watcher was removed 177 | * 178 | * console.log(store.getState(s => s.people)); 179 | * // [ 'Justin', 'Josh', <1 empty item> ] 180 | */ 181 | watchBatch(selectors, handler) { 182 | if (!selectors || typeof selectors.forEach !== "function") { 183 | throw new Error("selectors must be a list of functions"); 184 | } 185 | if (typeof handler !== "function") throw new Error("handler is not a function"); 186 | 187 | const snapshotsArray = []; 188 | 189 | let i = 0; 190 | let changed = false; 191 | selectors.forEach((fn) => { 192 | if (typeof fn !== "function") { 193 | selectors.forEach((fn) => this.listeners.delete(fn)); 194 | throw new Error("selector must be a function"); 195 | } 196 | 197 | let pos = i++; // pos = 0, i += 1 198 | try { 199 | snapshotsArray[pos] = fn(this.stateTree); 200 | } catch (_) { 201 | snapshotsArray[pos] = undefined; 202 | } 203 | this.watch(fn, (snapshot) => { 204 | snapshotsArray[pos] = snapshot; 205 | changed = true; 206 | }, false); 207 | }); 208 | 209 | const watchHandler = () => { 210 | if (changed) { 211 | handler(snapshotsArray.slice()); 212 | changed = false; 213 | } 214 | }; 215 | this.dispatchListeners.add(watchHandler); 216 | 217 | handler(snapshotsArray.slice()); 218 | 219 | return () => { 220 | this.dispatchListeners.delete(watchHandler); 221 | selectors.forEach((fn) => this.listeners.delete(fn)); 222 | }; 223 | }; 224 | 225 | /** 226 | * @method module:SimpleSharedState.Store#watchDispatch 227 | * 228 | * @description Listen for the after-dispatch event, which gets called with no arguments after every 229 | * dispatch completes. Dispatch is complete after all watchers have been called. 230 | * 231 | * @param {function} handler - A callback function. 232 | */ 233 | watchDispatch(handler) { 234 | if (typeof handler !== "function") throw new Error("handler must be a function"); 235 | this.dispatchListeners.add(handler); 236 | return () => this.dispatchListeners.delete(handler); 237 | }; 238 | 239 | /** 240 | * @method module:SimpleSharedState.Store#getState 241 | * 242 | * @param {function} [selector] - Optional but recommended function which returns a piece of the state. 243 | * Error handling not required, your selector will run inside a `try{} catch{}` block. 244 | * @returns {*} A shallow copy of the state tree, or a copy of the piece returned from the selector, 245 | * or undefined if the selector fails. 246 | */ 247 | getState(selector) { 248 | if (selector && typeof selector === "function") { 249 | let piece; 250 | try { 251 | piece = copy(selector(this.stateTree)); 252 | } catch (_) {} 253 | 254 | return piece; 255 | } 256 | 257 | return Object.assign({}, this.stateTree); 258 | }; 259 | 260 | /** 261 | * @method module:SimpleSharedState.Store#dispatchTyped 262 | * 263 | * @param {string} actionName - This is only for the benefit of providing a label in redux devtools. 264 | * @param {object|function} branch - A JavaScript object, or a function which takes state and returns a 265 | * JavaScript object. The object may contain any Array or JS primitive, but must be a plain JS object 266 | * at the top level, otherwise dispatch will throw. 267 | * 268 | * @description Please use [action creators]{@link module:SimpleSharedState#Store} instead of calling 269 | * dispatchTyped directly. 270 | */ 271 | dispatchTyped(actionName = "unknown", branch) { 272 | if (this.dispatching) throw new Error("can't dispatch while dispatching"); 273 | 274 | if (!branch) throw new Error("can't dispatch invalid branch"); 275 | 276 | if (typeof branch === "function") { 277 | branch = branch(this.getState()); 278 | } 279 | if (typeof branch !== "object") { 280 | throw new Error("dispatch got invalid branch"); 281 | } 282 | this._applyBranch(branch); 283 | 284 | if (this.devtool) this.devtool.send(actionName, this.getState()); 285 | }; 286 | 287 | /** 288 | * @method module:SimpleSharedState.Store#dispatch 289 | * 290 | * @param {object|function} branch - A JavaScript object, or a function which takes state and returns a 291 | * JavaScript object. The object may contain any Array or JS primitive, but must be a plain JS object 292 | * at the top level, otherwise dispatch will throw. 293 | * 294 | * @description Please use [action creators]{@link module:SimpleSharedState#Store} instead of calling 295 | * dispatch directly. 296 | * 297 | * @example 298 | * import { Store } from "simple-shared-state"; 299 | * 300 | * // Create a store with state: 301 | * const store = new Store({ 302 | * email: "user@example.com", 303 | * counters: { 304 | * likes: 1, 305 | * }, 306 | * todoList: [ 307 | * { label: "buy oat milk" }, 308 | * { label: "buy cat food" }, 309 | * ], 310 | * }); 311 | * 312 | * // To change email, call dispatch with a branch. The branch you provide must include the full path 313 | * // from the root of the state, to the value you want to change. 314 | * store.dispatch({ 315 | * email: "me@simplesharedstate.com", 316 | * }); 317 | * 318 | * // To increment likes: 319 | * store.dispatch((state) => ({ 320 | * counters: { 321 | * likes: state.counters.likes + 1, 322 | * }, 323 | * })); 324 | * 325 | * // To delete any piece of state, use a reference to `store.deleted` as the value in the branch. 326 | * // To remove `counters` from the state entirely: 327 | * store.dispatch({ 328 | * counters: store.deleted, 329 | * }); 330 | * 331 | * // To update items in arrays, you can use `partialArray`: 332 | * store.dispatch({ 333 | * todoList: partialArray(1, { 334 | * label: "buy oat milk (because it requires 80 times less water than almond milk)", 335 | * }), 336 | * }); 337 | */ 338 | dispatch(branch) { 339 | this.dispatchTyped("unknown", branch); 340 | } 341 | }; 342 | 343 | function copy(thing) { 344 | return !thing || typeof thing !== "object" ? thing : Object.assign(isArray(thing) ? [] : {}, thing); 345 | } 346 | -------------------------------------------------------------------------------- /packages/simple-shared-state/test/main.test.js: -------------------------------------------------------------------------------- 1 | import * as redux from "redux"; 2 | const bundles = { 3 | esm: require("../src/index"), 4 | es5: require("../dist/simple-shared-state.es5.umd"), 5 | es6: require("../dist/simple-shared-state.es6.umd"), 6 | esm_merge: require("../src/merge").merge, 7 | es6_merge: require("../test_bundle/merge").merge, 8 | }; 9 | 10 | const _100_000 = 100000; 11 | const _50_000_000 = 50000000; 12 | const _100_000_000 = 100000000; 13 | 14 | function testRawJSPerformance(times) { 15 | describe.skip(`JS Performance over ${(times).toLocaleString()} repetitions`, () => { 16 | const actionType = "SOME_ACTION_TYPE"; 17 | let string = "?"; 18 | let intrvl; 19 | let num; 20 | const state = { 21 | some_prop: { 22 | correct_path: 1, 23 | }, 24 | }; 25 | intrvl = setInterval(() => { 26 | string = Math.random().toString().split(".")[1]; 27 | }, 0); 28 | it(`time required to compare strings ${(times).toLocaleString()} times`, () => { 29 | for (let i = 0; i < times; i++) { 30 | if (string === actionType) { 31 | num = i; 32 | } 33 | } 34 | expect(num).toEqual(undefined); 35 | }); 36 | it(`time required to lookup object properties ${(times).toLocaleString()} times`, () => { 37 | for (let i = 0; i < times; i++) { 38 | try { 39 | num = state.some_prop[string]; 40 | } catch (_) {} 41 | } 42 | expect(num).toEqual(undefined); 43 | }); 44 | it(`time required to lookup object properties ${(times).toLocaleString()} times`, () => { 45 | for (let i = 0; i < times; i++) { 46 | try { 47 | num = state.some_prop.correct_path; 48 | } catch (_) {} 49 | } 50 | expect(num).toEqual(1); 51 | }); 52 | afterAll(() => clearInterval(intrvl)); 53 | }); 54 | } 55 | testRawJSPerformance(_100_000); 56 | testRawJSPerformance(_50_000_000); 57 | testRawJSPerformance(_100_000_000); 58 | 59 | describe("Redux Performance", () => { 60 | const reducer = (oldState = { thing1: 1 }, action = {}) => { 61 | return { 62 | ...oldState, 63 | a: { 64 | thing1: action.value + 1, 65 | thing2: -(action.value + 1), 66 | }, 67 | }; 68 | }; 69 | const store = redux.createStore(reducer, { 70 | a: { 71 | thing1: 1, 72 | thing2: -1, 73 | thing3: "ignored", 74 | }, 75 | b: { 76 | ignored: "asdf", 77 | }, 78 | }); 79 | 80 | it(`run dispatch ${(_100_000).toLocaleString()} times`, (done) => { 81 | store.subscribe(() => { 82 | const state = store.getState(); 83 | if (state && state.a.thing1 === _100_000) done(); 84 | }); 85 | for (var i = 0; i < _100_000; i++) { 86 | store.dispatch({ type: "A", value: i }); 87 | } 88 | }); 89 | }); 90 | 91 | function testBundle(bundle) { 92 | 93 | describe("SimpleSharedState Performance", () => { 94 | const store = new bundle.Store({ 95 | a: [ 96 | { 97 | thing1: 1, 98 | thing2: -1, 99 | thing3: "ignored", 100 | }, 101 | ], 102 | b: { 103 | ignored: "asdf", 104 | }, 105 | }); 106 | 107 | it(`run dispatch ${(_100_000).toLocaleString()} times`, (done) => { 108 | store.watch((state) => state.a[0], (state) => { 109 | if (state.thing1 === _100_000) { 110 | done(); 111 | } 112 | }); 113 | for (var i = 0; i < _100_000; i++) { 114 | store.dispatch({ 115 | a: [ 116 | { 117 | thing1: i+1, 118 | thing2: -(i+1), 119 | }, 120 | ], 121 | }); 122 | } 123 | }); 124 | }); 125 | 126 | describe("Store", () => { 127 | let store = {}; 128 | 129 | beforeEach(() => { 130 | store = new bundle.Store({ 131 | friends: { 132 | "1": { 133 | name: "Alice", 134 | age: 25, 135 | }, 136 | "2": { 137 | name: "Bob", 138 | age: 28, 139 | }, 140 | }, 141 | emptyArray: [], 142 | todos: [ 143 | { id: 1, label: "buy oat milk" }, 144 | { id: 2, label: "buy cat food" }, 145 | ], 146 | count: 1, 147 | }, (getState) => ({ 148 | increment: () => ({ 149 | count: getState(s => s.count) + 1, 150 | }), 151 | decrement: () => (state) => ({ 152 | count: state.count - 1, 153 | }), 154 | replaceTodos: () => ({ 155 | todos: [ true, "false" ], 156 | }), 157 | })); 158 | }); 159 | 160 | it("creates actions from provided function and passes reference to store", () => { 161 | const spy = jest.fn(); 162 | store.watch((state) => state.count, spy, false); 163 | store.actions.increment(); 164 | expect(spy.mock.calls).toEqual([[2]]); 165 | }); 166 | 167 | it("actions that return a function will call the function and receive state", () => { 168 | const spy = jest.fn(); 169 | store.watch((state) => state.count, spy, false); 170 | store.actions.decrement(); 171 | expect(spy.mock.calls).toEqual([[0]]); 172 | }); 173 | 174 | describe("getState", () => { 175 | it("uses optional selector function or returns entire state copy", () => { 176 | let state = store.getState(); 177 | const expectedState = { 178 | friends: { 179 | "1": { 180 | name: "Alice", 181 | age: 25, 182 | }, 183 | "2": { 184 | name: "Bob", 185 | age: 28, 186 | }, 187 | }, 188 | emptyArray: [], 189 | todos: [ 190 | { id: 1, label: "buy oat milk" }, 191 | { id: 2, label: "buy cat food" }, 192 | ], 193 | count: 1, 194 | }; 195 | expect(state).toEqual(expectedState); 196 | state = null; 197 | expect(state).toEqual(null); 198 | expect(store.getState()).toEqual(expectedState); 199 | expect(store.getState(s => s.friends)).toEqual({ 200 | "1": { 201 | name: "Alice", 202 | age: 25, 203 | }, 204 | "2": { 205 | name: "Bob", 206 | age: 28, 207 | }, 208 | }); 209 | let friends = store.getState(s => s.friends); 210 | friends = null; 211 | expect(friends).toEqual(null); 212 | expect(store.getState(s => s.friends[1])).toEqual({ 213 | name: "Alice", 214 | age: 25, 215 | }); 216 | }); 217 | }); 218 | 219 | describe("dispatch", () => { 220 | it("provides copy of state if called with a function", () => { 221 | const spy = jest.fn(); 222 | store.watch((state) => state.count, spy, false); 223 | 224 | const increment = () => { 225 | store.dispatch((state) => ({ count: state.count + 1 })); 226 | }; 227 | 228 | for (let i=0; i<8; i++) increment(); 229 | store.dispatch({ count: 0 }); 230 | 231 | expect(spy.mock.calls).toEqual([ [2], [3], [4], [5], [6], [7], [8], [9], [0] ]); 232 | }); 233 | 234 | it("handles large objects of numbered props", () => { 235 | const store = new bundle.Store({ 236 | colors: [ 237 | {r:0, g:0, b:0}, 238 | ], 239 | }); 240 | const colors = { 241 | "0": { 242 | "r": 48, 243 | "g": 173, 244 | "b": 3 245 | }, 246 | "1": { 247 | "r": 216, 248 | "g": 229, 249 | "b": 123 250 | }, 251 | "2": { 252 | "r": 255, 253 | "g": 36, 254 | "b": 245 255 | }, 256 | "3": { 257 | "r": 88, 258 | "g": 89, 259 | "b": 131 260 | }, 261 | }; 262 | store.dispatch({ colors }); 263 | expect(store.stateTree).toEqual({ colors: 264 | [ 265 | { "r": 48, "g": 173, "b": 3 }, 266 | { "r": 216, "g": 229, "b": 123 }, 267 | { "r": 255, "g": 36, "b": 245 }, 268 | { "r": 88, "g": 89, "b": 131 }, 269 | ] 270 | }); 271 | }); 272 | }); 273 | 274 | describe("watch", () => { 275 | it("dispatch works with values counting down to zero and up from below zero", () => { 276 | /* 277 | * This addresses a specific bug that I found while developing `useSimpleSharedState`. 278 | */ 279 | const spy = jest.fn(); 280 | store.watch((state) => state.count, spy, false); 281 | 282 | store.dispatch({ count: 2 }); 283 | store.dispatch({ count: 1 }); 284 | store.dispatch({ count: 0 }); 285 | store.dispatch({ count: -1 }); 286 | store.dispatch({ count: -2 }); 287 | store.dispatch({ count: -1 }); 288 | store.dispatch({ count: -0 }); 289 | store.dispatch({ count: 1 }); 290 | store.dispatch({ count: 2 }); 291 | expect(spy.mock.calls).toEqual([ [2], [1], [0], [-1], [-2], [-1], [-0], [1], [2] ]); 292 | }); 293 | 294 | it("watch calls handler immediately unless false is provided as 3rd arg", () => { 295 | const spy1 = jest.fn(); 296 | const spy2 = jest.fn(); 297 | store.watch((state) => state.count, spy1, false); 298 | store.watch((state) => state.count, spy2); 299 | expect(spy1.mock.calls.length).toEqual(0); 300 | expect(spy2.mock.calls.length).toEqual(1); 301 | expect(spy2.mock.calls[0]).toEqual([1]); 302 | }); 303 | 304 | it("watch selectors work for empty arrays", () => { 305 | const spy = jest.fn(); 306 | store.watchBatch([ 307 | (state) => state.emptyArray, 308 | (state) => state.count, 309 | ], spy); 310 | expect(spy.mock.calls.length).toEqual(1); 311 | expect(spy.mock.calls[0]).toEqual([[[], 1]]); 312 | }); 313 | 314 | it("dispatch invokes listeners", () => { 315 | const spy1 = jest.fn(); 316 | const spy2 = jest.fn(); 317 | store.watch((state) => state.friends[1].name, spy1, false); 318 | store.watch((state) => state.friends[1].name, spy2, false); 319 | store.dispatch({ 320 | friends: { 321 | "1": { 322 | name: "Carrol", 323 | }, 324 | }, 325 | }); 326 | expect(spy1).toHaveBeenCalled(); 327 | expect(spy2).toHaveBeenCalled(); 328 | }); 329 | 330 | it("emits nested objects for selectors having a partial path", () => { 331 | const spy1 = jest.fn(); 332 | const spy2 = jest.fn(); 333 | const spy3 = jest.fn(); 334 | store.watch((state) => state.friends, spy1, false); 335 | store.watch((state) => state.friends[1], spy2, false); 336 | store.watch((state) => state.friends[1].name, spy3, false); 337 | store.dispatch({ 338 | friends: { 339 | "1": { 340 | name: "Carrol", 341 | }, 342 | }, 343 | }); 344 | expect(spy1.mock.calls).toEqual([[{ 345 | "1": { 346 | name: "Carrol", 347 | age: 25, 348 | }, 349 | "2": { 350 | name: "Bob", 351 | age: 28, 352 | }, 353 | }]]); 354 | expect(spy2.mock.calls).toEqual([[{ 355 | name: "Carrol", 356 | age: 25, 357 | }]]); 358 | expect(spy3.mock.calls).toEqual([[ 359 | "Carrol", 360 | ]]); 361 | }); 362 | 363 | it("unwatch removes watch listeners", () => { 364 | const spy = jest.fn(); 365 | const unwatch = store.watch((state) => state.friends, spy, false); 366 | 367 | store.dispatch({ 368 | friends: { 369 | "1": { 370 | name: "Jim", 371 | age: 31, 372 | }, 373 | }, 374 | }); 375 | 376 | expect(spy.mock.calls.length).toEqual(1); 377 | 378 | unwatch(); 379 | 380 | store.dispatch({ 381 | friends: { 382 | "2": { 383 | name: "Peter", 384 | }, 385 | }, 386 | }); 387 | 388 | // has been called once more 389 | expect(spy.mock.calls.length).toEqual(1); 390 | }); 391 | 392 | it("should throw when attempting to reuse existing selector", () => { 393 | const selector = (state) => state.friends[1]; 394 | 395 | expect(() => { 396 | store.watch(selector, () => {}, false); 397 | store.watch(selector, () => {}, false); 398 | }).toThrow(); 399 | 400 | expect(typeof store.watch((state) => state.friends[1], () => {}, false)).toEqual("function"); 401 | expect(typeof store.watch((state) => state.friends[1], () => {}, false)).toEqual("function"); 402 | }); 403 | }); 404 | 405 | describe("watchDispatch", () => { 406 | it("can watch & unwatch dispatch events", () => { 407 | const spy = jest.fn(); 408 | const unwatch = store.watchDispatch(spy); 409 | 410 | store.dispatch({ 411 | friends: { 412 | "1": { 413 | name: "Jim", 414 | }, 415 | }, 416 | }); 417 | expect(spy.mock.calls.length).toEqual(1); 418 | 419 | store.dispatch({ 420 | friends: { 421 | "1": { 422 | name: "Jessica", 423 | }, 424 | }, 425 | }); 426 | expect(spy.mock.calls.length).toEqual(2); 427 | 428 | unwatch(); 429 | 430 | store.dispatch({ 431 | friends: { 432 | "1": { 433 | name: "Willard", 434 | }, 435 | }, 436 | }); 437 | expect(spy.mock.calls.length).toEqual(2); 438 | }); 439 | }); 440 | 441 | describe("dispatch with complete arrays", () => { 442 | it("replaces old array with new ones", () => { 443 | const spy = jest.fn(); 444 | expect(store.getState(s => s.todos)).toEqual([ 445 | { id: 1, label: "buy oat milk" }, 446 | { id: 2, label: "buy cat food" }, 447 | ]); 448 | store.watch((state) => state.todos, spy, false); 449 | 450 | store.actions.replaceTodos(); 451 | expect(store.getState(s => s.todos)).toEqual([ true, "false" ]); 452 | expect(store.getState(s => s.friends)).toEqual({ 453 | "1": { 454 | name: "Alice", 455 | age: 25, 456 | }, 457 | "2": { 458 | name: "Bob", 459 | age: 28, 460 | }, 461 | }); 462 | expect(store.getState(s => s.emptyArray)).toEqual([]); 463 | 464 | expect(spy.mock.calls.length).toEqual(1); 465 | expect(spy.mock.calls[0]).toEqual([[ true, "false" ]]); 466 | }); 467 | }); 468 | 469 | describe("dispatch with `deleted`", () => { 470 | it("can remove items from arrays", () => { 471 | const spy1 = jest.fn(); 472 | store.watch((state) => state.friends, spy1, false); 473 | store.dispatch({ 474 | friends: { 475 | "1": undefined, 476 | "2": bundle.deleted, 477 | }, 478 | }); 479 | expect(spy1.mock.calls).toEqual([[{ 480 | "1": undefined, 481 | }]]); 482 | expect(store.getState().friends.hasOwnProperty("1")).toEqual(true); 483 | expect(store.getState().friends.hasOwnProperty("2")).toEqual(false); 484 | 485 | store.dispatch({ friends: bundle.deleted }); 486 | expect(store.getState().hasOwnProperty("friends")).toEqual(false); 487 | }); 488 | 489 | it("can remove items from objects", () => { 490 | const spy1 = jest.fn(); 491 | const spy2 = jest.fn(); 492 | store.watch((state) => state.friends, spy1, false); 493 | store.watch((state) => state.friends[2].age, spy2, false); 494 | store.dispatch({ 495 | friends: { 496 | "1": { 497 | name: "Susan", 498 | age: undefined, // age will remain in state with value `undefined` 499 | }, 500 | "2": { 501 | age: bundle.deleted, // age will be removed from state 502 | }, 503 | }, 504 | }); 505 | expect(spy1.mock.calls).toEqual([[{ 506 | "1": { 507 | name: "Susan", 508 | age: undefined, 509 | }, 510 | "2": { 511 | name: "Bob", 512 | }, 513 | }]]); 514 | 515 | // spy2 should have only been called once 516 | expect(spy2.mock.calls).toEqual([[undefined]]); 517 | 518 | expect(store.getState().friends[1]).toEqual({ name: "Susan" }); 519 | expect(store.getState().friends[1].hasOwnProperty("age")).toEqual(true); // proof it remains 520 | 521 | expect(store.getState().friends[2]).toEqual({ name: "Bob" }); 522 | expect(store.getState().friends[2].hasOwnProperty("age")).toEqual(false); // proof it was removed 523 | 524 | store.dispatch({ 525 | friends: { 526 | "2": { 527 | age: bundle.deleted, 528 | }, 529 | }, 530 | }); 531 | store.dispatch({ 532 | friends: { 533 | "2": bundle.deleted, 534 | }, 535 | }); 536 | 537 | // spy2 should have only been called once 538 | expect(spy2.mock.calls).toEqual([[undefined]]); 539 | 540 | store.dispatch({ 541 | friends: { 542 | "2": { 543 | name: "Jorge", 544 | age: 41, 545 | }, 546 | }, 547 | }); 548 | 549 | 550 | // spy2 should have only been called once 551 | expect(spy2.mock.calls).toEqual([[undefined], [41]]); 552 | }); 553 | 554 | it("watchers receive `undefined` when state is deleted", () => { 555 | const spy = jest.fn(); 556 | store.watch((state) => state.friends[1].name, spy, false); 557 | store.dispatch({ 558 | friends: { 559 | "1": { 560 | name: "Howard", 561 | }, 562 | }, 563 | }); 564 | expect(spy.mock.calls[0]).toEqual(["Howard"]); 565 | 566 | store.dispatch({ 567 | friends: { 568 | "1": bundle.deleted, 569 | }, 570 | }); 571 | 572 | expect(spy.mock.calls[1]).toEqual([undefined]); 573 | 574 | store.dispatch({ 575 | friends: { 576 | "1": { 577 | name: "Matt", 578 | age: 35, 579 | }, 580 | }, 581 | }); 582 | 583 | expect(spy.mock.calls).toEqual([ 584 | ["Howard"], 585 | [undefined], 586 | ["Matt"], 587 | ]); 588 | 589 | // testing that this call doesn't trigger the watcher 590 | store.dispatch({ 591 | friends: { 592 | "1": { 593 | age: 34, 594 | }, 595 | }, 596 | }); 597 | expect(spy.mock.calls).toEqual([ 598 | ["Howard"], 599 | [undefined], 600 | ["Matt"], 601 | ]); 602 | }); 603 | }); 604 | 605 | describe("watchBatch", () => { 606 | it("array.slice(0, -1) removes the last element from an array in state", () => { 607 | const spy = jest.fn(); 608 | store.watchBatch([ 609 | (state) => state.friends[1], 610 | (state) => state.count, 611 | (state) => state.todos, 612 | ], spy); 613 | 614 | expect(spy.mock.calls[0]).toEqual([[ 615 | { 616 | age: 25, 617 | name: "Alice", 618 | }, 619 | 1, 620 | [ 621 | { id: 1, label: "buy oat milk" }, 622 | { id: 2, label: "buy cat food" }, 623 | ], 624 | ]]); 625 | 626 | store.dispatch((state) => ({ todos: state.todos.slice(0, -1) })); 627 | 628 | expect(spy.mock.calls[1]).toEqual([[ 629 | { 630 | age: 25, 631 | name: "Alice" 632 | }, 633 | 1, 634 | [ 635 | { id: 1, label: "buy oat milk" }, 636 | ], 637 | ]]); 638 | 639 | expect(store.getState().todos).toEqual([ 640 | { id: 1, label: "buy oat milk" }, 641 | ]); 642 | }); 643 | 644 | it("array.slice(1) removes the first element from an array in state", () => { 645 | const spy = jest.fn(); 646 | store.watchBatch([ 647 | (state) => state.friends[1], 648 | (state) => state.count, 649 | (state) => state.todos, 650 | ], spy); 651 | 652 | expect(spy.mock.calls[0]).toEqual([[ 653 | { 654 | age: 25, 655 | name: "Alice", 656 | }, 657 | 1, 658 | [ 659 | { id: 1, label: "buy oat milk" }, 660 | { id: 2, label: "buy cat food" }, 661 | ], 662 | ]]); 663 | 664 | store.dispatch((state) => ({ todos: state.todos.slice(1) })); 665 | 666 | expect(spy.mock.calls[1]).toEqual([[ 667 | { 668 | age: 25, 669 | name: "Alice" 670 | }, 671 | 1, 672 | [ 673 | { id: 2, label: "buy cat food" }, 674 | ], 675 | ]]); 676 | 677 | expect(store.getState().todos).toEqual([ 678 | { id: 2, label: "buy cat food" }, 679 | ]); 680 | }); 681 | 682 | it("is called only once for a list of selectors", () => { 683 | const spy = jest.fn(); 684 | const removeWatcher = store.watchBatch([ 685 | (state) => state.friends[1].age, 686 | (state) => state.friends[1].name, 687 | (state) => state.friends[2].age, 688 | (state) => state.friends[2].name, 689 | ], spy); 690 | expect(spy.mock.calls.length).toEqual(1); 691 | expect(spy.mock.calls).toEqual([[[25, "Alice", 28, "Bob"]]]); 692 | store.dispatch({ 693 | friends: { 694 | "1": { 695 | name: "Will", 696 | }, 697 | "2": { 698 | age: 56, 699 | }, 700 | }, 701 | }); 702 | expect(spy.mock.calls.length).toEqual(2); 703 | expect(spy.mock.calls).toEqual([ 704 | [[25, "Alice", 28, "Bob"]], 705 | [[25, "Will", 56, "Bob"]], 706 | ]); 707 | 708 | // verify that we can remove the watcher 709 | removeWatcher(); 710 | 711 | store.dispatch({ 712 | friends: { 713 | "1": { 714 | age: 29, 715 | }, 716 | }, 717 | }); 718 | expect(spy.mock.calls.length).toEqual(2); 719 | }); 720 | 721 | it("is not called repeatedly for inapplicable selectors", () => { 722 | const spy = jest.fn(); 723 | store.watchBatch([ 724 | (s) => s.something, 725 | (s) => s.nothing.here, 726 | (s) => s.none.state.here, 727 | ], spy); 728 | 729 | store.dispatch({ 730 | friends: { 731 | "1": { 732 | name: "Susan", 733 | }, 734 | }, 735 | }); 736 | store.dispatch({ 737 | friends: { 738 | "1": { 739 | age: 26, 740 | }, 741 | }, 742 | }); 743 | 744 | expect(spy.mock.calls.length).toEqual(1); 745 | expect(spy.mock.calls).toEqual([[ 746 | [undefined, undefined, undefined], 747 | ]]); 748 | }); 749 | }); 750 | 751 | describe("erroneous selectors", () => { 752 | it("does not spam the listener with `undefined` on every dispatch event", () => { 753 | const spy = jest.fn(); 754 | store.watch((state) => state.not.valid.selector, spy); 755 | store.dispatch({ 756 | friends: { 757 | "1": { 758 | name: "Josh", 759 | }, 760 | }, 761 | }); 762 | store.dispatch({ 763 | friends: { 764 | "3": { 765 | name: "Ronald", 766 | age: 5, 767 | }, 768 | }, 769 | }); 770 | store.dispatch({ 771 | friends: { 772 | "1": { 773 | age: 41, 774 | }, 775 | "2": { 776 | age: 7, 777 | }, 778 | }, 779 | }); 780 | expect(spy.mock.calls.length).toEqual(0); 781 | }); 782 | }); 783 | }); 784 | } 785 | 786 | function testMerge(bundle, merge) { 787 | describe("merge", () => { 788 | let state; 789 | const target = { 790 | a: [ 791 | { thing: 1 }, 792 | { thing: 2 }, 793 | ], 794 | b: { 795 | asdf1: "!", 796 | asdf2: 0, 797 | bool: false, 798 | }, 799 | }; 800 | beforeEach(() => { 801 | state = { ...target }; 802 | }); 803 | 804 | it("correctly merges partial arrays", () => { 805 | const array = [1, 2, 3, 4, 5]; 806 | const changes = { 807 | "1": "change1", 808 | "3": "change2", 809 | }; 810 | 811 | const expected = [1, "change1", 3, "change2", 5]; 812 | expect(merge(array, changes)).toEqual(expected); 813 | expect(array).toEqual(expected); 814 | }); 815 | 816 | it("can update simple values in objects in arrays", () => { 817 | const change = { 818 | a: { 819 | "1": { 820 | thing: 3, 821 | }, 822 | }, 823 | }; 824 | expect(Array.isArray(change.a)).toEqual(false); 825 | expect(change.a[1].thing).toEqual(3); 826 | expect(target.a[1].thing).toEqual(2); 827 | 828 | merge(state, change); 829 | expect(state.a[1].thing).toEqual(3); 830 | expect(Array.isArray(state.a)).toEqual(true); 831 | expect(JSON.stringify(state.a)).toEqual('[{"thing":1},{"thing":3}]'); 832 | }); 833 | 834 | it("can change simple values to other data types inside nested objects", () => { 835 | merge(state, { 836 | b: { 837 | bool: "true", 838 | }, 839 | }); 840 | expect(state.b.bool).toEqual("true"); 841 | }); 842 | 843 | it("can replace simple values in arrays with new objects", () => { 844 | merge(state, { 845 | a: { 846 | "1": { 847 | thing: { 848 | new_thing: 1, 849 | }, 850 | }, 851 | }, 852 | }); 853 | expect(state.a[1].thing).toEqual({ new_thing: 1 }); 854 | }); 855 | 856 | it("can append new items to arrays", () => { 857 | merge(state, { 858 | a: { 859 | "2": { 860 | thing: "was added", 861 | }, 862 | }, 863 | }); 864 | expect(state.a[2]).toEqual({ thing: "was added" }); 865 | }); 866 | 867 | it("doesn't fail on null values", () => { 868 | merge(state, { 869 | a: { 870 | "1": null, 871 | }, 872 | }); 873 | expect(state.a[1]).toEqual(null); 874 | }); 875 | 876 | it("doesn't fail for values of 0", () => { 877 | merge(state, { 878 | a: 0, 879 | b: { 880 | asdf1: 0, 881 | asdf2: { 882 | stuff: "stuff", 883 | }, 884 | }, 885 | }); 886 | expect(state.a).toEqual(0); 887 | expect(state.b.asdf1).toEqual(0); 888 | expect(state.b.asdf2).toEqual({ stuff: "stuff" }); 889 | }); 890 | }); 891 | } 892 | 893 | describe("Source", () => { 894 | testBundle(bundles.esm); 895 | testMerge(bundles.esm, bundles.esm_merge); 896 | }); 897 | describe("ES6", () => { 898 | testBundle(bundles.es6); 899 | testMerge(bundles.es6, bundles.es6_merge); 900 | }); 901 | describe("ES5", () => { 902 | testBundle(bundles.es5); 903 | }); 904 | --------------------------------------------------------------------------------