diff --git a/CHANGELOG.md b/CHANGELOG.md index 7338c8a..10630f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] ### Changed -- Reduce bundle size a little +- New API without context ## [0.17.0] - 2020-01-09 ### Changed diff --git a/README.md b/README.md index 2707567..6ae9426 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,8 @@ Simple global state for React with Hooks API ## Introduction -If you ever try to implement a global state with Context and Hooks, -you probably find it straightforward. -This library provide more or less the same functionality -with some following bonuses. +This is a library to provide a global state with React Hooks. +It has following characteristics. - Optimization for shallow state getter and setter. - The library cares the state object only one-level deep. @@ -20,9 +18,8 @@ with some following bonuses. - Redux middleware support to some extent - Some of libraries in Redux ecosystem can be used. - Redux DevTools Extension could be used in a simple scenario. - -Due to the fact that this library utilizes `unstable_observedBits` -for optimization, this library is still in alpha. +- Concurrent Mode support (Experimental) + - Undocumented `useGlobalStateProvider` supports CM without React Context. ## Install @@ -39,7 +36,7 @@ import React from 'react'; import { createGlobalState } from 'react-hooks-global-state'; const initialState = { count: 0 }; -const { GlobalStateProvider, useGlobalState } = createGlobalState(initialState); +const { useGlobalState } = createGlobalState(initialState); const Counter = () => { const [count, setCount] = useGlobalState('count'); @@ -55,10 +52,10 @@ const Counter = () => { }; const App = () => ( - + <> - + ); ``` @@ -76,7 +73,7 @@ const reducer = (state, action) => { } }; const initialState = { count: 0 }; -const { GlobalStateProvider, dispatch, useGlobalState } = createStore(reducer, initialState); +const { dispatch, useGlobalState } = createStore(reducer, initialState); const Counter = () => { const [value] = useGlobalState('count'); @@ -90,10 +87,10 @@ const Counter = () => { }; const App = () => ( - + <> - + ); ``` @@ -123,12 +120,6 @@ You can also try them in codesandbox.io: [12](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/master/examples/12_effect) [13](https://codesandbox.io/s/github/dai-shi/react-hooks-global-state/tree/master/examples/13_persistence) -## Limitations - -- Due to the implementation relying on `observedBits` in the Context API, - the performance may drop down if a state holds more than 30 items. - Reference: [#1](https://github.com/dai-shi/react-hooks-global-state/issues/1) - ## Blogs - [TypeScript-aware React hooks for global state](https://blog.axlight.com/posts/typescript-aware-react-hooks-for-global-state/) diff --git a/__tests__/01_basic_spec.js b/__tests__/01_basic_spec.js index 5e66918..43dcf98 100644 --- a/__tests__/01_basic_spec.js +++ b/__tests__/01_basic_spec.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { StrictMode } from 'react'; import { render, fireEvent, cleanup } from '@testing-library/react'; import { createGlobalState, createStore } from '../src/index'; @@ -12,7 +12,7 @@ describe('basic spec', () => { it('should be possible to not specify initial state', () => { const reducer = () => ({ count: 0 }); - const { GlobalStateProvider, useGlobalState } = createStore(reducer); + const { useGlobalState } = createStore(reducer); const Counter = () => { const [value, update] = useGlobalState('count'); return ( @@ -23,9 +23,9 @@ describe('basic spec', () => { ); }; const App = () => ( - + - + ); const { getByText } = render(); expect(getByText('0')).toBeDefined(); @@ -37,7 +37,7 @@ describe('basic spec', () => { const initialState = { count1: 0, }; - const { GlobalStateProvider, useGlobalState } = createGlobalState(initialState); + const { useGlobalState } = createGlobalState(initialState); const Counter = () => { const [value, update] = useGlobalState('count1'); return ( @@ -48,10 +48,10 @@ describe('basic spec', () => { ); }; const App = () => ( - + - + ); const { getAllByText, container } = render(); expect(container).toMatchSnapshot(); diff --git a/__tests__/02_useeffect_spec.js b/__tests__/02_useeffect_spec.js index bac171b..7045023 100644 --- a/__tests__/02_useeffect_spec.js +++ b/__tests__/02_useeffect_spec.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { StrictMode, useEffect } from 'react'; import { render, cleanup } from '@testing-library/react'; import { createGlobalState } from '../src/index'; @@ -10,7 +10,7 @@ describe('useeffect spec', () => { const initialState = { count1: 0, }; - const { GlobalStateProvider, useGlobalState } = createGlobalState(initialState); + const { useGlobalState } = createGlobalState(initialState); const Counter = () => { const [value, update] = useGlobalState('count1'); useEffect(() => { @@ -23,9 +23,9 @@ describe('useeffect spec', () => { ); }; const App = () => ( - + - + ); const { container } = render(); expect(container).toMatchSnapshot(); diff --git a/__tests__/03_startup_spec.js b/__tests__/03_startup_spec.js index 1889e6f..b2a0bd1 100644 --- a/__tests__/03_startup_spec.js +++ b/__tests__/03_startup_spec.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { StrictMode } from 'react'; import { render, cleanup } from '@testing-library/react'; import { createGlobalState, createStore } from '../src/index'; @@ -11,7 +11,7 @@ describe('startup spec', () => { count1: 0, count2: 0, }; - const { GlobalStateProvider, setGlobalState, useGlobalState } = createGlobalState(initialState); + const { setGlobalState, useGlobalState } = createGlobalState(initialState); const Counter = ({ name }) => { setGlobalState(name, 9); const [value] = useGlobalState(name); @@ -23,10 +23,10 @@ describe('startup spec', () => { ); }; const App = () => ( - + - + ); const { getByTestId } = render(); expect(getByTestId('count1').innerHTML).toBe('9'); @@ -47,11 +47,7 @@ describe('startup spec', () => { } return state; }; - const { - GlobalStateProvider, - dispatch, - useGlobalState, - } = createStore(reducer, initialState); + const { dispatch, useGlobalState } = createStore(reducer, initialState); const Counter = ({ name }) => { dispatch({ type: 'setCounter', name, value: 9 }); const [value] = useGlobalState(name); @@ -63,10 +59,10 @@ describe('startup spec', () => { ); }; const App = () => ( - + - + ); const { getByTestId } = render(); expect(getByTestId('count1').innerHTML).toBe('9'); diff --git a/dist/index.d.ts b/dist/index.d.ts index 81641fd..736b351 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,10 +1,12 @@ -import { ComponentType, SetStateAction, Reducer } from 'react'; +import { SetStateAction, Reducer } from 'react'; type SetGlobalState = ( name: N, setStateAction: SetStateAction, ) => void; +type UseGlobalStateProvider = () => void; + type UseGlobalState = (name: N) => [ S[N], (setStateAction: SetStateAction) => void, @@ -13,7 +15,7 @@ type UseGlobalState = (name: N) => [ export type Dispatch = (action: A) => A; export type Store = { - GlobalStateProvider: ComponentType; + useGlobalStateProvider: UseGlobalStateProvider; useGlobalState: UseGlobalState; getState: () => S; dispatch: Dispatch; @@ -26,7 +28,7 @@ export type Enhancer = (creator: StoreCreator) => StoreCreator type AnyEnhancer = unknown; export type CreateGlobalState = (initialState: S) => { - GlobalStateProvider: ComponentType; + useGlobalStateProvider: UseGlobalStateProvider; useGlobalState: UseGlobalState; setGlobalState: SetGlobalState; getGlobalState: (name: N) => S[N]; diff --git a/dist/index.js b/dist/index.js index d6eba8b..2ce688e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7,12 +7,6 @@ exports.createStore = exports.createGlobalState = void 0; var _react = require("react"); -function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } - -function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } @@ -21,6 +15,12 @@ function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + // utility functions var isFunction = function isFunction(fn) { return typeof fn === 'function'; @@ -32,24 +32,15 @@ var updateValue = function updateValue(oldValue, newValue) { } return newValue; -}; // ref: https://github.com/dai-shi/react-hooks-global-state/issues/5 - - -var useUnstableContextWithoutWarning = function useUnstableContextWithoutWarning(Context, observedBits) { - var ReactCurrentDispatcher = _react.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher; - var dispatcher = ReactCurrentDispatcher.current; - - if (!dispatcher) { - throw new Error('Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)'); - } - - return dispatcher.useContext(Context, observedBits); }; // core functions -var EMPTY_OBJECT = {}; -var UPDATE_STATE = Symbol('UPDATE_STATE'); -var PROP_GLOBAL_STATE_PROVIDER = 'p'; +var UPDATE_STATE = process.env.NODE_ENV !== 'production' ? Symbol('UPDATE_STATE') +/* for production */ +: Symbol(); +var PROP_UPDATER = 'r'; +var PROP_STATE = 'e'; +var PROP_USE_GLOBAL_STATE_PROVIDER = 'p'; var PROP_SET_GLOBAL_STATE = 's'; var PROP_USE_GLOBAL_STATE = 'u'; var PROP_GET_GLOBAL_STATE = 'g'; @@ -58,71 +49,24 @@ var PROP_SET_WHOLE_STATE = 'i'; var PROP_DISPATCH_ACTION = 'd'; var createGlobalStateCommon = function createGlobalStateCommon(reducer, initialState) { - var _ref2; + var _ref; var keys = Object.keys(initialState); - var wholeState = initialState; - var listener = null; + var globalState = initialState; + var linkedDispatch = null; + var listeners = {}; + keys.forEach(function (key) { + listeners[key] = new Set(); + }); var patchedReducer = function patchedReducer(state, action) { if (action.type === UPDATE_STATE) { - return action.updater(state); + return action[PROP_UPDATER] ? action[PROP_UPDATER](state) : action[PROP_STATE]; } return reducer(state, action); }; - var calculateChangedBits = function calculateChangedBits(a, b) { - var bits = 0; - keys.forEach(function (k, i) { - if (a[k] !== b[k]) bits |= 1 << i; - }); - return bits; - }; - - var Context = (0, _react.createContext)(EMPTY_OBJECT, calculateChangedBits); - - var GlobalStateProvider = function GlobalStateProvider(_ref) { - var children = _ref.children; - - var _useReducer = (0, _react.useReducer)(patchedReducer, initialState), - _useReducer2 = _slicedToArray(_useReducer, 2), - state = _useReducer2[0], - dispatch = _useReducer2[1]; - - (0, _react.useEffect)(function () { - if (listener) throw new Error('You cannot use more than once.'); - listener = dispatch; - - if (state !== initialState) { - // probably state was saved by react-hot-loader, so restore it - wholeState = state; - } else if (state !== wholeState) { - // wholeState was updated during initialization - dispatch({ - type: UPDATE_STATE, - updater: function updater() { - return wholeState; - } - }); - } - - var cleanup = function cleanup() { - listener = null; - }; - - return cleanup; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialState]); // trick for react-hot-loader - - (0, _react.useEffect)(function () { - // store the latest state - wholeState = state; - }); - return (0, _react.createElement)(Context.Provider, { - value: state - }, children); - }; - var validateName = function validateName(name) { if (!keys.includes(name)) { throw new Error("'".concat(name, "' not found. It must be provided in initialState as a property key.")); @@ -138,29 +82,83 @@ var createGlobalStateCommon = function createGlobalStateCommon(reducer, initialS return _objectSpread({}, previousState, _defineProperty({}, name, updateValue(previousState[name], update))); }; - if (listener) { - listener({ - type: UPDATE_STATE, - updater: updater - }); + if (linkedDispatch) { + linkedDispatch(_defineProperty({ + type: UPDATE_STATE + }, PROP_UPDATER, updater)); } else { - wholeState = updater(wholeState); + globalState = updater(globalState); + var nextPartialState = globalState[name]; + listeners[name].forEach(function (listener) { + return listener(nextPartialState); + }); } }; + var notifyListeners = function notifyListeners(prevState, nextState) { + keys.forEach(function (key) { + var nextPartialState = nextState[key]; + + if (prevState[key] !== nextPartialState) { + listeners[key].forEach(function (listener) { + return listener(nextPartialState); + }); + } + }); + }; + + var useGlobalStateProvider = function useGlobalStateProvider() { + var _useReducer = (0, _react.useReducer)(patchedReducer, globalState), + _useReducer2 = _slicedToArray(_useReducer, 2), + state = _useReducer2[0], + dispatch = _useReducer2[1]; + + (0, _react.useEffect)(function () { + if (linkedDispatch) throw new Error('Only one global state provider is allowed'); + linkedDispatch = dispatch; // in case it's changed before this effect is handled + + dispatch(_defineProperty({ + type: UPDATE_STATE + }, PROP_STATE, globalState)); + + var cleanup = function cleanup() { + linkedDispatch = null; + }; + + return cleanup; + }, []); + var prevGlobalState = (0, _react.useRef)(state); + notifyListeners(prevGlobalState.current, state); + prevGlobalState.current = state; + (0, _react.useEffect)(function () { + globalState = state; + }, [state]); + }; + var useGlobalState = function useGlobalState(name) { if (process.env.NODE_ENV !== 'production') { validateName(name); } - var index = keys.indexOf(name); - var observedBits = 1 << index; - var state = useUnstableContextWithoutWarning(Context, observedBits); - if (state === EMPTY_OBJECT) throw new Error('Please use '); + var _useState = (0, _react.useState)(globalState[name]), + _useState2 = _slicedToArray(_useState, 2), + partialState = _useState2[0], + setPartialState = _useState2[1]; + + (0, _react.useEffect)(function () { + listeners[name].add(setPartialState); + setPartialState(globalState[name]); // in case it's changed before this effect is handled + + var cleanup = function cleanup() { + listeners[name]["delete"](setPartialState); + }; + + return cleanup; + }, [name]); var updater = (0, _react.useCallback)(function (u) { return setGlobalState(name, u); }, [name]); - return [state[name], updater]; + return [partialState, updater]; }; var getGlobalState = function getGlobalState(name) { @@ -168,37 +166,38 @@ var createGlobalStateCommon = function createGlobalStateCommon(reducer, initialS validateName(name); } - return wholeState[name]; + return globalState[name]; }; var getWholeState = function getWholeState() { - return wholeState; + return globalState; }; - var setWholeState = function setWholeState(state) { - if (listener) { - listener({ - type: UPDATE_STATE, - updater: function updater() { - return state; - } - }); + var setWholeState = function setWholeState(nextGlobalState) { + if (linkedDispatch) { + linkedDispatch(_defineProperty({ + type: UPDATE_STATE + }, PROP_STATE, nextGlobalState)); } else { - wholeState = state; + var prevGlobalState = globalState; + globalState = nextGlobalState; + notifyListeners(prevGlobalState, globalState); } }; var dispatchAction = function dispatchAction(action) { - if (listener) { - listener(action); + if (linkedDispatch) { + linkedDispatch(action); } else { - wholeState = reducer(wholeState, action); + var prevGlobalState = globalState; + globalState = reducer(globalState, action); + notifyListeners(prevGlobalState, globalState); } return action; }; - return _ref2 = {}, _defineProperty(_ref2, PROP_GLOBAL_STATE_PROVIDER, GlobalStateProvider), _defineProperty(_ref2, PROP_SET_GLOBAL_STATE, setGlobalState), _defineProperty(_ref2, PROP_USE_GLOBAL_STATE, useGlobalState), _defineProperty(_ref2, PROP_GET_GLOBAL_STATE, getGlobalState), _defineProperty(_ref2, PROP_GET_WHOLE_STATE, getWholeState), _defineProperty(_ref2, PROP_SET_WHOLE_STATE, setWholeState), _defineProperty(_ref2, PROP_DISPATCH_ACTION, dispatchAction), _ref2; + return _ref = {}, _defineProperty(_ref, PROP_USE_GLOBAL_STATE_PROVIDER, useGlobalStateProvider), _defineProperty(_ref, PROP_SET_GLOBAL_STATE, setGlobalState), _defineProperty(_ref, PROP_USE_GLOBAL_STATE, useGlobalState), _defineProperty(_ref, PROP_GET_GLOBAL_STATE, getGlobalState), _defineProperty(_ref, PROP_GET_WHOLE_STATE, getWholeState), _defineProperty(_ref, PROP_SET_WHOLE_STATE, setWholeState), _defineProperty(_ref, PROP_DISPATCH_ACTION, dispatchAction), _ref; }; // export functions @@ -206,13 +205,13 @@ var createGlobalState = function createGlobalState(initialState) { var _createGlobalStateCom = createGlobalStateCommon(function (state) { return state; }, initialState), - GlobalStateProvider = _createGlobalStateCom[PROP_GLOBAL_STATE_PROVIDER], + useGlobalStateProvider = _createGlobalStateCom[PROP_USE_GLOBAL_STATE_PROVIDER], useGlobalState = _createGlobalStateCom[PROP_USE_GLOBAL_STATE], setGlobalState = _createGlobalStateCom[PROP_SET_GLOBAL_STATE], getGlobalState = _createGlobalStateCom[PROP_GET_GLOBAL_STATE]; return { - GlobalStateProvider: GlobalStateProvider, + useGlobalStateProvider: useGlobalStateProvider, useGlobalState: useGlobalState, setGlobalState: setGlobalState, getGlobalState: getGlobalState @@ -228,14 +227,14 @@ var createStore = function createStore(reducer, initialState, enhancer) { if (enhancer) return enhancer(createStore)(reducer, initialState); var _createGlobalStateCom2 = createGlobalStateCommon(reducer, initialState), - GlobalStateProvider = _createGlobalStateCom2[PROP_GLOBAL_STATE_PROVIDER], + useGlobalStateProvider = _createGlobalStateCom2[PROP_USE_GLOBAL_STATE_PROVIDER], useGlobalState = _createGlobalStateCom2[PROP_USE_GLOBAL_STATE], getWholeState = _createGlobalStateCom2[PROP_GET_WHOLE_STATE], setWholeState = _createGlobalStateCom2[PROP_SET_WHOLE_STATE], dispatchAction = _createGlobalStateCom2[PROP_DISPATCH_ACTION]; return { - GlobalStateProvider: GlobalStateProvider, + useGlobalStateProvider: useGlobalStateProvider, useGlobalState: useGlobalState, getState: getWholeState, setState: setWholeState, diff --git a/examples/01_minimal/src/index.js b/examples/01_minimal/src/index.js index 59d85ab..829353e 100644 --- a/examples/01_minimal/src/index.js +++ b/examples/01_minimal/src/index.js @@ -7,7 +7,7 @@ const initialState = { count: 0, text: 'hello', }; -const { GlobalStateProvider, useGlobalState } = createGlobalState(initialState); +const { useGlobalState } = createGlobalState(initialState); const Counter = () => { const [value, update] = useGlobalState('count'); @@ -32,14 +32,12 @@ const TextBox = () => { const App = () => ( - -

