├── .gitignore ├── src ├── index.ts ├── createGlobalState.ts └── createStore.ts ├── jest-puppeteer.config.js ├── examples ├── 04_fetch │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── PageInfo.tsx │ │ ├── ErrorMessage.tsx │ │ ├── App.tsx │ │ ├── state.ts │ │ └── RandomButton.tsx │ └── package.json ├── 08_thunk │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── state.ts │ │ └── Person.tsx │ └── package.json ├── 10_immer │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── state.ts │ │ └── Person.tsx │ └── package.json ├── 11_deep │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── state.ts │ │ └── Person.tsx │ └── package.json ├── 01_minimal │ ├── public │ │ └── index.html │ ├── package.json │ └── src │ │ └── index.js ├── 02_typescript │ ├── public │ │ └── index.html │ ├── src │ │ ├── state.ts │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ └── Person.tsx │ └── package.json ├── 03_actions │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── state.ts │ │ └── Person.tsx │ └── package.json ├── 05_onmount │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── ErrorMessage.tsx │ │ ├── PageInfo.tsx │ │ ├── state.ts │ │ ├── RandomButton.tsx │ │ └── App.tsx │ └── package.json ├── 06_reducer │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── Person.tsx │ │ └── state.ts │ └── package.json ├── 07_middleware │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── Person.tsx │ │ └── state.ts │ └── package.json ├── 09_comparison │ ├── public │ │ └── index.html │ ├── src │ │ ├── state.ts │ │ ├── index.ts │ │ ├── Counter.tsx │ │ ├── Counter2.tsx │ │ ├── App.tsx │ │ ├── state2.tsx │ │ ├── Person.tsx │ │ ├── common.ts │ │ └── Person2.tsx │ └── package.json └── 13_persistence │ ├── public │ └── index.html │ ├── src │ ├── index.ts │ ├── App.tsx │ ├── Counter.tsx │ ├── Person.tsx │ └── state.ts │ └── package.json ├── __tests__ ├── __snapshots__ │ ├── 02_useeffect_spec.tsx.snap │ └── 01_basic_spec.tsx.snap ├── e2e │ ├── __snapshots__ │ │ ├── 05_onmount.ts.snap │ │ ├── 04_fetch.ts.snap │ │ ├── 13_persistence.ts.snap │ │ ├── 01_minimal.ts.snap │ │ ├── 10_immer.ts.snap │ │ ├── 03_actions.ts.snap │ │ ├── 06_reducer.ts.snap │ │ ├── 02_typescript.ts.snap │ │ ├── 07_middleware.ts.snap │ │ ├── 08_thunk.ts.snap │ │ ├── 11_deep.ts.snap │ │ └── 09_comparison.ts.snap │ ├── 05_onmount.ts │ ├── 04_fetch.ts │ ├── 13_persistence.ts │ ├── 01_minimal.ts │ ├── 11_deep.ts │ ├── 10_immer.ts │ ├── 03_actions.ts │ ├── 06_reducer.ts │ ├── 02_typescript.ts │ ├── 07_middleware.ts │ ├── 09_comparison.ts │ └── 08_thunk.ts ├── 02_useeffect_spec.tsx ├── 04_issue33_spec.tsx ├── 03_startup_spec.tsx └── 01_basic_spec.tsx ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ └── cd.yml ├── webpack.config.js ├── LICENSE ├── .eslintrc.json ├── CHANGELOG.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | node_modules 4 | /dist 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createGlobalState } from './createGlobalState'; 2 | export { createStore } from './createStore'; 3 | -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | launch: { 3 | headless: process.env.HEADLESS !== 'false', 4 | slowMo: process.env.SLOWMO ? process.env.SLOWMO : 0, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/04_fetch/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/08_thunk/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/10_immer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/11_deep/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/01_minimal/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/02_typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/03_actions/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/05_onmount/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/06_reducer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/07_middleware/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/09_comparison/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/13_persistence/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-hooks-global-state example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/09_comparison/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'react-hooks-global-state'; 2 | 3 | import { initialState, reducer } from './common'; 4 | 5 | export const { dispatch, useStoreState } = createStore(reducer, initialState); 6 | -------------------------------------------------------------------------------- /examples/02_typescript/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState } from 'react-hooks-global-state'; 2 | 3 | export const { useGlobalState } = createGlobalState({ 4 | count: 0, 5 | person: { 6 | age: 0, 7 | firstName: '', 8 | lastName: '', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/02_useeffect_spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`useeffect spec should update a global state with useEffect 1`] = ` 4 |
5 |
6 | 7 | 9 8 | 9 |
10 |
11 | `; 12 | -------------------------------------------------------------------------------- /examples/11_deep/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/03_actions/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/04_fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/05_onmount/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/06_reducer/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/08_thunk/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/10_immer/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/02_typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/07_middleware/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/09_comparison/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/13_persistence/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | createRoot(ele).render(React.createElement(App)); 9 | -------------------------------------------------------------------------------- /examples/04_fetch/src/PageInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useGlobalState } from './state'; 4 | 5 | const PageInfo = () => { 6 | const [value] = useGlobalState('pageTitle'); 7 | return ( 8 |
9 |

PageInfo

10 | {value} 11 |
12 | ); 13 | }; 14 | 15 | export default PageInfo; 16 | -------------------------------------------------------------------------------- /examples/04_fetch/src/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useGlobalState } from './state'; 4 | 5 | const ErrorMessage = () => { 6 | const [value] = useGlobalState('errorMessage'); 7 | return ( 8 |
9 | {value} 10 |
11 | ); 12 | }; 13 | 14 | export default ErrorMessage; 15 | -------------------------------------------------------------------------------- /examples/05_onmount/src/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useGlobalState } from './state'; 4 | 5 | const ErrorMessage = () => { 6 | const [value] = useGlobalState('errorMessage'); 7 | return ( 8 |
9 | {value} 10 |
11 | ); 12 | }; 13 | 14 | export default ErrorMessage; 15 | -------------------------------------------------------------------------------- /examples/05_onmount/src/PageInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useGlobalState } from './state'; 4 | 5 | const PageInfo = () => { 6 | const [value] = useGlobalState('pageTitle'); 7 | return ( 8 |
9 |

PageInfo

10 | {value && {value}} 11 |
12 | ); 13 | }; 14 | 15 | export default PageInfo; 16 | -------------------------------------------------------------------------------- /examples/08_thunk/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/10_immer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/11_deep/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/02_typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/03_actions/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/04_fetch/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import ErrorMessage from './ErrorMessage'; 4 | import PageInfo from './PageInfo'; 5 | import RandomButton from './RandomButton'; 6 | 7 | const App = () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /examples/06_reducer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/07_middleware/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/13_persistence/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | const App = () => ( 7 | 8 |

Counter

9 | 10 | 11 |

Person

12 | 13 | 14 |
15 | ); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/05_onmount.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`05_onmount should work with events 1`] = ` 4 | " 5 |

PageInfo

sunt aut facere repellat provident occaecati excepturi optio reprehenderit
6 | 7 | 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /examples/04_fetch/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState } from 'react-hooks-global-state'; 2 | 3 | const { setGlobalState, useGlobalState } = createGlobalState({ 4 | errorMessage: '', 5 | pageTitle: '', 6 | }); 7 | 8 | export const setErrorMessage = (s: string) => { 9 | setGlobalState('errorMessage', s); 10 | }; 11 | 12 | export const setPageTitle = (s: string) => { 13 | setGlobalState('pageTitle', s); 14 | }; 15 | 16 | export { useGlobalState }; 17 | -------------------------------------------------------------------------------- /examples/05_onmount/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState } from 'react-hooks-global-state'; 2 | 3 | const { setGlobalState, useGlobalState } = createGlobalState({ 4 | errorMessage: '', 5 | pageTitle: '', 6 | }); 7 | 8 | export const setErrorMessage = (s: string) => { 9 | setGlobalState('errorMessage', s); 10 | }; 11 | 12 | export const setPageTitle = (s: string) => { 13 | setGlobalState('pageTitle', s); 14 | }; 15 | 16 | export { useGlobalState }; 17 | -------------------------------------------------------------------------------- /examples/03_actions/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { countDown, countUp, useGlobalState } from './state'; 4 | 5 | const Counter = () => { 6 | const [value] = useGlobalState('count'); 7 | return ( 8 |
9 | Count: {value} 10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Counter; 17 | -------------------------------------------------------------------------------- /examples/02_typescript/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useGlobalState } from './state'; 4 | 5 | const Counter = () => { 6 | const [value, update] = useGlobalState('count'); 7 | return ( 8 |
9 | Count: {value} 10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Counter; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es2015", 5 | "esModuleInterop": true, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noUncheckedIndexedAccess": true, 13 | "exactOptionalPropertyTypes": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "react-hooks-global-state": ["./src"] 18 | }, 19 | "outDir": "./dist" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/08_thunk/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | const Counter = () => { 9 | const value = useStoreState('count'); 10 | return ( 11 |
12 | Count: {value} 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Counter; 20 | -------------------------------------------------------------------------------- /examples/10_immer/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | const Counter = () => { 9 | const value = useStoreState('count'); 10 | return ( 11 |
12 | Count: {value} 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Counter; 20 | -------------------------------------------------------------------------------- /examples/06_reducer/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | const Counter = () => { 9 | const value = useStoreState('count'); 10 | return ( 11 |
12 | Count: {value} 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Counter; 20 | -------------------------------------------------------------------------------- /examples/07_middleware/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | const Counter = () => { 9 | const value = useStoreState('count'); 10 | return ( 11 |
12 | Count: {value} 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Counter; 20 | -------------------------------------------------------------------------------- /examples/13_persistence/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | const Counter = () => { 9 | const value = useStoreState('count'); 10 | return ( 11 |
12 | Count: {value} 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Counter; 20 | -------------------------------------------------------------------------------- /__tests__/e2e/05_onmount.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('05_onmount', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.waitForSelector('body > div > div > span'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await browser.close(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/01_minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "latest", 7 | "react-dom": "latest", 8 | "react-hooks-global-state": "latest", 9 | "react-scripts": "latest" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/11_deep/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | let numRendered = 0; 9 | 10 | const Counter = () => { 11 | const value = useStoreState('count'); 12 | numRendered += 1; 13 | return ( 14 |
15 | Count: {value} 16 | 17 | 18 | (numRendered: {numRendered}) 19 |
20 | ); 21 | }; 22 | 23 | export default Counter; 24 | -------------------------------------------------------------------------------- /examples/09_comparison/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const increment = () => dispatch({ type: 'increment' }); 6 | const decrement = () => dispatch({ type: 'decrement' }); 7 | 8 | let numRendered = 0; 9 | 10 | const Counter = () => { 11 | const value = useStoreState('count'); 12 | numRendered += 1; 13 | return ( 14 |
15 | Count: {value} 16 | 17 | 18 | (numRendered: {numRendered}) 19 |
20 | ); 21 | }; 22 | 23 | export default Counter; 24 | -------------------------------------------------------------------------------- /examples/05_onmount/src/RandomButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { setErrorMessage, setPageTitle } from './state'; 4 | 5 | const fetchPageTitle = async () => { 6 | try { 7 | const id = Math.floor(100 * Math.random()); 8 | const url = `https://jsonplaceholder.typicode.com/posts/${id}`; 9 | const response = await fetch(url); 10 | const body = await response.json(); 11 | setPageTitle(body.title); 12 | } catch (e) { 13 | setErrorMessage(`Error: ${e}`); 14 | } 15 | }; 16 | 17 | const RandomButton = () => ( 18 |
19 | 22 |
23 | ); 24 | 25 | export default RandomButton; 26 | -------------------------------------------------------------------------------- /examples/11_deep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/02_typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/03_actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/05_onmount/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/06_reducer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/09_comparison/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/10_immer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "immer": "latest", 9 | "react": "latest", 10 | "react-dom": "latest", 11 | "react-hooks-global-state": "latest", 12 | "react-scripts": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/04_fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "react-use": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/07_middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "redux": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/13_persistence/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "redux": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/09_comparison/src/Counter2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | 3 | import { useDispatch, useGlobalState } from './state2'; 4 | 5 | let numRendered = 0; 6 | 7 | const Counter = () => { 8 | const value = useGlobalState('count'); 9 | const dispatch = useDispatch(); 10 | const increment = useCallback(() => dispatch({ type: 'increment' }), [dispatch]); 11 | const decrement = useCallback(() => dispatch({ type: 'decrement' }), [dispatch]); 12 | numRendered += 1; 13 | return ( 14 |
15 | Count: {value} 16 | 17 | 18 | (numRendered: {numRendered}) 19 |
20 | ); 21 | }; 22 | 23 | export default Counter; 24 | -------------------------------------------------------------------------------- /examples/08_thunk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-hooks-global-state": "latest", 11 | "react-scripts": "latest", 12 | "redux": "latest", 13 | "redux-logger": "latest", 14 | "redux-thunk": "latest", 15 | "typescript": "latest" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: '14.x' 17 | 18 | - name: Get yarn cache 19 | id: yarn-cache 20 | run: echo "::set-output name=dir::$(yarn cache dir)" 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v1 24 | with: 25 | path: ${{ steps.yarn-cache.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - name: Install dependencies 31 | run: yarn install 32 | 33 | - name: Test 34 | run: yarn test 35 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/04_fetch.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`04_fetch should work with events 1`] = ` 4 | " 5 |

