Repository: streamich/react-use
Pull request #1819 (2021-02-15 20:11:11)

Makes createGlobalState API compatible with React's useState API
# Description

This PR makes the `createGlobalState` function compatible with the React API. This means 2 things:

- It allows using a function in setState, as in `setState(prevState => prevState + 1)`
- It allows using a function in the initial state, as in `createGlobalState(() => 0)`

Also, as a side effect:

- It improves the types when the initial value is passed or set as the generic argument, while relying on the React original type for the return (`SetStateAction`).
- It fixes the current tests that were using an incorrect syntax and extends them to the new use cases and types.
- Extends docs and fixes a minor grammar issue.

## Type of change

<!-- Check all relevant options. -->
- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [x] New feature _(non-breaking change which adds functionality)_
- [ ] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_

# Checklist
- [x] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md)
- [x] Perform a code self-review 
   We mobbed this in our team at Spotify.
- [x] Comment the code, particularly in hard-to-understand areas
- [x] Add documentation
- [x] Add hook's story at Storybook
- [x] Cover changes with tests
- [x] Ensure the test suite passes (`yarn test`)
- [x] Provide 100% tests coverage
- [x] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure.
- [x] Make sure types are fine (`yarn lint:types`).

<!-- If you can't check all the checkboxes right now - check what you can, create a Draft PR, make some changes if needed and get back to it when you will be able to put some marks in list. -->



Patch:
diff --git a/docs/createGlobalState.md b/docs/createGlobalState.md
index b7451765..0e028134 100644
--- a/docs/createGlobalState.md
+++ b/docs/createGlobalState.md
@@ -1,6 +1,6 @@
 # `useGlobalState`
 
-A React hook which creates a globally shared state.
+A React hook that creates a globally shared state.
 
 ## Usage
 