Counter

- - -

TextBox

- - -
+

Counter

+ + +

TextBox

+ +
); diff --git a/examples/02_typescript/src/App.tsx b/examples/02_typescript/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/02_typescript/src/App.tsx +++ b/examples/02_typescript/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/02_typescript/src/state.ts b/examples/02_typescript/src/state.ts index b1df74f..bc098ce 100644 --- a/examples/02_typescript/src/state.ts +++ b/examples/02_typescript/src/state.ts @@ -1,6 +1,6 @@ import { createGlobalState } from 'react-hooks-global-state'; -export const { GlobalStateProvider, useGlobalState } = createGlobalState({ +export const { useGlobalState } = createGlobalState({ count: 0, person: { age: 0, diff --git a/examples/03_actions/src/App.tsx b/examples/03_actions/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/03_actions/src/App.tsx +++ b/examples/03_actions/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/03_actions/src/state.ts b/examples/03_actions/src/state.ts index 0d39f25..f1748e1 100644 --- a/examples/03_actions/src/state.ts +++ b/examples/03_actions/src/state.ts @@ -1,6 +1,6 @@ import { createGlobalState } from 'react-hooks-global-state'; -const { GlobalStateProvider, setGlobalState, useGlobalState } = createGlobalState({ +const { setGlobalState, useGlobalState } = createGlobalState({ count: 0, person: { age: 0, @@ -29,4 +29,4 @@ export const setPersonAge = (age: number) => { setGlobalState('person', (v) => ({ ...v, age })); }; -export { GlobalStateProvider, useGlobalState }; +export { useGlobalState }; diff --git a/examples/04_fetch/src/App.tsx b/examples/04_fetch/src/App.tsx index b7a904b..8ba0da9 100644 --- a/examples/04_fetch/src/App.tsx +++ b/examples/04_fetch/src/App.tsx @@ -1,18 +1,14 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import ErrorMessage from './ErrorMessage'; import PageInfo from './PageInfo'; import RandomButton from './RandomButton'; const App = () => ( - - - - - + + + ); diff --git a/examples/04_fetch/src/state.ts b/examples/04_fetch/src/state.ts index 7806678..fbe47b6 100644 --- a/examples/04_fetch/src/state.ts +++ b/examples/04_fetch/src/state.ts @@ -1,6 +1,6 @@ import { createGlobalState } from 'react-hooks-global-state'; -const { GlobalStateProvider, setGlobalState, useGlobalState } = createGlobalState({ +const { setGlobalState, useGlobalState } = createGlobalState({ errorMessage: '', pageTitle: '', }); @@ -13,4 +13,4 @@ export const setPageTitle = (s: string) => { setGlobalState('pageTitle', s); }; -export { GlobalStateProvider, useGlobalState }; +export { useGlobalState }; diff --git a/examples/05_onmount/src/App.tsx b/examples/05_onmount/src/App.tsx index e7b8351..c90d874 100644 --- a/examples/05_onmount/src/App.tsx +++ b/examples/05_onmount/src/App.tsx @@ -1,6 +1,6 @@ import React, { useEffect, StrictMode } from 'react'; -import { GlobalStateProvider, setPageTitle } from './state'; +import { setPageTitle } from './state'; import ErrorMessage from './ErrorMessage'; import PageInfo from './PageInfo'; @@ -23,11 +23,9 @@ const App = () => { return ( - - - - - + + + ); }; diff --git a/examples/05_onmount/src/state.ts b/examples/05_onmount/src/state.ts index 7806678..fbe47b6 100644 --- a/examples/05_onmount/src/state.ts +++ b/examples/05_onmount/src/state.ts @@ -1,6 +1,6 @@ import { createGlobalState } from 'react-hooks-global-state'; -const { GlobalStateProvider, setGlobalState, useGlobalState } = createGlobalState({ +const { setGlobalState, useGlobalState } = createGlobalState({ errorMessage: '', pageTitle: '', }); @@ -13,4 +13,4 @@ export const setPageTitle = (s: string) => { setGlobalState('pageTitle', s); }; -export { GlobalStateProvider, useGlobalState }; +export { useGlobalState }; diff --git a/examples/06_reducer/src/App.tsx b/examples/06_reducer/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/06_reducer/src/App.tsx +++ b/examples/06_reducer/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/06_reducer/src/state.ts b/examples/06_reducer/src/state.ts index ac9c037..56bed8e 100644 --- a/examples/06_reducer/src/state.ts +++ b/examples/06_reducer/src/state.ts @@ -7,7 +7,7 @@ type Action = | { type: 'setLastName'; lastName: string } | { type: 'setAge'; age: number }; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore( +export const { dispatch, useGlobalState } = createStore( (state, action: Action) => { switch (action.type) { case 'increment': return { diff --git a/examples/07_middleware/src/App.tsx b/examples/07_middleware/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/07_middleware/src/App.tsx +++ b/examples/07_middleware/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/07_middleware/src/state.ts b/examples/07_middleware/src/state.ts index a474c4f..2fb0a7b 100644 --- a/examples/07_middleware/src/state.ts +++ b/examples/07_middleware/src/state.ts @@ -62,7 +62,7 @@ const logger = ( return returnValue; }; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore( +export const { dispatch, useGlobalState } = createStore( reducer, initialState, applyMiddleware(logger), diff --git a/examples/08_thunk/src/App.tsx b/examples/08_thunk/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/08_thunk/src/App.tsx +++ b/examples/08_thunk/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/08_thunk/src/state.ts b/examples/08_thunk/src/state.ts index 8b11238..772aeb9 100644 --- a/examples/08_thunk/src/state.ts +++ b/examples/08_thunk/src/state.ts @@ -52,7 +52,7 @@ const reducer = combineReducers({ person: personReducer, }); -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore< +export const { dispatch, useGlobalState } = createStore< typeof initialState, Action >( diff --git a/examples/09_comparison/src/App.tsx b/examples/09_comparison/src/App.tsx index a5f64ba..09a5cd4 100644 --- a/examples/09_comparison/src/App.tsx +++ b/examples/09_comparison/src/App.tsx @@ -2,7 +2,6 @@ import React, { StrictMode } from 'react'; import Counter from './Counter'; import Person from './Person'; -import { GlobalStateProvider } from './state'; import Counter2 from './Counter2'; import Person2 from './Person2'; @@ -24,14 +23,12 @@ const App = () => (

react-hooks-global-state

- -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
diff --git a/examples/09_comparison/src/state.ts b/examples/09_comparison/src/state.ts index 5b452ee..c41b0a9 100644 --- a/examples/09_comparison/src/state.ts +++ b/examples/09_comparison/src/state.ts @@ -2,4 +2,4 @@ import { createStore } from 'react-hooks-global-state'; import { initialState, reducer } from './common'; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore(reducer, initialState); +export const { dispatch, useGlobalState } = createStore(reducer, initialState); diff --git a/examples/10_immer/src/App.tsx b/examples/10_immer/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/10_immer/src/App.tsx +++ b/examples/10_immer/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/10_immer/src/state.ts b/examples/10_immer/src/state.ts index 156adf9..4e50d41 100644 --- a/examples/10_immer/src/state.ts +++ b/examples/10_immer/src/state.ts @@ -9,7 +9,7 @@ type Action = | { type: 'setLastName'; lastName: string } | { type: 'setAge'; age: number }; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore( +export const { dispatch, useGlobalState } = createStore( (state, action: Action) => produce(state, (draft) => { switch (action.type) { case 'increment': draft.count += 1; break; diff --git a/examples/11_deep/src/App.tsx b/examples/11_deep/src/App.tsx index 9209fcd..3dfeda3 100644 --- a/examples/11_deep/src/App.tsx +++ b/examples/11_deep/src/App.tsx @@ -1,20 +1,16 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/11_deep/src/state.ts b/examples/11_deep/src/state.ts index ac9c037..56bed8e 100644 --- a/examples/11_deep/src/state.ts +++ b/examples/11_deep/src/state.ts @@ -7,7 +7,7 @@ type Action = | { type: 'setLastName'; lastName: string } | { type: 'setAge'; age: number }; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore( +export const { dispatch, useGlobalState } = createStore( (state, action: Action) => { switch (action.type) { case 'increment': return { diff --git a/examples/12_effect/src/App.tsx b/examples/12_effect/src/App.tsx index 8a8f2de..12589f5 100644 --- a/examples/12_effect/src/App.tsx +++ b/examples/12_effect/src/App.tsx @@ -1,16 +1,12 @@ import React, { StrictMode } from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; const App = () => ( - -

Counter

- - -
+

Counter

+ +
); diff --git a/examples/12_effect/src/state.ts b/examples/12_effect/src/state.ts index e6e402c..46c10d5 100644 --- a/examples/12_effect/src/state.ts +++ b/examples/12_effect/src/state.ts @@ -5,7 +5,7 @@ type Action = | { type: 'decrement' } | { type: 'addBonus'; value: number }; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore( +export const { dispatch, useGlobalState } = createStore( (state, action: Action) => { switch (action.type) { case 'increment': return { diff --git a/examples/13_persistence/src/App.tsx b/examples/13_persistence/src/App.tsx index ef27cfd..12f0f00 100644 --- a/examples/13_persistence/src/App.tsx +++ b/examples/13_persistence/src/App.tsx @@ -1,20 +1,16 @@ import React from 'react'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/13_persistence/src/state.ts b/examples/13_persistence/src/state.ts index a63100e..88d1de8 100644 --- a/examples/13_persistence/src/state.ts +++ b/examples/13_persistence/src/state.ts @@ -103,7 +103,7 @@ const saveStateToStorage = ( return returnValue; }; -export const { GlobalStateProvider, dispatch, useGlobalState } = createStore( +export const { dispatch, useGlobalState } = createStore( reducer, initialState, applyMiddleware(saveStateToStorage), diff --git a/examples/14_hotloader/src/App.tsx b/examples/14_hotloader/src/App.tsx index 49fc095..06a95db 100644 --- a/examples/14_hotloader/src/App.tsx +++ b/examples/14_hotloader/src/App.tsx @@ -1,21 +1,17 @@ import React, { StrictMode } from 'react'; import { hot } from 'react-hot-loader/root'; -import { GlobalStateProvider } from './state'; - import Counter from './Counter'; import Person from './Person'; const App = () => ( - -

Counter

- - -

Person

- - -
+

Counter

+ + +

Person

+ +
); diff --git a/examples/14_hotloader/src/state.ts b/examples/14_hotloader/src/state.ts index b1df74f..bc098ce 100644 --- a/examples/14_hotloader/src/state.ts +++ b/examples/14_hotloader/src/state.ts @@ -1,6 +1,6 @@ import { createGlobalState } from 'react-hooks-global-state'; -export const { GlobalStateProvider, useGlobalState } = createGlobalState({ +export const { useGlobalState } = createGlobalState({ count: 0, person: { age: 0, diff --git a/package-lock.json b/package-lock.json index 4fac38f..30e2fe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-hooks-global-state", - "version": "0.17.0", + "version": "1.0.0-alpha.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7490fe8..486694d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-hooks-global-state", "description": "Simple global state for React with Hooks API", - "version": "0.17.0", + "version": "1.0.0-alpha.1", "author": "Daishi Kato", "repository": { "type": "git", diff --git a/src/index.d.ts b/src/index.d.ts index 81641fd..736b351 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,10 +1,12 @@ -import { ComponentType, SetStateAction, Reducer } from 'react'; +import { SetStateAction, Reducer } from 'react'; type SetGlobalState = ( name: N, setStateAction: SetStateAction, ) => void; +type UseGlobalStateProvider = () => void; + type UseGlobalState = (name: N) => [ S[N], (setStateAction: SetStateAction) => void, @@ -13,7 +15,7 @@ type UseGlobalState = (name: N) => [ export type Dispatch
= (action: A) => A; export type Store = { - GlobalStateProvider: ComponentType; + useGlobalStateProvider: UseGlobalStateProvider; useGlobalState: UseGlobalState; getState: () => S; dispatch: Dispatch; @@ -26,7 +28,7 @@ export type Enhancer = (creator: StoreCreator) => StoreCreator type AnyEnhancer = unknown; export type CreateGlobalState = (initialState: S) => { - GlobalStateProvider: ComponentType; + useGlobalStateProvider: UseGlobalStateProvider; useGlobalState: UseGlobalState; setGlobalState: SetGlobalState; getGlobalState: (name: N) => S[N]; diff --git a/src/index.js b/src/index.js index 3322969..f6ed6f9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,9 @@ import { - createContext, - createElement, useCallback, useEffect, + useRef, useReducer, - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + useState, } from 'react'; // utility functions @@ -18,20 +17,8 @@ const updateValue = (oldValue, newValue) => { return newValue; }; -// ref: https://github.com/dai-shi/react-hooks-global-state/issues/5 -const useUnstableContextWithoutWarning = (Context, observedBits) => { - const { ReactCurrentDispatcher } = __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; - const dispatcher = ReactCurrentDispatcher.current; - if (!dispatcher) { - throw new Error('Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)'); - } - return dispatcher.useContext(Context, observedBits); -}; - // core functions -const EMPTY_OBJECT = {}; - const UPDATE_STATE = ( process.env.NODE_ENV !== 'production' ? Symbol('UPDATE_STATE') /* for production */ : Symbol() @@ -40,7 +27,7 @@ const UPDATE_STATE = ( const PROP_UPDATER = 'r'; const PROP_STATE = 'e'; -const PROP_GLOBAL_STATE_PROVIDER = 'p'; +const PROP_USE_GLOBAL_STATE_PROVIDER = 'p'; const PROP_SET_GLOBAL_STATE = 's'; const PROP_USE_GLOBAL_STATE = 'u'; const PROP_GET_GLOBAL_STATE = 'g'; @@ -50,8 +37,12 @@ const PROP_DISPATCH_ACTION = 'd'; const createGlobalStateCommon = (reducer, initialState) => { const keys = Object.keys(initialState); - let wholeState = initialState; - let listener = null; + let globalState = initialState; + let linkedDispatch = null; + const listeners = {}; + keys.forEach((key) => { + listeners[key] = new Set(); + }); const patchedReducer = (state, action) => { if (action.type === UPDATE_STATE) { @@ -60,41 +51,6 @@ const createGlobalStateCommon = (reducer, initialState) => { return reducer(state, action); }; - const calculateChangedBits = (a, b) => { - let bits = 0; - keys.forEach((k, i) => { - if (a[k] !== b[k]) bits |= 1 << i; - }); - return bits; - }; - - const Context = createContext(EMPTY_OBJECT, calculateChangedBits); - - const GlobalStateProvider = ({ children }) => { - const [state, dispatch] = useReducer(patchedReducer, initialState); - useEffect(() => { - if (listener) throw new Error('You cannot use more than once.'); - listener = dispatch; - if (state !== initialState) { - // probably state was saved by react-hot-loader, so restore it - wholeState = state; - } else if (state !== wholeState) { - // wholeState was updated during initialization - dispatch({ type: UPDATE_STATE, [PROP_STATE]: wholeState }); - } - const cleanup = () => { - listener = null; - }; - return cleanup; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialState]); // trick for react-hot-loader - useEffect(() => { - // store the latest state - wholeState = state; - }); - return createElement(Context.Provider, { value: state }, children); - }; - const validateName = (name) => { if (!keys.includes(name)) { throw new Error(`'${name}' not found. It must be provided in initialState as a property key.`); @@ -109,53 +65,93 @@ const createGlobalStateCommon = (reducer, initialState) => { ...previousState, [name]: updateValue(previousState[name], update), }); - if (listener) { - listener({ type: UPDATE_STATE, [PROP_UPDATER]: updater }); + if (linkedDispatch) { + linkedDispatch({ type: UPDATE_STATE, [PROP_UPDATER]: updater }); } else { - wholeState = updater(wholeState); + globalState = updater(globalState); + const nextPartialState = globalState[name]; + listeners[name].forEach((listener) => listener(nextPartialState)); } }; + const notifyListeners = (prevState, nextState) => { + keys.forEach((key) => { + const nextPartialState = nextState[key]; + if (prevState[key] !== nextPartialState) { + listeners[key].forEach((listener) => listener(nextPartialState)); + } + }); + }; + + const useGlobalStateProvider = () => { + const [state, dispatch] = useReducer(patchedReducer, globalState); + useEffect(() => { + if (linkedDispatch) throw new Error('Only one global state provider is allowed'); + linkedDispatch = dispatch; + // in case it's changed before this effect is handled + dispatch({ type: UPDATE_STATE, [PROP_STATE]: globalState }); + const cleanup = () => { + linkedDispatch = null; + }; + return cleanup; + }, []); + const prevGlobalState = useRef(state); + notifyListeners(prevGlobalState.current, state); + prevGlobalState.current = state; + useEffect(() => { + globalState = state; + }, [state]); + }; + const useGlobalState = (name) => { if (process.env.NODE_ENV !== 'production') { validateName(name); } - const index = keys.indexOf(name); - const observedBits = 1 << index; - const state = useUnstableContextWithoutWarning(Context, observedBits); - if (state === EMPTY_OBJECT) throw new Error('Please use '); + const [partialState, setPartialState] = useState(globalState[name]); + useEffect(() => { + listeners[name].add(setPartialState); + setPartialState(globalState[name]); // in case it's changed before this effect is handled + const cleanup = () => { + listeners[name].delete(setPartialState); + }; + return cleanup; + }, [name]); const updater = useCallback((u) => setGlobalState(name, u), [name]); - return [state[name], updater]; + return [partialState, updater]; }; const getGlobalState = (name) => { if (process.env.NODE_ENV !== 'production') { validateName(name); } - return wholeState[name]; + return globalState[name]; }; - const getWholeState = () => wholeState; + const getWholeState = () => globalState; - const setWholeState = (state) => { - if (listener) { - listener({ type: UPDATE_STATE, [PROP_STATE]: state }); + const setWholeState = (nextGlobalState) => { + if (linkedDispatch) { + linkedDispatch({ type: UPDATE_STATE, [PROP_STATE]: nextGlobalState }); } else { - wholeState = state; + const prevGlobalState = globalState; + globalState = nextGlobalState; + notifyListeners(prevGlobalState, globalState); } }; const dispatchAction = (action) => { - if (listener) { - listener(action); + if (linkedDispatch) { + linkedDispatch(action); } else { - wholeState = reducer(wholeState, action); + const prevGlobalState = globalState; + globalState = reducer(globalState, action); + notifyListeners(prevGlobalState, globalState); } return action; }; return { - [PROP_GLOBAL_STATE_PROVIDER]: GlobalStateProvider, + [PROP_USE_GLOBAL_STATE_PROVIDER]: useGlobalStateProvider, [PROP_SET_GLOBAL_STATE]: setGlobalState, [PROP_USE_GLOBAL_STATE]: useGlobalState, [PROP_GET_GLOBAL_STATE]: getGlobalState, @@ -169,13 +165,13 @@ const createGlobalStateCommon = (reducer, initialState) => { export const createGlobalState = (initialState) => { const { - [PROP_GLOBAL_STATE_PROVIDER]: GlobalStateProvider, + [PROP_USE_GLOBAL_STATE_PROVIDER]: useGlobalStateProvider, [PROP_USE_GLOBAL_STATE]: useGlobalState, [PROP_SET_GLOBAL_STATE]: setGlobalState, [PROP_GET_GLOBAL_STATE]: getGlobalState, } = createGlobalStateCommon((state) => state, initialState); return { - GlobalStateProvider, + useGlobalStateProvider, useGlobalState, setGlobalState, getGlobalState, @@ -186,14 +182,14 @@ export const createStore = (reducer, initialState, enhancer) => { if (!initialState) initialState = reducer(undefined, { type: undefined }); if (enhancer) return enhancer(createStore)(reducer, initialState); const { - [PROP_GLOBAL_STATE_PROVIDER]: GlobalStateProvider, + [PROP_USE_GLOBAL_STATE_PROVIDER]: useGlobalStateProvider, [PROP_USE_GLOBAL_STATE]: useGlobalState, [PROP_GET_WHOLE_STATE]: getWholeState, [PROP_SET_WHOLE_STATE]: setWholeState, [PROP_DISPATCH_ACTION]: dispatchAction, } = createGlobalStateCommon(reducer, initialState); return { - GlobalStateProvider, + useGlobalStateProvider, useGlobalState, getState: getWholeState, setState: setWholeState, // for devtools.js diff --git a/tsconfig.json b/tsconfig.json index ddceef0..c535bb9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "sourceMap": true, "baseUrl": ".", "paths": { - "react-hooks-global-state": ["."], + "react-hooks-global-state": ["./src"], "react-hooks-global-state/src/devtools": ["./src/devtools"] } } diff --git a/webpack.config.js b/webpack.config.js index 591283f..eec99d3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,7 +38,8 @@ module.exports = { resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], alias: { - 'react-hooks-global-state': __dirname, + 'react-hooks-global-state/src/devtools': `${__dirname}/src/devtools`, + 'react-hooks-global-state': `${__dirname}/src`, }, }, devServer: {