PageInfo

6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`04_fetch should work with events 2`] = ` 12 | " 13 |

PageInfo

sunt aut facere repellat provident occaecati excepturi optio reprehenderit
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`04_fetch should work with events 3`] = ` 20 | " 21 |

PageInfo

qui est esse
22 | 23 | 24 | " 25 | `; 26 | -------------------------------------------------------------------------------- /examples/03_actions/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState } from 'react-hooks-global-state'; 2 | 3 | const { setGlobalState, useGlobalState } = createGlobalState({ 4 | count: 0, 5 | person: { 6 | age: 0, 7 | firstName: '', 8 | lastName: '', 9 | }, 10 | }); 11 | 12 | export const countUp = () => { 13 | setGlobalState('count', (v) => v + 1); 14 | }; 15 | 16 | export const countDown = () => { 17 | setGlobalState('count', (v) => v - 1); 18 | }; 19 | 20 | export const setPersonFirstName = (firstName: string) => { 21 | setGlobalState('person', (v) => ({ ...v, firstName })); 22 | }; 23 | 24 | export const setPersonLastName = (lastName: string) => { 25 | setGlobalState('person', (v) => ({ ...v, lastName })); 26 | }; 27 | 28 | export const setPersonAge = (age: number) => { 29 | setGlobalState('person', (v) => ({ ...v, age })); 30 | }; 31 | 32 | export { useGlobalState }; 33 | -------------------------------------------------------------------------------- /examples/05_onmount/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, StrictMode } from 'react'; 2 | 3 | import { setPageTitle } from './state'; 4 | 5 | import ErrorMessage from './ErrorMessage'; 6 | import PageInfo from './PageInfo'; 7 | import RandomButton from './RandomButton'; 8 | 9 | const initPageInfo = async () => { 10 | const url = 'https://jsonplaceholder.typicode.com/posts/1'; 11 | const response = await fetch(url); 12 | const body = await response.json(); 13 | setPageTitle(body.title); 14 | }; 15 | 16 | const App = () => { 17 | const mounted = useRef(false); 18 | useEffect(() => { 19 | if (mounted.current) return; 20 | mounted.current = true; 21 | initPageInfo(); 22 | }); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /__tests__/e2e/04_fetch.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('04_fetch', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.waitForSelector('button'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('button'); 17 | await page.waitForSelector('button'); 18 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 19 | 20 | await page.click('button'); 21 | await page.waitForSelector('button'); 22 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 23 | 24 | await browser.close(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/01_basic_spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic spec should create a component with a global state 1`] = ` 4 |
5 |
6 | 7 | 0 8 | 9 | 14 |
15 |
16 | 17 | 0 18 | 19 | 24 |
25 |
26 | `; 27 | 28 | exports[`basic spec should create a component with a global state 2`] = ` 29 |
30 |
31 | 32 | 1 33 | 34 | 39 |
40 |
41 | 42 | 1 43 | 44 | 49 |
50 |
51 | `; 52 | -------------------------------------------------------------------------------- /__tests__/02_useeffect_spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode, useEffect } from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | 4 | import { createGlobalState } from '../src/index'; 5 | 6 | describe('useeffect spec', () => { 7 | afterEach(cleanup); 8 | 9 | it('should update a global state with useEffect', () => { 10 | const initialState = { 11 | count1: 0, 12 | }; 13 | const { useGlobalState } = createGlobalState(initialState); 14 | const Counter = () => { 15 | const [value, update] = useGlobalState('count1'); 16 | useEffect(() => { 17 | update(9); 18 | }, [update]); 19 | return ( 20 |
21 | {value} 22 |
23 | ); 24 | }; 25 | const App = () => ( 26 | 27 | 28 | 29 | ); 30 | const { container } = render(); 31 | expect(container).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/03_actions/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | setPersonAge, 5 | setPersonFirstName, 6 | setPersonLastName, 7 | useGlobalState, 8 | } from './state'; 9 | 10 | const Person = () => { 11 | const [{ firstName, lastName, age }] = useGlobalState('person'); 12 | return ( 13 |
14 |
15 | First Name: 16 | setPersonFirstName(event.target.value)} 19 | /> 20 |
21 |
22 | Last Name: 23 | setPersonLastName(event.target.value)} 26 | /> 27 |
28 |
29 | Age: 30 | setPersonAge(Number(event.target.value) || 0)} 33 | /> 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default Person; 40 | -------------------------------------------------------------------------------- /examples/09_comparison/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | import Person from './Person'; 5 | 6 | import Counter2 from './Counter2'; 7 | import Person2 from './Person2'; 8 | import { Provider } from './state2'; 9 | 10 | const App = () => ( 11 | 12 |
13 |
14 |

useReducer + useContext

15 | 16 |

Counter

17 | 18 | 19 |

Person

20 | 21 | 22 |
23 |
24 |
25 |

react-hooks-global-state

26 |

Counter

27 | 28 | 29 |

Person

30 | 31 | 32 |
33 |
34 |
35 | ); 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /examples/10_immer/src/state.ts: -------------------------------------------------------------------------------- 1 | import { produce } from 'immer'; 2 | 3 | import { createStore } from 'react-hooks-global-state'; 4 | 5 | type Action = 6 | | { type: 'increment' } 7 | | { type: 'decrement' } 8 | | { type: 'setFirstName'; firstName: string } 9 | | { type: 'setLastName'; lastName: string } 10 | | { type: 'setAge'; age: number }; 11 | 12 | export const { dispatch, useStoreState } = createStore( 13 | (state, action: Action) => produce(state, (draft) => { 14 | switch (action.type) { 15 | case 'increment': draft.count += 1; break; 16 | case 'decrement': draft.count -= 1; break; 17 | case 'setFirstName': draft.person.firstName = action.firstName; break; 18 | case 'setLastName': draft.person.lastName = action.lastName; break; 19 | case 'setAge': draft.person.age = action.age; break; 20 | } 21 | }), 22 | { 23 | count: 0, 24 | person: { 25 | age: 0, 26 | firstName: '', 27 | lastName: '', 28 | }, 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /examples/04_fetch/src/RandomButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { setErrorMessage, setPageTitle } from './state'; 4 | 5 | let id = 0; 6 | 7 | const fetchPageTitle = async (setLoading: (x: boolean) => void) => { 8 | setLoading(true); 9 | try { 10 | if (id < 3) { 11 | id += 1; // fixing for e2e test 12 | } else { 13 | id = Math.floor(100 * Math.random()); 14 | } 15 | const url = `https://jsonplaceholder.typicode.com/posts/${id}`; 16 | const response = await fetch(url); 17 | const body = await response.json(); 18 | setPageTitle(body.title); 19 | } catch (e) { 20 | setErrorMessage(`Error: ${e}`); 21 | } 22 | setLoading(false); 23 | }; 24 | 25 | const RandomButton = () => { 26 | const [loading, setLoading] = useState(false); 27 | return ( 28 |
29 | {loading ? 'Loading...' : ( 30 | 33 | )} 34 |
35 | ); 36 | }; 37 | 38 | export default RandomButton; 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const { DIR, EXT = 'ts' } = process.env; 5 | 6 | module.exports = { 7 | mode: 'development', 8 | devtool: 'cheap-module-source-map', 9 | entry: `./examples/${DIR}/src/index.${EXT}`, 10 | output: { 11 | publicPath: '/', 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: `./examples/${DIR}/public/index.html`, 16 | }), 17 | ], 18 | module: { 19 | rules: [{ 20 | test: /\.[jt]sx?$/, 21 | exclude: /node_modules/, 22 | loader: 'ts-loader', 23 | options: { 24 | transpileOnly: true, 25 | }, 26 | }], 27 | }, 28 | resolve: { 29 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 30 | alias: { 31 | 'react-hooks-global-state': `${__dirname}/src`, 32 | }, 33 | }, 34 | devServer: { 35 | port: process.env.PORT || '8080', 36 | static: { 37 | directory: `./examples/${DIR}/public`, 38 | }, 39 | historyApiFallback: true, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /examples/09_comparison/src/state2.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode, 3 | createContext, 4 | useContext, 5 | useReducer, 6 | } from 'react'; 7 | 8 | import { 9 | Action, 10 | initialState, 11 | reducer, 12 | State, 13 | } from './common'; 14 | 15 | const stateCtx = createContext(initialState); 16 | const dispatchCtx = createContext((() => 0) as React.Dispatch); 17 | 18 | export const Provider = ({ children }: { children: ReactNode }) => { 19 | const [state, dispatch] = useReducer(reducer, initialState); 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | }; 28 | 29 | export const useDispatch = () => { 30 | const dispatch = useContext(dispatchCtx); 31 | return dispatch; 32 | }; 33 | 34 | // eslint-disable-next-line arrow-parens 35 | export const useGlobalState = (property: K) => { 36 | const state = useContext(stateCtx); 37 | return state[property]; // only one depth selector for comparison 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '14.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Get yarn cache 21 | id: yarn-cache 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ${{ steps.yarn-cache.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | 32 | - name: Install dependencies 33 | run: yarn install 34 | 35 | - name: Test 36 | run: yarn test 37 | 38 | - name: Compile 39 | run: yarn run compile 40 | 41 | - name: Publish 42 | run: npm publish 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /examples/06_reducer/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | 10 | const setLastName = (event: React.FormEvent) => dispatch({ 11 | lastName: event.currentTarget.value, 12 | type: 'setLastName', 13 | }); 14 | 15 | const setAge = (event: React.FormEvent) => dispatch({ 16 | age: Number(event.currentTarget.value) || 0, 17 | type: 'setAge', 18 | }); 19 | 20 | const Person = () => { 21 | const value = useStoreState('person'); 22 | return ( 23 |
24 |
25 | First Name: 26 | 27 |
28 |
29 | Last Name: 30 | 31 |
32 |
33 | Age: 34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Person; 41 | -------------------------------------------------------------------------------- /examples/10_immer/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | 10 | const setLastName = (event: React.FormEvent) => dispatch({ 11 | lastName: event.currentTarget.value, 12 | type: 'setLastName', 13 | }); 14 | 15 | const setAge = (event: React.FormEvent) => dispatch({ 16 | age: Number(event.currentTarget.value) || 0, 17 | type: 'setAge', 18 | }); 19 | 20 | const Person = () => { 21 | const value = useStoreState('person'); 22 | return ( 23 |
24 |
25 | First Name: 26 | 27 |
28 |
29 | Last Name: 30 | 31 |
32 |
33 | Age: 34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Person; 41 | -------------------------------------------------------------------------------- /examples/07_middleware/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | 10 | const setLastName = (event: React.FormEvent) => dispatch({ 11 | lastName: event.currentTarget.value, 12 | type: 'setLastName', 13 | }); 14 | 15 | const setAge = (event: React.FormEvent) => dispatch({ 16 | age: Number(event.currentTarget.value) || 0, 17 | type: 'setAge', 18 | }); 19 | 20 | const Person = () => { 21 | const value = useStoreState('person'); 22 | return ( 23 |
24 |
25 | First Name: 26 | 27 |
28 |
29 | Last Name: 30 | 31 |
32 |
33 | Age: 34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Person; 41 | -------------------------------------------------------------------------------- /examples/13_persistence/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | 10 | const setLastName = (event: React.FormEvent) => dispatch({ 11 | lastName: event.currentTarget.value, 12 | type: 'setLastName', 13 | }); 14 | 15 | const setAge = (event: React.FormEvent) => dispatch({ 16 | age: Number(event.currentTarget.value) || 0, 17 | type: 'setAge', 18 | }); 19 | 20 | const Person = () => { 21 | const value = useStoreState('person'); 22 | return ( 23 |
24 |
25 | First Name: 26 | 27 |
28 |
29 | Last Name: 30 | 31 |
32 |
33 | Age: 34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Person; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2022 Daishi Kato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/02_typescript/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useGlobalState } from './state'; 4 | 5 | const Person = () => { 6 | const [value, update] = useGlobalState('person'); 7 | return ( 8 |
9 |
10 | First Name: 11 | { 14 | const firstName = event.target.value; 15 | update((p) => ({ ...p, firstName })); 16 | }} 17 | /> 18 |
19 |
20 | Last Name: 21 | { 24 | const lastName = event.target.value; 25 | update((p) => ({ ...p, lastName })); 26 | }} 27 | /> 28 |
29 |
30 | Age: 31 | { 34 | const age = Number(event.target.value) || 0; 35 | update((p) => ({ ...p, age })); 36 | }} 37 | /> 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Person; 44 | -------------------------------------------------------------------------------- /examples/01_minimal/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { createGlobalState } from 'react-hooks-global-state'; 5 | 6 | const initialState = { 7 | count: 0, 8 | text: 'hello', 9 | }; 10 | const { useGlobalState } = createGlobalState(initialState); 11 | 12 | const Counter = () => { 13 | const [value, update] = useGlobalState('count'); 14 | return ( 15 |
16 | Count: {value} 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | const TextBox = () => { 24 | const [value, update] = useGlobalState('text'); 25 | return ( 26 |
27 | Text: {value} 28 | update(event.target.value)} /> 29 |
30 | ); 31 | }; 32 | 33 | const App = () => ( 34 | 35 |

Counter

36 | 37 | 38 |

TextBox

39 | 40 | 41 |
42 | ); 43 | 44 | createRoot(document.getElementById('app')).render(); 45 | -------------------------------------------------------------------------------- /__tests__/e2e/13_persistence.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('13_persistence', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.reload(); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await browser.close(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/09_comparison/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | const setLastName = (event: React.FormEvent) => dispatch({ 10 | lastName: event.currentTarget.value, 11 | type: 'setLastName', 12 | }); 13 | const setAge = (event: React.FormEvent) => dispatch({ 14 | age: Number(event.currentTarget.value) || 0, 15 | type: 'setAge', 16 | }); 17 | 18 | let numRendered = 0; 19 | 20 | const Person = () => { 21 | const value = useStoreState('person'); 22 | numRendered += 1; 23 | return ( 24 |
25 |
26 | First Name: 27 | 28 |
29 |
30 | Last Name: 31 | 32 |
33 |
34 | Age: 35 | 36 |
37 |
(numRendered: {numRendered})
38 |
39 | ); 40 | }; 41 | 42 | export default Person; 43 | -------------------------------------------------------------------------------- /examples/09_comparison/src/common.ts: -------------------------------------------------------------------------------- 1 | export const initialState = { 2 | count: 0, 3 | person: { 4 | age: 0, 5 | firstName: '', 6 | lastName: '', 7 | }, 8 | }; 9 | 10 | export type State = typeof initialState; 11 | 12 | export type Action = 13 | | { type: 'increment' } 14 | | { type: 'decrement' } 15 | | { type: 'setFirstName'; firstName: string } 16 | | { type: 'setLastName'; lastName: string } 17 | | { type: 'setAge'; age: number }; 18 | 19 | export const reducer = (state: State, action: Action) => { 20 | switch (action.type) { 21 | case 'increment': return { 22 | ...state, 23 | count: state.count + 1, 24 | }; 25 | case 'decrement': return { 26 | ...state, 27 | count: state.count - 1, 28 | }; 29 | case 'setFirstName': return { 30 | ...state, 31 | person: { 32 | ...state.person, 33 | firstName: action.firstName, 34 | }, 35 | }; 36 | case 'setLastName': return { 37 | ...state, 38 | person: { 39 | ...state.person, 40 | lastName: action.lastName, 41 | }, 42 | }; 43 | case 'setAge': return { 44 | ...state, 45 | person: { 46 | ...state.person, 47 | age: action.age, 48 | }, 49 | }; 50 | default: return state; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /examples/11_deep/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'react-hooks-global-state'; 2 | 3 | type Action = 4 | | { type: 'increment' } 5 | | { type: 'decrement' } 6 | | { type: 'setFirstName'; firstName: string } 7 | | { type: 'setLastName'; lastName: string } 8 | | { type: 'setAge'; age: number }; 9 | 10 | export const { dispatch, useStoreState } = createStore( 11 | (state, action: Action) => { 12 | switch (action.type) { 13 | case 'increment': return { 14 | ...state, 15 | count: state.count + 1, 16 | }; 17 | case 'decrement': return { 18 | ...state, 19 | count: state.count - 1, 20 | }; 21 | case 'setFirstName': return { 22 | ...state, 23 | person: { 24 | ...state.person, 25 | firstName: action.firstName, 26 | }, 27 | }; 28 | case 'setLastName': return { 29 | ...state, 30 | person: { 31 | ...state.person, 32 | lastName: action.lastName, 33 | }, 34 | }; 35 | case 'setAge': return { 36 | ...state, 37 | person: { 38 | ...state.person, 39 | age: action.age, 40 | }, 41 | }; 42 | default: return state; 43 | } 44 | }, 45 | { 46 | count: 0, 47 | person: { 48 | age: 0, 49 | firstName: '', 50 | lastName: '', 51 | }, 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /examples/06_reducer/src/state.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'react-hooks-global-state'; 2 | 3 | type Action = 4 | | { type: 'increment' } 5 | | { type: 'decrement' } 6 | | { type: 'setFirstName'; firstName: string } 7 | | { type: 'setLastName'; lastName: string } 8 | | { type: 'setAge'; age: number }; 9 | 10 | export const { dispatch, useStoreState } = createStore( 11 | (state, action: Action) => { 12 | switch (action.type) { 13 | case 'increment': return { 14 | ...state, 15 | count: state.count + 1, 16 | }; 17 | case 'decrement': return { 18 | ...state, 19 | count: state.count - 1, 20 | }; 21 | case 'setFirstName': return { 22 | ...state, 23 | person: { 24 | ...state.person, 25 | firstName: action.firstName, 26 | }, 27 | }; 28 | case 'setLastName': return { 29 | ...state, 30 | person: { 31 | ...state.person, 32 | lastName: action.lastName, 33 | }, 34 | }; 35 | case 'setAge': return { 36 | ...state, 37 | person: { 38 | ...state.person, 39 | age: action.age, 40 | }, 41 | }; 42 | default: return state; 43 | } 44 | }, 45 | { 46 | count: 0, 47 | person: { 48 | age: 0, 49 | firstName: '', 50 | lastName: '', 51 | }, 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /examples/09_comparison/src/Person2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | 3 | import { useDispatch, useGlobalState } from './state2'; 4 | 5 | let numRendered = 0; 6 | 7 | const Person = () => { 8 | const value = useGlobalState('person'); 9 | const dispatch = useDispatch(); 10 | const setFirstName = useCallback( 11 | (event: React.FormEvent) => dispatch({ 12 | firstName: event.currentTarget.value, 13 | type: 'setFirstName', 14 | }), 15 | [dispatch], 16 | ); 17 | const setLastName = useCallback( 18 | (event: React.FormEvent) => dispatch({ 19 | lastName: event.currentTarget.value, 20 | type: 'setLastName', 21 | }), 22 | [dispatch], 23 | ); 24 | const setAge = useCallback( 25 | (event: React.FormEvent) => dispatch({ 26 | age: Number(event.currentTarget.value) || 0, 27 | type: 'setAge', 28 | }), 29 | [dispatch], 30 | ); 31 | numRendered += 1; 32 | return ( 33 |
34 |
35 | First Name: 36 | 37 |
38 |
39 | Last Name: 40 | 41 |
42 |
43 | Age: 44 | 45 |
46 |
(numRendered: {numRendered})
47 |
48 | ); 49 | }; 50 | 51 | export default Person; 52 | -------------------------------------------------------------------------------- /__tests__/04_issue33_spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, cleanup } from '@testing-library/react'; 3 | 4 | import { createGlobalState } from '../src/index'; 5 | 6 | describe('issue #33 spec', () => { 7 | afterEach(cleanup); 8 | 9 | it('should sync getGlobaState with useGlobalState', () => { 10 | const initialState = { 11 | count1: 0, 12 | }; 13 | const { useGlobalState, getGlobalState } = createGlobalState(initialState); 14 | const Positive = ({ count }: { count: number }) => { 15 | if (count !== getGlobalState('count1')) throw Error('count mismatch'); 16 | return
{count} is positive
; 17 | }; 18 | const Counter = () => { 19 | const [value, update] = useGlobalState('count1'); 20 | return ( 21 |
22 | {value} 23 | 24 | {value > 0 && } 25 |
26 | ); 27 | }; 28 | const App = () => ( 29 |
30 | 31 |
32 | ); 33 | const { getAllByText, container } = render(); 34 | expect(container.querySelector('span')?.textContent).toBe('0'); 35 | expect(getGlobalState('count1')).toBe(0); 36 | fireEvent.click(getAllByText('+1')[0] as HTMLElement); 37 | expect(container.querySelector('span')?.textContent).toBe('1'); 38 | expect(getGlobalState('count1')).toBe(1); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/08_thunk/src/state.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, combineReducers, compose } from 'redux'; 2 | import reduxLogger from 'redux-logger'; 3 | import reduxThunk from 'redux-thunk'; 4 | 5 | import { createStore } from 'react-hooks-global-state'; 6 | 7 | const initialState = { 8 | count: 0, 9 | person: { 10 | age: 0, 11 | firstName: '', 12 | lastName: '', 13 | }, 14 | }; 15 | 16 | export type Action = 17 | | { type: 'increment' } 18 | | { type: 'decrement' } 19 | | { type: 'setFirstName'; firstName: string } 20 | | { type: 'setLastName'; lastName: string } 21 | | { type: 'setAge'; age: number }; 22 | 23 | const countReducer = (state = initialState.count, action: Action) => { 24 | switch (action.type) { 25 | case 'increment': return state + 1; 26 | case 'decrement': return state - 1; 27 | default: return state; 28 | } 29 | }; 30 | 31 | const personReducer = (state = initialState.person, action: Action) => { 32 | switch (action.type) { 33 | case 'setFirstName': return { 34 | ...state, 35 | firstName: action.firstName, 36 | }; 37 | case 'setLastName': return { 38 | ...state, 39 | lastName: action.lastName, 40 | }; 41 | case 'setAge': return { 42 | ...state, 43 | age: action.age, 44 | }; 45 | default: return state; 46 | } 47 | }; 48 | 49 | const reducer = combineReducers({ 50 | count: countReducer, 51 | person: personReducer, 52 | }); 53 | 54 | export const { dispatch, useStoreState } = createStore< 55 | typeof initialState, 56 | Action 57 | >( 58 | reducer, 59 | initialState, 60 | compose( 61 | applyMiddleware(reduxThunk, reduxLogger), 62 | ), 63 | ); 64 | -------------------------------------------------------------------------------- /__tests__/e2e/01_minimal.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('01_minimal', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(4) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await browser.close(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/07_middleware/src/state.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'react'; 2 | 3 | import { applyMiddleware, combineReducers } from 'redux'; 4 | 5 | import { createStore } from 'react-hooks-global-state'; 6 | 7 | const initialState = { 8 | count: 0, 9 | person: { 10 | age: 0, 11 | firstName: '', 12 | lastName: '', 13 | }, 14 | }; 15 | 16 | type State = typeof initialState; 17 | 18 | type Action = 19 | | { type: 'increment' } 20 | | { type: 'decrement' } 21 | | { type: 'setFirstName'; firstName: string } 22 | | { type: 'setLastName'; lastName: string } 23 | | { type: 'setAge'; age: number }; 24 | 25 | const countReducer = (state = initialState.count, action: Action) => { 26 | switch (action.type) { 27 | case 'increment': return state + 1; 28 | case 'decrement': return state - 1; 29 | default: return state; 30 | } 31 | }; 32 | 33 | const personReducer = (state = initialState.person, action: Action) => { 34 | switch (action.type) { 35 | case 'setFirstName': return { 36 | ...state, 37 | firstName: action.firstName, 38 | }; 39 | case 'setLastName': return { 40 | ...state, 41 | lastName: action.lastName, 42 | }; 43 | case 'setAge': return { 44 | ...state, 45 | age: action.age, 46 | }; 47 | default: return state; 48 | } 49 | }; 50 | 51 | const reducer = combineReducers({ 52 | count: countReducer, 53 | person: personReducer, 54 | }); 55 | 56 | const logger = ( 57 | { getState }: { getState: () => State }, 58 | ) => (next: Dispatch) => (action: Action) => { 59 | /* eslint-disable no-console */ 60 | console.log('will dispatch', action); 61 | const returnValue = next(action); 62 | console.log('state after dispatch', getState()); 63 | /* eslint-enable no-console */ 64 | return returnValue; 65 | }; 66 | 67 | export const { dispatch, useStoreState } = createStore( 68 | reducer, 69 | initialState, 70 | applyMiddleware(logger), 71 | ); 72 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint", 5 | "react-hooks" 6 | ], 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "airbnb" 10 | ], 11 | "env": { 12 | "browser": true 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "node": { 17 | "extensions": [".js", ".ts", ".tsx"] 18 | } 19 | } 20 | }, 21 | "rules": { 22 | "react-hooks/rules-of-hooks": "error", 23 | "react-hooks/exhaustive-deps": "error", 24 | "@typescript-eslint/explicit-function-return-type": "off", 25 | "@typescript-eslint/explicit-module-boundary-types": "off", 26 | "@typescript-eslint/ban-types": "off", 27 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }], 28 | "react/prop-types": "off", 29 | "react/jsx-one-expression-per-line": "off", 30 | "import/extensions": ["error", "never"], 31 | "import/prefer-default-export": "off", 32 | "import/no-unresolved": ["error", { "ignore": ["react-hooks-global-state"] }], 33 | "no-underscore-dangle": ["error", { "allow": ["__REDUX_DEVTOOLS_EXTENSION__"] }], 34 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 35 | "no-plusplus": "off", 36 | "default-case": "off", 37 | "no-param-reassign": "off", 38 | "symbol-description": "off", 39 | "no-use-before-define": "off", 40 | "no-unused-vars": "off", 41 | "react/function-component-definition": ["error", { "namedComponents": "arrow-function" }], 42 | "default-param-last": "off" 43 | }, 44 | "overrides": [{ 45 | "files": ["__tests__/**/*"], 46 | "env": { 47 | "jest": true 48 | }, 49 | "rules": { 50 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 51 | } 52 | }, { 53 | "files": ["examples/**/*"], 54 | "rules": { 55 | "import/no-extraneous-dependencies": "off" 56 | } 57 | }] 58 | } 59 | -------------------------------------------------------------------------------- /__tests__/e2e/11_deep.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('11_deep', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(4) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(5) > div > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(6) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(7) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(8) > div> input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await browser.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/e2e/10_immer.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('10_immer', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > div:nth-of-type(2) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(3) > div:nth-of-type(3) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(4) > div:nth-of-type(2) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(4) > div:nth-of-type(3) > input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await browser.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/e2e/03_actions.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('03_actions', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > div:nth-of-type(2) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(3) > div:nth-of-type(3) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(4) > div:nth-of-type(2) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(4) > div:nth-of-type(3) > input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await browser.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/e2e/06_reducer.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('06_reducer', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > div:nth-of-type(2) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(3) > div:nth-of-type(3) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(4) > div:nth-of-type(2) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(4) > div:nth-of-type(3) > input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await browser.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/e2e/02_typescript.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('02_typescript', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > div:nth-of-type(2) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(3) > div:nth-of-type(3) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(4) > div:nth-of-type(2) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(4) > div:nth-of-type(3) > input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await browser.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/e2e/07_middleware.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('07_middleware', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > div:nth-of-type(2) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(3) > div:nth-of-type(3) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(4) > div:nth-of-type(2) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(4) > div:nth-of-type(3) > input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await browser.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/08_thunk/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch } from 'react'; 2 | 3 | import { Action, dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | 10 | const setLastName = (event: React.FormEvent) => dispatch({ 11 | lastName: event.currentTarget.value, 12 | type: 'setLastName', 13 | }); 14 | 15 | const setAge = (event: React.FormEvent) => dispatch({ 16 | age: Number(event.currentTarget.value) || 0, 17 | type: 'setAge', 18 | }); 19 | 20 | let id = 0; 21 | 22 | const setRandomFirstName = () => { 23 | const dispatchForThunk = dispatch as Dispatch) => void)>; 24 | dispatchForThunk(async (d: Dispatch) => { 25 | d({ 26 | firstName: 'Loading...', 27 | type: 'setFirstName', 28 | }); 29 | try { 30 | if (id < 3) { 31 | id += 1; // fixing for e2e test 32 | } else { 33 | id = Math.floor(100 * Math.random()); 34 | } 35 | const url = `https://jsonplaceholder.typicode.com/posts/${id}`; 36 | const response = await fetch(url); 37 | const body = await response.json(); 38 | d({ 39 | firstName: body.title.split(' ')[0], 40 | type: 'setFirstName', 41 | }); 42 | } catch (e) { 43 | d({ 44 | firstName: 'ERROR: fetching', 45 | type: 'setFirstName', 46 | }); 47 | } 48 | }); 49 | }; 50 | 51 | const Person = () => { 52 | const value = useStoreState('person'); 53 | return ( 54 |
55 | 56 |
57 | First Name: 58 | {value.firstName === 'Loading...' ? ( 59 | Loading... 60 | ) : ( 61 | 62 | )} 63 |
64 |
65 | Last Name: 66 | 67 |
68 |
69 | Age: 70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Person; 77 | -------------------------------------------------------------------------------- /__tests__/e2e/09_comparison.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | let base = ''; 6 | let page: puppeteer.Page; 7 | 8 | const run = async () => { 9 | await page.click(`${base}div:nth-of-type(1) > button:nth-of-type(1)`); 10 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 11 | 12 | await page.click(`${base}div:nth-of-type(1) > button:nth-of-type(2)`); 13 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 14 | 15 | await page.click(`${base}div:nth-of-type(2) > button:nth-of-type(1)`); 16 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 17 | 18 | await page.click(`${base}div:nth-of-type(2) > button:nth-of-type(2)`); 19 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 20 | 21 | await page.type(`${base}div:nth-of-type(3) > div:nth-of-type(1) > input`, '1'); 22 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 23 | 24 | await page.type(`${base}div:nth-of-type(3) > div:nth-of-type(2) > input`, '2'); 25 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 26 | 27 | await page.type(`${base}div:nth-of-type(3) > div:nth-of-type(3) > input`, '3'); 28 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 29 | 30 | await page.type(`${base}div:nth-of-type(4) > div:nth-of-type(1) > input`, '4'); 31 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 32 | 33 | await page.type(`${base}div:nth-of-type(4) > div:nth-of-type(2) > input`, '5'); 34 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 35 | 36 | await page.type(`${base}div:nth-of-type(4) > div:nth-of-type(3) > input`, '6'); 37 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 38 | }; 39 | 40 | describe('09_comparison', () => { 41 | const port = process.env.PORT || '8080'; 42 | 43 | it('should work with events', async () => { 44 | const browser = await puppeteer.launch(); 45 | page = await browser.newPage(); 46 | await page.goto(`http://localhost:${port}/`); 47 | 48 | base = 'body > div > div > div:nth-of-type(1) > '; 49 | await run(); 50 | 51 | base = 'body > div > div > div:nth-of-type(2) > '; 52 | await run(); 53 | 54 | await browser.close(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /examples/11_deep/src/Person.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | import { dispatch, useStoreState } from './state'; 4 | 5 | const setFirstName = (event: React.FormEvent) => dispatch({ 6 | firstName: event.currentTarget.value, 7 | type: 'setFirstName', 8 | }); 9 | 10 | const setLastName = (event: React.FormEvent) => dispatch({ 11 | lastName: event.currentTarget.value, 12 | type: 'setLastName', 13 | }); 14 | 15 | const setAge = (event: React.FormEvent) => dispatch({ 16 | age: Number(event.currentTarget.value) || 0, 17 | type: 'setAge', 18 | }); 19 | 20 | const TextBox = ({ text }: { text: string }) => { 21 | // eslint-disable-next-line no-console 22 | console.log('rendering text:', text); 23 | return {text}; 24 | }; 25 | 26 | let numRendered = 0; 27 | const PersonFirstName = () => { 28 | const value = useStoreState('person'); 29 | const { firstName } = value; 30 | return useMemo( 31 | () => ( 32 |
33 | First Name: 34 | 35 | 36 | (numRendered: {++numRendered}) 37 |
38 | ), 39 | [firstName], 40 | ); 41 | }; 42 | 43 | const PersonLastName = () => { 44 | const value = useStoreState('person'); 45 | const { lastName } = value; 46 | return useMemo( 47 | () => ( 48 |
49 | Last Name: 50 | 51 | 52 | (numRendered: {++numRendered}) 53 |
54 | ), 55 | [lastName], 56 | ); 57 | }; 58 | 59 | const PersonAge = () => { 60 | const value = useStoreState('person'); 61 | const { age } = value; 62 | const ageDoubled = useMemo(() => age * 2, [age]); 63 | return useMemo( 64 | () => ( 65 |
66 |
67 | Age: 68 | 69 | (numRendered: {++numRendered}) 70 |
71 |
72 | Age Doubled: 73 | {ageDoubled} 74 |
75 |
76 | ), 77 | [age, ageDoubled], // only `age` is actually fine. 78 | ); 79 | }; 80 | 81 | const Person = () => ( 82 | <> 83 | 84 | 85 | 86 | 87 | ); 88 | 89 | export default Person; 90 | -------------------------------------------------------------------------------- /__tests__/03_startup_spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | 4 | import { createGlobalState, createStore } from '../src/index'; 5 | 6 | describe('startup spec', () => { 7 | afterEach(cleanup); 8 | 9 | it('should setSetGlobalState at start up', () => { 10 | const initialState = { 11 | count1: 0, 12 | count2: 0, 13 | }; 14 | const { setGlobalState, useGlobalState } = createGlobalState(initialState); 15 | const Counter = ({ name }: { name: 'count1' | 'count2' }) => { 16 | setGlobalState(name, 9); 17 | const [value] = useGlobalState(name); 18 | return ( 19 |
20 |
{name}
21 |
{value}
22 |
23 | ); 24 | }; 25 | const App = () => ( 26 | 27 | 28 | 29 | 30 | ); 31 | const { getByTestId } = render(); 32 | expect(getByTestId('count1').innerHTML).toBe('9'); 33 | expect(getByTestId('count2').innerHTML).toBe('9'); 34 | }); 35 | 36 | it('should dispatch at start up', () => { 37 | const initialState = { 38 | count1: 0, 39 | count2: 0, 40 | }; 41 | type State = typeof initialState; 42 | type Action = { 43 | type: 'setCounter'; 44 | name: 'count1' | 'count2'; 45 | value: number; 46 | }; 47 | const reducer = (state: State, action: Action) => { 48 | if (action.type === 'setCounter') { 49 | return { 50 | ...state, 51 | [action.name]: action.value, 52 | }; 53 | } 54 | return state; 55 | }; 56 | const { dispatch, useStoreState } = createStore(reducer, initialState); 57 | const Counter = ({ name }: { name: 'count1' | 'count2' }) => { 58 | dispatch({ type: 'setCounter', name, value: 9 }); 59 | const value = useStoreState(name); 60 | return ( 61 |
62 |
{name}
63 |
{value}
64 |
65 | ); 66 | }; 67 | const App = () => ( 68 | 69 | 70 | 71 | 72 | ); 73 | const { getByTestId } = render(); 74 | expect(getByTestId('count1').innerHTML).toBe('9'); 75 | expect(getByTestId('count2').innerHTML).toBe('9'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /__tests__/e2e/08_thunk.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | jest.setTimeout(15 * 1000); 4 | 5 | describe('08_thunk', () => { 6 | const port = process.env.PORT || '8080'; 7 | 8 | it('should work with events', async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.goto(`http://localhost:${port}/`); 12 | 13 | await page.click('div:nth-of-type(1) > button:nth-of-type(1)'); 14 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 15 | 16 | await page.click('div:nth-of-type(1) > button:nth-of-type(2)'); 17 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 18 | 19 | await page.click('div:nth-of-type(2) > button:nth-of-type(1)'); 20 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 21 | 22 | await page.click('div:nth-of-type(2) > button:nth-of-type(2)'); 23 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 24 | 25 | await page.type('div:nth-of-type(3) > div:nth-of-type(1) > input', '1'); 26 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 27 | 28 | await page.type('div:nth-of-type(3) > div:nth-of-type(2) > input', '2'); 29 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 30 | 31 | await page.type('div:nth-of-type(3) > div:nth-of-type(3) > input', '3'); 32 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 33 | 34 | await page.type('div:nth-of-type(4) > div:nth-of-type(1) > input', '4'); 35 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 36 | 37 | await page.type('div:nth-of-type(4) > div:nth-of-type(2) > input', '5'); 38 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 39 | 40 | await page.type('div:nth-of-type(4) > div:nth-of-type(3) > input', '6'); 41 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 42 | 43 | await page.click('div:nth-of-type(3) > button'); 44 | await page.waitForSelector('div:nth-of-type(3) > div:nth-of-type(1) > span'); 45 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 46 | 47 | await page.waitForSelector('div:nth-of-type(3) > div:nth-of-type(1) > input'); 48 | expect(await page.evaluate(() => document.body.innerHTML)).toMatchSnapshot(); 49 | 50 | await browser.close(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/13_persistence/src/state.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'react'; 2 | import { applyMiddleware } from 'redux'; 3 | 4 | import { createStore } from 'react-hooks-global-state'; 5 | 6 | type State = { 7 | count: number; 8 | person: { 9 | age: number; 10 | firstName: string; 11 | lastName: string; 12 | }; 13 | }; 14 | 15 | type Action = 16 | | { type: 'increment' } 17 | | { type: 'decrement' } 18 | | { type: 'setFirstName'; firstName: string } 19 | | { type: 'setLastName'; lastName: string } 20 | | { type: 'setAge'; age: number }; 21 | 22 | const defaultState: State = { 23 | count: 0, 24 | person: { 25 | age: 0, 26 | firstName: '', 27 | lastName: '', 28 | }, 29 | }; 30 | 31 | const LOCAL_STORAGE_KEY = 'my_local_storage_key'; 32 | const parseState = (str: string | null): State | null => { 33 | try { 34 | const state = JSON.parse(str || ''); 35 | if (typeof state.count !== 'number') throw new Error(); 36 | if (typeof state.person.age !== 'number') throw new Error(); 37 | if (typeof state.person.firstName !== 'string') throw new Error(); 38 | if (typeof state.person.lastName !== 'string') throw new Error(); 39 | return state as State; 40 | } catch (e) { 41 | return null; 42 | } 43 | }; 44 | const stateFromStorage = parseState(localStorage.getItem(LOCAL_STORAGE_KEY)); 45 | const initialState: State = stateFromStorage || defaultState; 46 | 47 | const reducer = (state = initialState, action: Action) => { 48 | switch (action.type) { 49 | case 'increment': return { 50 | ...state, 51 | count: state.count + 1, 52 | }; 53 | case 'decrement': return { 54 | ...state, 55 | count: state.count - 1, 56 | }; 57 | case 'setFirstName': return { 58 | ...state, 59 | person: { 60 | ...state.person, 61 | firstName: action.firstName, 62 | }, 63 | }; 64 | case 'setLastName': return { 65 | ...state, 66 | person: { 67 | ...state.person, 68 | lastName: action.lastName, 69 | }, 70 | }; 71 | case 'setAge': return { 72 | ...state, 73 | person: { 74 | ...state.person, 75 | age: action.age, 76 | }, 77 | }; 78 | default: return state; 79 | } 80 | }; 81 | 82 | const saveStateToStorage = ( 83 | { getState }: { getState: () => State }, 84 | ) => (next: Dispatch) => (action: Action) => { 85 | const returnValue = next(action); 86 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(getState())); 87 | return returnValue; 88 | }; 89 | 90 | export const { dispatch, useStoreState } = createStore( 91 | reducer, 92 | initialState, 93 | applyMiddleware(saveStateToStorage), 94 | ); 95 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/13_persistence.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`13_persistence should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`13_persistence should work with events 2`] = ` 12 | " 13 |

Counter

Count: 2
Count: 2

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`13_persistence should work with events 3`] = ` 20 | " 21 |

Counter

Count: 2
Count: 2

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`13_persistence should work with events 4`] = ` 28 | " 29 |

Counter

Count: 2
Count: 2

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`13_persistence should work with events 5`] = ` 36 | " 37 |

Counter

Count: 2
Count: 2

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | -------------------------------------------------------------------------------- /src/createGlobalState.ts: -------------------------------------------------------------------------------- 1 | import { SetStateAction, useCallback } from 'react'; 2 | 3 | import create from 'zustand'; 4 | 5 | const validateStateKey = (keys: string[], stateKey: string) => { 6 | if (!keys.includes(stateKey)) { 7 | throw new Error(`'${stateKey}' not found. It must be provided in initialState as a property key.`); 8 | } 9 | }; 10 | 11 | const isFunction = (fn: unknown): fn is Function => (typeof fn === 'function'); 12 | 13 | const updateValue = (oldValue: Value, newValue: SetStateAction) => ( 14 | isFunction(newValue) ? newValue(oldValue) : newValue 15 | ); 16 | 17 | /** 18 | * Create a global state. 19 | * 20 | * It returns a set of functions 21 | * - `useGlobalState`: a custom hook works like React.useState 22 | * - `getGlobalState`: a function to get a global state by key outside React 23 | * - `setGlobalState`: a function to set a global state by key outside React 24 | * - `subscribe`: a function that subscribes to state changes 25 | * 26 | * @example 27 | * import { createGlobalState } from 'react-hooks-global-state'; 28 | * 29 | * const { useGlobalState } = createGlobalState({ count: 0 }); 30 | * 31 | * const Component = () => { 32 | * const [count, setCount] = useGlobalState('count'); 33 | * ... 34 | * }; 35 | */ 36 | export const createGlobalState = (initialState: State) => { 37 | const useStore = create(() => initialState); 38 | 39 | type StateKeys = keyof State; 40 | const keys = Object.keys(initialState); 41 | 42 | const setGlobalState = ( 43 | stateKey: StateKey, 44 | update: SetStateAction, 45 | ) => { 46 | if (process.env.NODE_ENV !== 'production') { 47 | validateStateKey(keys, stateKey as string); 48 | } 49 | useStore.setState((previousState) => ({ 50 | [stateKey]: updateValue(previousState[stateKey], update), 51 | } as Pick as Partial)); 52 | }; 53 | 54 | const useGlobalState = (stateKey: StateKey) => { 55 | if (process.env.NODE_ENV !== 'production') { 56 | validateStateKey(keys, stateKey as string); 57 | } 58 | const selector = useCallback((state: State) => state[stateKey], [stateKey]); 59 | const partialState = useStore(selector); 60 | const updater = useCallback( 61 | (u: SetStateAction) => setGlobalState(stateKey, u), 62 | [stateKey], 63 | ); 64 | return [partialState, updater] as const; 65 | }; 66 | 67 | const getGlobalState = (stateKey: StateKey) => { 68 | if (process.env.NODE_ENV !== 'production') { 69 | validateStateKey(keys, stateKey as string); 70 | } 71 | return useStore.getState()[stateKey]; 72 | }; 73 | 74 | const subscribe = ( 75 | stateKey: StateKey, 76 | listener: (value: State[StateKey]) => void, 77 | ) => { 78 | useStore.subscribe((state, prevState) => { 79 | if (state[stateKey] !== prevState[stateKey]) { 80 | listener(state[stateKey]); 81 | } 82 | }); 83 | }; 84 | 85 | return { 86 | useGlobalState, 87 | getGlobalState, 88 | setGlobalState, 89 | subscribe, 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /__tests__/01_basic_spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { render, fireEvent, cleanup } from '@testing-library/react'; 3 | 4 | import { createGlobalState, createStore } from '../src/index'; 5 | 6 | describe('basic spec', () => { 7 | afterEach(cleanup); 8 | 9 | it('should exists exported functions', () => { 10 | expect(createGlobalState).toBeDefined(); 11 | expect(createStore).toBeDefined(); 12 | }); 13 | 14 | it('should be possible to not specify initial state', () => { 15 | const reducer = (state = { count: 0 }, action: { type: 'INC' }) => { 16 | if (action.type === 'INC') { 17 | return { count: state.count + 1 }; 18 | } 19 | return state; 20 | }; 21 | const { useStoreState, dispatch } = createStore(reducer); 22 | const Counter = () => { 23 | const value = useStoreState('count'); 24 | return ( 25 |
26 | {value} 27 | 28 |
29 | ); 30 | }; 31 | const App = () => ( 32 | 33 | 34 | 35 | ); 36 | const { getByText } = render(); 37 | expect(getByText('0')).toBeDefined(); 38 | fireEvent.click(getByText('+1')); 39 | expect(getByText('1')).toBeDefined(); 40 | }); 41 | 42 | it('should create a component with a global state', () => { 43 | const initialState = { 44 | count1: 0, 45 | }; 46 | const { useGlobalState } = createGlobalState(initialState); 47 | const Counter = () => { 48 | const [value, update] = useGlobalState('count1'); 49 | return ( 50 |
51 | {value} 52 | 53 |
54 | ); 55 | }; 56 | const App = () => ( 57 | 58 | 59 | 60 | 61 | ); 62 | const { getAllByText, container } = render(); 63 | expect(container).toMatchSnapshot(); 64 | fireEvent.click(getAllByText('+1')[0] as HTMLElement); 65 | expect(container).toMatchSnapshot(); 66 | }); 67 | 68 | it('should subscribe to global state change', () => { 69 | const containerRef : { current: null | HTMLDivElement } = { 70 | current: null, 71 | }; 72 | const { useGlobalState, subscribe } = createGlobalState({ 73 | count: 0, 74 | }); 75 | subscribe('count', (count) => { 76 | containerRef.current?.setAttribute('data-testid', `count ${count}`); 77 | }); 78 | 79 | const Counter = () => { 80 | const [value, update] = useGlobalState('count'); 81 | return ( 82 |
{ 84 | if (ref) { 85 | containerRef.current = ref; 86 | } 87 | }} 88 | > 89 | {value} 90 | 91 |
92 | ); 93 | }; 94 | const App = () => ( 95 | 96 | 97 | 98 | ); 99 | const { getByTestId, getAllByText } = render(); 100 | fireEvent.click(getAllByText('+1')[0] as HTMLElement); 101 | expect(getByTestId('count 1')).toBeDefined(); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ## [2.1.0] - 2022-12-04 6 | ### Added 7 | - expose "subscribe" function for "createGlobalState" #85 8 | 9 | ## [2.0.0] - 2022-08-05 10 | ### Changed from 2.0.0-rc.0 11 | - Depends on zustand v4.0.0 12 | 13 | ## [2.0.0-rc.0] - 2022-05-28 14 | ### Changed 15 | - Re-implemented with zustand as a dependency 16 | - `createStore` returns `useStoreState` 17 | - `useGlobalState` returned by `createStore` is deprecated 18 | ### Removed 19 | - BREAKING CHANGE: drop reduxDevToolsExt 20 | 21 | ## [1.0.2] - 2021-08-14 22 | ### Changed 23 | - Fix package.json properly for ESM 24 | 25 | ## [1.0.1] - 2020-07-04 26 | ### Changed 27 | - Modern build 28 | 29 | ## [1.0.0] - 2020-02-18 30 | ### Added 31 | - More tests 32 | ### Changed 33 | - Improve naming: stateKey 34 | 35 | ## [1.0.0-alpha.2] - 2020-01-25 36 | ### Changed 37 | - Migration to TypeScript (#36) 38 | - BREAKING CHANGE: reduxDevToolsExt is now in index 39 | 40 | ## [1.0.0-alpha.1] - 2020-01-24 41 | ### Changed 42 | - New API without context (#35) 43 | - BREAKING CHANGE: No longer GlobalStateProvider required 44 | 45 | ## [0.17.0] - 2020-01-09 46 | ### Changed 47 | - Support state branching (#31) 48 | 49 | ## [0.16.0] - 2019-10-22 50 | ### Changed 51 | - Improve typings with React types 52 | 53 | ## [0.15.0] - 2019-09-25 54 | ### Changed 55 | - Remove not-in-render warning in getGlobalState (#26) 56 | 57 | ## [0.14.0] - 2019-06-15 58 | ### Changed 59 | - Better error message for missing GlobalStateProvider 60 | 61 | ## [0.13.0] - 2019-06-11 62 | ### Added 63 | - Check name availability in runtime (DEV only) 64 | ### Changed 65 | - Update devDependencies 66 | 67 | ## [0.12.0] - 2019-06-07 68 | ### Changed 69 | - Proper GlobalStateProvider type 70 | ### Added 71 | - Add getGlobalState function 72 | 73 | ## [0.11.0] - 2019-05-25 74 | ### Changed 75 | - Fix failing to update state during initialization 76 | 77 | ## [0.10.0] - 2019-05-25 78 | ### Changed 79 | - Trick to support react-hot-loader 80 | 81 | ## [0.9.0] - 2019-03-18 82 | ### Changed 83 | - Better typings for optional initialState 84 | 85 | ## [0.8.0] - 2019-03-16 86 | ### Changed 87 | - Experimental support for eliminating initialState 88 | - Copied devtools.d.ts into the dist folder 89 | 90 | ## [0.7.0] - 2019-02-11 91 | ### Changed 92 | - Fixed type definition of CreateGlobalState 93 | - Eliminated React warning for using unstable_observedBits 94 | 95 | ## [0.6.0] - 2019-02-07 96 | ### Changed 97 | - Updated Dependencies (react 16.8) 98 | 99 | ## [0.5.0] - 2019-01-26 100 | ### Changed 101 | - AnyEnhancer type hack for Redux 102 | 103 | ## [0.4.0] - 2018-12-03 104 | ### Changed 105 | - Fix Initialization bug (#2) 106 | 107 | ## [0.3.0] - 2018-11-12 108 | ### Changed 109 | - New API using Context and observedBits 110 | 111 | ## [0.2.0] - 2018-11-09 112 | ### Changed 113 | - Hacky/dirty Redux DevTools Extension support 114 | - Fix type definition 115 | 116 | ## [0.1.0] - 2018-11-07 117 | ### Changed 118 | - API changed to useGlobalState/setGlobalState 119 | 120 | ## [0.0.5] - 2018-11-06 121 | ### Changed 122 | - Store enhancer support 123 | 124 | ## [0.0.4] - 2018-11-05 125 | ### Changed 126 | - Redux-like createStore 127 | - Update README 128 | 129 | ## [0.0.3] - 2018-11-04 130 | ### Added 131 | - Reducer support 132 | 133 | ## [0.0.2] - 2018-10-30 134 | ### Changed 135 | - Update README 136 | 137 | ## [0.0.1] - 2018-10-28 138 | ### Added 139 | - Initial experimental release 140 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/01_minimal.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`01_minimal should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

TextBox

Text: hello
Text: hello
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`01_minimal should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

TextBox

Text: hello
Text: hello
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`01_minimal should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

TextBox

Text: hello
Text: hello
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`01_minimal should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

TextBox

Text: hello
Text: hello
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`01_minimal should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

TextBox

Text: hello1
Text: hello1
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`01_minimal should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

TextBox

Text: hello12
Text: hello12
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`01_minimal should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

TextBox

Text: hello123
Text: hello123
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`01_minimal should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

TextBox

Text: hello1234
Text: hello1234
62 | 63 | 64 | " 65 | `; 66 | -------------------------------------------------------------------------------- /src/createStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-explicit-any: off */ 2 | 3 | import { Reducer, SetStateAction, useCallback } from 'react'; 4 | 5 | import create from 'zustand'; 6 | import { redux } from 'zustand/middleware'; 7 | 8 | type ExtractState = S extends { 9 | getState: () => infer T; 10 | } ? T : never; 11 | 12 | const validateStateKey = (keys: string[], stateKey: string) => { 13 | if (!keys.includes(stateKey)) { 14 | throw new Error(`'${stateKey}' not found. It must be provided in initialState as a property key.`); 15 | } 16 | }; 17 | 18 | const isFunction = (fn: unknown): fn is Function => (typeof fn === 'function'); 19 | 20 | const updateValue = (oldValue: Value, newValue: SetStateAction) => ( 21 | isFunction(newValue) ? newValue(oldValue) : newValue 22 | ); 23 | 24 | /** 25 | * Create a global store. 26 | * 27 | * It returns a set of functions 28 | * - `useStoreState`: a custom hook to read store state by key 29 | * - `getState`: a function to get store state by key outside React 30 | * - `dispatch`: a function to dispatch an action to store 31 | * 32 | * A store works somewhat similarly to Redux, but not the same. 33 | * 34 | * @example 35 | * import { createStore } from 'react-hooks-global-state'; 36 | * 37 | * const initialState = { count: 0 }; 38 | * const reducer = ...; 39 | * 40 | * const store = createStore(reducer, initialState); 41 | * const { useStoreState, dispatch } = store; 42 | * 43 | * const Component = () => { 44 | * const count = useStoreState('count'); 45 | * ... 46 | * }; 47 | */ 48 | export const createStore = ( 49 | reducer: Reducer, 50 | initialState: State = (reducer as any)(undefined, { type: undefined }), 51 | enhancer?: any, 52 | ): Store => { 53 | if (enhancer) return enhancer(createStore)(reducer, initialState); 54 | 55 | const useStore = create(redux(reducer, initialState)); 56 | 57 | type BoundState = ExtractState; 58 | type StateKeys = keyof BoundState; 59 | const keys = Object.keys(initialState); 60 | 61 | const useStoreState = (stateKey: StateKey) => { 62 | if (process.env.NODE_ENV !== 'production') { 63 | validateStateKey(keys, stateKey as string); 64 | } 65 | const selector = useCallback( 66 | (state: BoundState) => state[stateKey], 67 | [stateKey], 68 | ); 69 | return useStore(selector); 70 | }; 71 | 72 | const useGlobalState = (stateKey: StateKey) => { 73 | if (process.env.NODE_ENV !== 'production') { 74 | // eslint-disable-next-line no-console 75 | console.warn('[DEPRECATED] useStoreState instead'); 76 | } 77 | const partialState = useStoreState(stateKey); 78 | const updater = useCallback( 79 | (update: SetStateAction) => { 80 | useStore.setState((previousState) => ({ 81 | [stateKey]: updateValue(previousState[stateKey], update), 82 | } as Pick as Partial)); 83 | }, 84 | [stateKey], 85 | ); 86 | return [partialState, updater] as const; 87 | }; 88 | 89 | return { 90 | useStoreState, 91 | useGlobalState, 92 | getState: useStore.getState, 93 | dispatch: useStore.dispatch, 94 | } as unknown as Store; 95 | }; 96 | 97 | type Store = { 98 | useStoreState: (stateKey: StateKey) => State[StateKey]; 99 | /** 100 | * useGlobalState created by createStore is deprecated. 101 | * 102 | * @deprecated useStoreState instead 103 | */ 104 | useGlobalState: (stateKey: StateKey) => readonly [ 105 | State[StateKey], 106 | (u: SetStateAction) => void, 107 | ]; 108 | getState: () => State; 109 | dispatch: (action: Action) => Action; 110 | }; 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-global-state", 3 | "description": "Simple global state for React with Hooks API without Context API", 4 | "version": "2.1.0", 5 | "author": "Daishi Kato", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dai-shi/react-hooks-global-state.git" 9 | }, 10 | "source": "./src/index.ts", 11 | "main": "./dist/index.umd.js", 12 | "module": "./dist/index.modern.js", 13 | "types": "./dist/src/index.d.ts", 14 | "exports": { 15 | "./package.json": "./package.json", 16 | ".": { 17 | "types": "./dist/src/index.d.ts", 18 | "module": "./dist/index.modern.js", 19 | "import": "./dist/index.modern.mjs", 20 | "default": "./dist/index.umd.js" 21 | } 22 | }, 23 | "sideEffects": false, 24 | "files": [ 25 | "src", 26 | "dist" 27 | ], 28 | "scripts": { 29 | "compile": "microbundle build -f modern,umd --globals react=React", 30 | "postcompile": "cp dist/index.modern.js dist/index.modern.mjs && cp dist/index.modern.js.map dist/index.modern.mjs.map", 31 | "test": "run-s eslint tsc-test jest e2e-test:*", 32 | "eslint": "eslint --ext .js,.ts,.tsx --ignore-pattern dist .", 33 | "jest": "jest --preset ts-jest/presets/js-with-ts __tests__/*.tsx", 34 | "tsc-test": "tsc --project . --noEmit", 35 | "apidoc": "documentation readme --section API --markdown-toc false --parse-extension ts src/createGlobalState.ts src/createStore.ts", 36 | "e2e-test:01_minimal": "server-test examples:01_minimal 8080 'jest --preset jest-puppeteer __tests__/e2e/01_minimal.ts'", 37 | "e2e-test:02_typescript": "server-test examples:02_typescript 8080 'jest --preset jest-puppeteer __tests__/e2e/02_typescript.ts'", 38 | "e2e-test:03_actions": "server-test examples:03_actions 8080 'jest --preset jest-puppeteer __tests__/e2e/03_actions.ts'", 39 | "e2e-test:04_fetch": "server-test examples:04_fetch 8080 'jest --preset jest-puppeteer __tests__/e2e/04_fetch.ts'", 40 | "e2e-test:05_onmount": "server-test examples:05_onmount 8080 'jest --preset jest-puppeteer __tests__/e2e/05_onmount.ts'", 41 | "e2e-test:06_reducer": "server-test examples:06_reducer 8080 'jest --preset jest-puppeteer __tests__/e2e/06_reducer.ts'", 42 | "e2e-test:07_middleware": "server-test examples:07_middleware 8080 'jest --preset jest-puppeteer __tests__/e2e/07_middleware.ts'", 43 | "e2e-test:08_thunk": "server-test examples:08_thunk 8080 'jest --preset jest-puppeteer __tests__/e2e/08_thunk.ts'", 44 | "e2e-test:09_comparison": "server-test examples:09_comparison 8080 'jest --preset jest-puppeteer __tests__/e2e/09_comparison.ts'", 45 | "e2e-test:10_immer": "server-test examples:10_immer 8080 'jest --preset jest-puppeteer __tests__/e2e/10_immer.ts'", 46 | "e2e-test:11_deep": "server-test examples:11_deep 8080 'jest --preset jest-puppeteer __tests__/e2e/11_deep.ts'", 47 | "e2e-test:13_persistence": "server-test examples:13_persistence 8080 'jest --preset jest-puppeteer __tests__/e2e/13_persistence.ts'", 48 | "examples:01_minimal": "DIR=01_minimal EXT=js webpack serve", 49 | "examples:02_typescript": "DIR=02_typescript webpack serve", 50 | "examples:03_actions": "DIR=03_actions webpack serve", 51 | "examples:04_fetch": "DIR=04_fetch webpack serve", 52 | "examples:05_onmount": "DIR=05_onmount webpack serve", 53 | "examples:06_reducer": "DIR=06_reducer webpack serve", 54 | "examples:07_middleware": "DIR=07_middleware webpack serve", 55 | "examples:08_thunk": "DIR=08_thunk webpack serve", 56 | "examples:09_comparison": "DIR=09_comparison webpack serve", 57 | "examples:10_immer": "DIR=10_immer webpack serve", 58 | "examples:11_deep": "DIR=11_deep webpack serve", 59 | "examples:13_persistence": "DIR=13_persistence webpack serve" 60 | }, 61 | "jest": { 62 | "testEnvironment": "jsdom", 63 | "transform": { 64 | "^.+\\.ts$": "ts-jest" 65 | } 66 | }, 67 | "keywords": [ 68 | "react", 69 | "state", 70 | "hooks", 71 | "stateless", 72 | "thisless", 73 | "pure" 74 | ], 75 | "license": "MIT", 76 | "dependencies": { 77 | "zustand": "4.0.0" 78 | }, 79 | "devDependencies": { 80 | "@testing-library/react": "^13.3.0", 81 | "@types/expect-puppeteer": "^5.0.1", 82 | "@types/jest": "^28.1.6", 83 | "@types/jest-environment-puppeteer": "^5.0.2", 84 | "@types/puppeteer": "^5.4.6", 85 | "@types/react": "^18.0.15", 86 | "@types/react-dom": "^18.0.6", 87 | "@types/redux-logger": "^3.0.9", 88 | "@typescript-eslint/eslint-plugin": "^5.32.0", 89 | "@typescript-eslint/parser": "^5.32.0", 90 | "documentation": "^13.2.5", 91 | "eslint": "^8.21.0", 92 | "eslint-config-airbnb": "^19.0.4", 93 | "eslint-plugin-import": "^2.26.0", 94 | "eslint-plugin-jsx-a11y": "^6.6.1", 95 | "eslint-plugin-react": "^7.30.1", 96 | "eslint-plugin-react-hooks": "^4.6.0", 97 | "html-webpack-plugin": "^5.5.0", 98 | "immer": "^9.0.15", 99 | "jest": "^28.1.3", 100 | "jest-environment-jsdom": "^28.1.3", 101 | "jest-puppeteer": "^6.1.1", 102 | "microbundle": "^0.15.0", 103 | "npm-run-all": "^4.1.5", 104 | "puppeteer": "^16.0.0", 105 | "react": "^18.2.0", 106 | "react-dom": "^18.2.0", 107 | "redux": "^4.2.0", 108 | "redux-logger": "^3.0.6", 109 | "redux-thunk": "^2.4.1", 110 | "start-server-and-test": "^1.14.0", 111 | "ts-jest": "^28.0.7", 112 | "ts-loader": "^9.3.1", 113 | "typescript": "^4.7.4", 114 | "webpack": "^5.74.0", 115 | "webpack-cli": "^4.10.0", 116 | "webpack-dev-server": "^4.9.3" 117 | }, 118 | "peerDependencies": { 119 | "react": ">=16.8.0" 120 | }, 121 | "resolutions": { 122 | "@types/node": "^15" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/10_immer.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`10_immer should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`10_immer should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`10_immer should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`10_immer should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`10_immer should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`10_immer should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`10_immer should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`10_immer should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`10_immer should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`10_immer should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
78 | 79 | 80 | " 81 | `; 82 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/03_actions.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`03_actions should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`03_actions should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`03_actions should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`03_actions should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`03_actions should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`03_actions should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`03_actions should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`03_actions should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`03_actions should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`03_actions should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
78 | 79 | 80 | " 81 | `; 82 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/06_reducer.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`06_reducer should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`06_reducer should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`06_reducer should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`06_reducer should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`06_reducer should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`06_reducer should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`06_reducer should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`06_reducer should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`06_reducer should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`06_reducer should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
78 | 79 | 80 | " 81 | `; 82 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/02_typescript.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`02_typescript should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`02_typescript should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`02_typescript should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`02_typescript should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`02_typescript should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`02_typescript should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`02_typescript should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`02_typescript should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`02_typescript should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`02_typescript should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
78 | 79 | 80 | " 81 | `; 82 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/07_middleware.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`07_middleware should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`07_middleware should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`07_middleware should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`07_middleware should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`07_middleware should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`07_middleware should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`07_middleware should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`07_middleware should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`07_middleware should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`07_middleware should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
78 | 79 | 80 | " 81 | `; 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is no longer maintained. 2 | Please directly use [Zustand](https://github.com/pmndrs/zustand). 3 | 4 | --- 5 | 6 | # react-hooks-global-state 7 | 8 | [![CI](https://img.shields.io/github/actions/workflow/status/dai-shi/react-hooks-global-state/ci.yml?branch=main)](https://github.com/dai-shi/react-hooks-global-state/actions?query=workflow%3ACI) 9 | [![npm](https://img.shields.io/npm/v/react-hooks-global-state)](https://www.npmjs.com/package/react-hooks-global-state) 10 | [![size](https://img.shields.io/bundlephobia/minzip/react-hooks-global-state)](https://bundlephobia.com/result?p=react-hooks-global-state) 11 | [![discord](https://img.shields.io/discord/627656437971288081)](https://discord.gg/MrQdmzd) 12 | 13 | Simple global state for React with Hooks API without Context API 14 | 15 | ## Introduction 16 | 17 | This is a library to provide a global state with React Hooks. 18 | It has following characteristics. 19 | 20 | * Optimization for shallow state getter and setter. 21 | * The library cares the state object only one-level deep. 22 | * TypeScript type definitions 23 | * A creator function creates hooks with types inferred. 24 | * Redux middleware support to some extent 25 | * Some of libraries in Redux ecosystem can be used. 26 | 27 | ## Install 28 | 29 | ```bash 30 | npm install react-hooks-global-state 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### setState style 36 | 37 | ```javascript 38 | import React from 'react'; 39 | import { createGlobalState } from 'react-hooks-global-state'; 40 | 41 | const initialState = { count: 0 }; 42 | const { useGlobalState } = createGlobalState(initialState); 43 | 44 | const Counter = () => { 45 | const [count, setCount] = useGlobalState('count'); 46 | return ( 47 |
48 | Counter: {count} 49 | {/* update state by passing callback function */} 50 | 51 | {/* update state by passing new value */} 52 | 53 |
54 | ); 55 | }; 56 | 57 | const App = () => ( 58 | <> 59 | 60 | 61 | 62 | ); 63 | ``` 64 | 65 | ### reducer style 66 | 67 | ```javascript 68 | import React from 'react'; 69 | import { createStore } from 'react-hooks-global-state'; 70 | 71 | const reducer = (state, action) => { 72 | switch (action.type) { 73 | case 'increment': return { ...state, count: state.count + 1 }; 74 | case 'decrement': return { ...state, count: state.count - 1 }; 75 | default: return state; 76 | } 77 | }; 78 | const initialState = { count: 0 }; 79 | const { dispatch, useStoreState } = createStore(reducer, initialState); 80 | 81 | const Counter = () => { 82 | const value = useStoreState('count'); 83 | return ( 84 |
85 | Counter: {value} 86 | 87 | 88 |
89 | ); 90 | }; 91 | 92 | const App = () => ( 93 | <> 94 | 95 | 96 | 97 | ); 98 | ``` 99 | 100 | ## API 101 | 102 | 103 | 104 | ### createGlobalState 105 | 106 | Create a global state. 107 | 108 | It returns a set of functions 109 | 110 | * `useGlobalState`: a custom hook works like React.useState 111 | * `getGlobalState`: a function to get a global state by key outside React 112 | * `setGlobalState`: a function to set a global state by key outside React 113 | * `subscribe`: a function that subscribes to state changes 114 | 115 | #### Parameters 116 | 117 | * `initialState` **State** 118 | 119 | #### Examples 120 | 121 | ```javascript 122 | import { createGlobalState } from 'react-hooks-global-state'; 123 | 124 | const { useGlobalState } = createGlobalState({ count: 0 }); 125 | 126 | const Component = () => { 127 | const [count, setCount] = useGlobalState('count'); 128 | ... 129 | }; 130 | ``` 131 | 132 | ### createStore 133 | 134 | Create a global store. 135 | 136 | It returns a set of functions 137 | 138 | * `useStoreState`: a custom hook to read store state by key 139 | * `getState`: a function to get store state by key outside React 140 | * `dispatch`: a function to dispatch an action to store 141 | 142 | A store works somewhat similarly to Redux, but not the same. 143 | 144 | #### Parameters 145 | 146 | * `reducer` **Reducer\** 147 | * `initialState` **State** (optional, default `(reducer as any)(undefined,{type:undefined})`) 148 | * `enhancer` **any?** 149 | 150 | #### Examples 151 | 152 | ```javascript 153 | import { createStore } from 'react-hooks-global-state'; 154 | 155 | const initialState = { count: 0 }; 156 | const reducer = ...; 157 | 158 | const store = createStore(reducer, initialState); 159 | const { useStoreState, dispatch } = store; 160 | 161 | const Component = () => { 162 | const count = useStoreState('count'); 163 | ... 164 | }; 165 | ``` 166 | 167 | Returns **Store\** 168 | 169 | ### useGlobalState 170 | 171 | useGlobalState created by createStore is deprecated. 172 | 173 | Type: function (stateKey: StateKey): any 174 | 175 | **Meta** 176 | 177 | * **deprecated**: useStoreState instead 178 | 179 | ## Examples 180 | 181 | The [examples](examples) folder contains working examples. 182 | You can run one of them with 183 | 184 | ```bash 185 | PORT=8080 npm run examples:01_minimal 186 | ``` 187 | 188 | and open in your web browser. 189 | 190 | You can also try them in codesandbox.io: 191 | [01](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/01\_minimal) 192 | [02](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/02\_typescript) 193 | [03](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/03\_actions) 194 | [04](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/04\_fetch) 195 | [05](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/05\_onmount) 196 | [06](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/06\_reducer) 197 | [07](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/07\_middleware) 198 | [08](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/08\_thunk) 199 | [09](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/09\_comparison) 200 | [10](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/10\_immer) 201 | [11](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/11\_deep) 202 | [13](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/main/examples/13\_persistence) 203 | 204 | ## Blogs 205 | 206 | * [TypeScript-aware React hooks for global state](https://blog.axlight.com/posts/typescript-aware-react-hooks-for-global-state/) 207 | * [An alternative to React Redux by React Hooks API (For both JavaScript and TypeScript)](https://blog.axlight.com/posts/an-alternative-to-react-redux-by-react-hooks-api-for-both-javascript-and-typescript/) 208 | * [Redux middleware compatible React Hooks library for easy global state management](https://blog.axlight.com/posts/redux-middleware-compatible-react-hooks-library-for-easy-global-state-management/) 209 | * [React Hooks Tutorial on pure useReducer + useContext for global state like Redux and comparison with react-hooks-global-state](https://blog.axlight.com/posts/react-hooks-tutorial-for-pure-usereducer-usecontext-for-global-state-like-redux-and-comparison/) 210 | * [Four patterns for global state with React hooks: Context or Redux](https://blog.axlight.com/posts/four-patterns-for-global-state-with-react-hooks-context-or-redux/) 211 | * [Steps to Develop Global State for React With Hooks Without Context](https://blog.axlight.com/posts/steps-to-develop-global-state-for-react/) 212 | 213 | ## Community Wiki 214 | 215 | * [Persistence](https://github.com/dai-shi/react-hooks-global-state/wiki/Persistence) 216 | * [Optional initialState](https://github.com/dai-shi/react-hooks-global-state/wiki/Optional-initialState) 217 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/08_thunk.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`08_thunk should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`08_thunk should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`08_thunk should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1
Count: 1

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`08_thunk should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`08_thunk should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`08_thunk should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`08_thunk should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`08_thunk should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`08_thunk should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`08_thunk should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
78 | 79 | 80 | " 81 | `; 82 | 83 | exports[`08_thunk should work with events 11`] = ` 84 | " 85 |

Counter

Count: 0
Count: 0

Person

First Name:Loading...
Last Name:
Age:
First Name:Loading...
Last Name:
Age:
86 | 87 | 88 | " 89 | `; 90 | 91 | exports[`08_thunk should work with events 12`] = ` 92 | " 93 |

Counter

Count: 0
Count: 0

Person

First Name:
Last Name:
Age:
First Name:
Last Name:
Age:
94 | 95 | 96 | " 97 | `; 98 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/11_deep.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`11_deep should work with events 1`] = ` 4 | " 5 |

Counter

Count: 1(numRendered: 6)
Count: 1(numRendered: 8)

Person

First Name:(numRendered: 2)
Last Name:(numRendered: 4)
Age:(numRendered: 6)
Age Doubled:0
First Name:(numRendered: 8)
Last Name:(numRendered: 10)
Age:(numRendered: 12)
Age Doubled:0
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`11_deep should work with events 2`] = ` 12 | " 13 |

Counter

Count: 0(numRendered: 10)
Count: 0(numRendered: 12)

Person

First Name:(numRendered: 2)
Last Name:(numRendered: 4)
Age:(numRendered: 6)
Age Doubled:0
First Name:(numRendered: 8)
Last Name:(numRendered: 10)
Age:(numRendered: 12)
Age Doubled:0
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`11_deep should work with events 3`] = ` 20 | " 21 |

Counter

Count: 1(numRendered: 14)
Count: 1(numRendered: 16)

Person

First Name:(numRendered: 2)
Last Name:(numRendered: 4)
Age:(numRendered: 6)
Age Doubled:0
First Name:(numRendered: 8)
Last Name:(numRendered: 10)
Age:(numRendered: 12)
Age Doubled:0
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`11_deep should work with events 4`] = ` 28 | " 29 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:(numRendered: 2)
Last Name:(numRendered: 4)
Age:(numRendered: 6)
Age Doubled:0
First Name:(numRendered: 8)
Last Name:(numRendered: 10)
Age:(numRendered: 12)
Age Doubled:0
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`11_deep should work with events 5`] = ` 36 | " 37 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:1(numRendered: 14)
Last Name:(numRendered: 4)
Age:(numRendered: 6)
Age Doubled:0
First Name:1(numRendered: 16)
Last Name:(numRendered: 10)
Age:(numRendered: 12)
Age Doubled:0
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`11_deep should work with events 6`] = ` 44 | " 45 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:1(numRendered: 14)
Last Name:2(numRendered: 18)
Age:(numRendered: 6)
Age Doubled:0
First Name:1(numRendered: 16)
Last Name:2(numRendered: 20)
Age:(numRendered: 12)
Age Doubled:0
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`11_deep should work with events 7`] = ` 52 | " 53 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:1(numRendered: 14)
Last Name:2(numRendered: 18)
Age:(numRendered: 22)
Age Doubled:6
First Name:1(numRendered: 16)
Last Name:2(numRendered: 20)
Age:(numRendered: 24)
Age Doubled:6
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`11_deep should work with events 8`] = ` 60 | " 61 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:14(numRendered: 26)
Last Name:2(numRendered: 18)
Age:(numRendered: 22)
Age Doubled:6
First Name:14(numRendered: 28)
Last Name:2(numRendered: 20)
Age:(numRendered: 24)
Age Doubled:6
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`11_deep should work with events 9`] = ` 68 | " 69 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:14(numRendered: 26)
Last Name:25(numRendered: 30)
Age:(numRendered: 22)
Age Doubled:6
First Name:14(numRendered: 28)
Last Name:25(numRendered: 32)
Age:(numRendered: 24)
Age Doubled:6
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`11_deep should work with events 10`] = ` 76 | " 77 |

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:14(numRendered: 26)
Last Name:25(numRendered: 30)
Age:(numRendered: 34)
Age Doubled:72
First Name:14(numRendered: 28)
Last Name:25(numRendered: 32)
Age:(numRendered: 36)
Age Doubled:72
78 | 79 | 80 | " 81 | `; 82 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/09_comparison.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`09_comparison should work with events 1`] = ` 4 | " 5 |

useReducer + useContext

Counter

Count: 1(numRendered: 6)
Count: 1(numRendered: 8)

Person

First Name:
Last Name:
Age:
(numRendered: 6)
First Name:
Last Name:
Age:
(numRendered: 8)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
6 | 7 | 8 | " 9 | `; 10 | 11 | exports[`09_comparison should work with events 2`] = ` 12 | " 13 |

useReducer + useContext

Counter

Count: 0(numRendered: 10)
Count: 0(numRendered: 12)

Person

First Name:
Last Name:
Age:
(numRendered: 10)
First Name:
Last Name:
Age:
(numRendered: 12)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
14 | 15 | 16 | " 17 | `; 18 | 19 | exports[`09_comparison should work with events 3`] = ` 20 | " 21 |

useReducer + useContext

Counter

Count: 1(numRendered: 14)
Count: 1(numRendered: 16)

Person

First Name:
Last Name:
Age:
(numRendered: 14)
First Name:
Last Name:
Age:
(numRendered: 16)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
22 | 23 | 24 | " 25 | `; 26 | 27 | exports[`09_comparison should work with events 4`] = ` 28 | " 29 |

useReducer + useContext

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 18)
First Name:
Last Name:
Age:
(numRendered: 20)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
30 | 31 | 32 | " 33 | `; 34 | 35 | exports[`09_comparison should work with events 5`] = ` 36 | " 37 |

useReducer + useContext

Counter

Count: 0(numRendered: 22)
Count: 0(numRendered: 24)

Person

First Name:
Last Name:
Age:
(numRendered: 22)
First Name:
Last Name:
Age:
(numRendered: 24)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
38 | 39 | 40 | " 41 | `; 42 | 43 | exports[`09_comparison should work with events 6`] = ` 44 | " 45 |

useReducer + useContext

Counter

Count: 0(numRendered: 26)
Count: 0(numRendered: 28)

Person

First Name:
Last Name:
Age:
(numRendered: 26)
First Name:
Last Name:
Age:
(numRendered: 28)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
46 | 47 | 48 | " 49 | `; 50 | 51 | exports[`09_comparison should work with events 7`] = ` 52 | " 53 |

useReducer + useContext

Counter

Count: 0(numRendered: 30)
Count: 0(numRendered: 32)

Person

First Name:
Last Name:
Age:
(numRendered: 30)
First Name:
Last Name:
Age:
(numRendered: 32)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
54 | 55 | 56 | " 57 | `; 58 | 59 | exports[`09_comparison should work with events 8`] = ` 60 | " 61 |

useReducer + useContext

Counter

Count: 0(numRendered: 34)
Count: 0(numRendered: 36)

Person

First Name:
Last Name:
Age:
(numRendered: 34)
First Name:
Last Name:
Age:
(numRendered: 36)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
62 | 63 | 64 | " 65 | `; 66 | 67 | exports[`09_comparison should work with events 9`] = ` 68 | " 69 |

useReducer + useContext

Counter

Count: 0(numRendered: 38)
Count: 0(numRendered: 40)

Person

First Name:
Last Name:
Age:
(numRendered: 38)
First Name:
Last Name:
Age:
(numRendered: 40)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
70 | 71 | 72 | " 73 | `; 74 | 75 | exports[`09_comparison should work with events 10`] = ` 76 | " 77 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 2)
Count: 0(numRendered: 4)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
78 | 79 | 80 | " 81 | `; 82 | 83 | exports[`09_comparison should work with events 11`] = ` 84 | " 85 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 1(numRendered: 6)
Count: 1(numRendered: 8)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
86 | 87 | 88 | " 89 | `; 90 | 91 | exports[`09_comparison should work with events 12`] = ` 92 | " 93 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 10)
Count: 0(numRendered: 12)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
94 | 95 | 96 | " 97 | `; 98 | 99 | exports[`09_comparison should work with events 13`] = ` 100 | " 101 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 1(numRendered: 14)
Count: 1(numRendered: 16)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
102 | 103 | 104 | " 105 | `; 106 | 107 | exports[`09_comparison should work with events 14`] = ` 108 | " 109 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 2)
First Name:
Last Name:
Age:
(numRendered: 4)
110 | 111 | 112 | " 113 | `; 114 | 115 | exports[`09_comparison should work with events 15`] = ` 116 | " 117 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 6)
First Name:
Last Name:
Age:
(numRendered: 8)
118 | 119 | 120 | " 121 | `; 122 | 123 | exports[`09_comparison should work with events 16`] = ` 124 | " 125 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 10)
First Name:
Last Name:
Age:
(numRendered: 12)
126 | 127 | 128 | " 129 | `; 130 | 131 | exports[`09_comparison should work with events 17`] = ` 132 | " 133 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 14)
First Name:
Last Name:
Age:
(numRendered: 16)
134 | 135 | 136 | " 137 | `; 138 | 139 | exports[`09_comparison should work with events 18`] = ` 140 | " 141 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 18)
First Name:
Last Name:
Age:
(numRendered: 20)
142 | 143 | 144 | " 145 | `; 146 | 147 | exports[`09_comparison should work with events 19`] = ` 148 | " 149 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 22)
First Name:
Last Name:
Age:
(numRendered: 24)
150 | 151 | 152 | " 153 | `; 154 | 155 | exports[`09_comparison should work with events 20`] = ` 156 | " 157 |

useReducer + useContext

Counter

Count: 0(numRendered: 42)
Count: 0(numRendered: 44)

Person

First Name:
Last Name:
Age:
(numRendered: 42)
First Name:
Last Name:
Age:
(numRendered: 44)

react-hooks-global-state

Counter

Count: 0(numRendered: 18)
Count: 0(numRendered: 20)

Person

First Name:
Last Name:
Age:
(numRendered: 26)
First Name:
Last Name:
Age:
(numRendered: 28)
158 | 159 | 160 | " 161 | `; 162 | --------------------------------------------------------------------------------