@@ -30,3 +30,32 @@ const Demo: FC = () => {
   );
 };
 ```
+
+It also allows initializing the state with a function and using a function in the setState:
+
+```tsx
+const useGlobalValue = createGlobalState<number>(() => 0);
+
+const CompA: FC = () => {
+  const [value, setValue] = useGlobalValue();
+
+  return <button onClick={() => setValue(value => value + 1)}>+</button>;
+};
+
+const CompB: FC = () => {
+  const [value, setValue] = useGlobalValue();
+
+  return <button onClick={() => setValue(value => value - 1)}>-</button>;
+};
+
+const Demo: FC = () => {
+  const [value] = useGlobalValue();
+  return (
+    <div>
+      <p>{value}</p>
+      <CompA />
+      <CompB />
+    </div>
+  );
+};
+```
diff --git a/src/factory/createGlobalState.ts b/src/factory/createGlobalState.ts
index dca16642..c06ba9a0 100644
--- a/src/factory/createGlobalState.ts
+++ b/src/factory/createGlobalState.ts
@@ -1,18 +1,31 @@
 import { useState } from 'react';
+import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from '../misc/hookState';
 import useEffectOnce from '../useEffectOnce';
 import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
 
-export function createGlobalState<S = any>(initialState?: S) {
-  const store: { state: S | undefined; setState: (state: S) => void; setters: any[] } = {
-    state: initialState,
-    setState(state: S) {
-      store.state = state;
+export function createGlobalState<S = any>(
+  initialState: IHookStateInitAction<S>
+): () => [S, (state: IHookStateSetAction<S>) => void];
+export function createGlobalState<S = undefined>(): () => [
+  S,
+  (state: IHookStateSetAction<S>) => void
+];
+
+export function createGlobalState<S>(initialState?: S) {
+  const store: {
+    state: S;
+    setState: (state: IHookStateSetAction<S>) => void;
+    setters: any[];
+  } = {
+    state: initialState instanceof Function ? initialState() : initialState,
+    setState(nextState: IHookStateSetAction<S>) {
+      store.state = resolveHookState(nextState, store.state);
       store.setters.forEach((setter) => setter(store.state));
     },
     setters: [],
   };
 
-  return (): [S | undefined, (state: S) => void] => {
+  return () => {
     const [globalState, stateSetter] = useState<S | undefined>(store.state);
 
     useEffectOnce(() => () => {


Test patch:
diff --git a/tests/createGlobalState.test.ts b/tests/createGlobalState.test.ts
index dce9e051..6c1945cd 100644
--- a/tests/createGlobalState.test.ts
+++ b/tests/createGlobalState.test.ts
@@ -10,12 +10,77 @@ describe('useGlobalState', () => {
     const useGlobalValue = createGlobalState(0);
     const { result: result1 } = renderHook(() => useGlobalValue());
     const { result: result2 } = renderHook(() => useGlobalValue());
-    expect(result1.current[0] === 0);
-    expect(result2.current[0] === 0);
+    expect(result1.current[0]).toBe(0);
+    expect(result2.current[0]).toBe(0);
     act(() => {
       result1.current[1](1);
     });
-    expect(result1.current[0] === 1);
-    expect(result2.current[0] === 1);
+    expect(result1.current[0]).toBe(1);
+    expect(result2.current[0]).toBe(1);
+  });
+
+  it('allows setting state with function and previous value', () => {
+    const useGlobalValue = createGlobalState(0);
+    const { result: result1 } = renderHook(() => useGlobalValue());
+    const { result: result2 } = renderHook(() => useGlobalValue());
+    expect(result1.current[0]).toBe(0);
+    expect(result2.current[0]).toBe(0);
+    act(() => {
+      result1.current[1]((value) => value + 1);
+    });
+    expect(result1.current[0]).toBe(1);
+    expect(result2.current[0]).toBe(1);
+  });
+
+  it('allows setting state with function and no previous value', () => {
+    const useGlobalValue = createGlobalState(0);
+    const { result: result1 } = renderHook(() => useGlobalValue());
+    const { result: result2 } = renderHook(() => useGlobalValue());
+    expect(result1.current[0]).toBe(0);
+    expect(result2.current[0]).toBe(0);
+    act(() => {
+      result1.current[1](() => 1);
+    });
+    expect(result1.current[0]).toBe(1);
+    expect(result2.current[0]).toBe(1);
+  });
+
+  it('initializes and updates with undefined', () => {
+    const useGlobalValue = createGlobalState<number>();
+    const { result: result1 } = renderHook(() => useGlobalValue());
+    const { result: result2 } = renderHook(() => useGlobalValue());
+    expect(result1.current[0]).toBe(undefined);
+    expect(result2.current[0]).toBe(undefined);
+    act(() => {
+      // this is the only case where types fail, it should guard the number
+      result1.current[1]((value) => value);
+    });
+    expect(result1.current[0]).toBe(undefined);
+    expect(result2.current[0]).toBe(undefined);
+  });
+
+  it('initializes with undefined and updates with different type', () => {
+    const useGlobalValue = createGlobalState();
+    const { result: result1 } = renderHook(() => useGlobalValue());
+    const { result: result2 } = renderHook(() => useGlobalValue());
+    expect(result1.current[0]).toBe(undefined);
+    expect(result2.current[0]).toBe(undefined);
+    act(() => {
+      // @ts-expect-error this case it checks correctly, hence the comment
+      result1.current[1]((value) => 1);
+    });
+  });
+
+  it('initializes with function', () => {
+    const useGlobalValue = createGlobalState(() => 0);
+    const { result: result1 } = renderHook(() => useGlobalValue());
+    const { result: result2 } = renderHook(() => useGlobalValue());
+    expect(result1.current[0]).toBe(0);
+    expect(result2.current[0]).toBe(0);
+    act(() => {
+      result1.current[1](1);
+    });
+    expect(result1.current[0]).toBe(1);
+    expect(result2.current[0]).toBe(1);
   });
 });