Repository: streamich/react-use
Pull request #850 (2019-12-26 11:11:25)

feat: add global state hook
# Description

<!-- Please include a summary of the change along with relevant motivation and context. -->


## 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
- [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/README.md b/README.md
index 6b65d8a3..3a4cca8d 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,7 @@
   - [`useMediatedState`](./docs/useMediatedState.md) &mdash; like the regular `useState` but with mediation by custom function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemediatedstate--demo)
   - [`useFirstMountState`](./docs/useFirstMountState.md) &mdash; check if current render is first. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usefirstmountstate--demo)
   - [`useRendersCount`](./docs/useRendersCount.md) &mdash; count component renders. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userenderscount--demo)
+  - [`createGlobalState`](./docs/createGlobalState.md) &mdash; cross component shared state.[![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-createglobalstate--demo)
     <br/>
     <br/>
 - [**Miscellaneous**]()
diff --git a/docs/createGlobalState.md b/docs/createGlobalState.md
new file mode 100644
index 00000000..b7451765
--- /dev/null
+++ b/docs/createGlobalState.md
@@ -0,0 +1,32 @@
+# `useGlobalState`
+
+A React hook which creates a globally shared state.
+
+## Usage
+
+```tsx
+const useGlobalValue = createGlobalState<number>(0);
+
+const CompA: FC = () => {
+  const [value, setValue] = useGlobalValue();
+
+  return <button onClick={() => setValue(value + 1)}>+</button>;
+};
+
+const CompB: FC = () => {
+  const [value, setValue] = useGlobalValue();
+
+  return <button onClick={() => setValue(value - 1)}>-</button>;
+};
+
+const Demo: FC = () => {
+  const [value] = useGlobalValue();
+  return (
+    <div>
+      <p>{value}</p>
+      <CompA />
+      <CompB />
+    </div>
+  );
+};
+```
diff --git a/src/createGlobalState.ts b/src/createGlobalState.ts
new file mode 100644
index 00000000..b7cfa81c
--- /dev/null
+++ b/src/createGlobalState.ts
@@ -0,0 +1,31 @@
+import { useLayoutEffect, useState } from 'react';
+import useEffectOnce from './useEffectOnce';
+
+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;
+      store.setters.forEach(setter => setter(store.state));
+    },
+    setters: [],
+  };
+
+  return (): [S | undefined, (state: S) => void] => {
+    const [globalState, stateSetter] = useState<S | undefined>(store.state);
+
+    useEffectOnce(() => () => {
+      store.setters = store.setters.filter(setter => setter !== stateSetter);
+    });
+
+    useLayoutEffect(() => {
+      if (!store.setters.includes(stateSetter)) {
+        store.setters.push(stateSetter);
+      }
+    });
+
+    return [globalState, store.setState];
+  };
+}
+
+export default createGlobalState;
diff --git a/src/index.ts b/src/index.ts
index 9d78994a..1d991fa7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -100,3 +100,4 @@ export { default as useMeasure } from './useMeasure';
 export { useRendersCount } from './useRendersCount';
 export { useFirstMountState } from './useFirstMountState';
 export { default as useSet } from './useSet';
+export { createGlobalState } from './createGlobalState';
diff --git a/stories/createGlobalState.story.tsx b/stories/createGlobalState.story.tsx
new file mode 100644
index 00000000..ed4bdd84
--- /dev/null
+++ b/stories/createGlobalState.story.tsx
@@ -0,0 +1,33 @@
+import { storiesOf } from "@storybook/react";
+import React, { FC } from "react";
+import { createGlobalState } from "../src";
+import ShowDocs from "./util/ShowDocs";
+
+const useGlobalValue = createGlobalState<number>(0);
+
+const CompA: FC = () => {
+  const [value, setValue] = useGlobalValue();
+
+  return <button onClick={() => setValue(value + 1)}>+</button>;
+};
+
+const CompB: FC = () => {
+  const [value, setValue] = useGlobalValue();
+
+  return <button onClick={() => setValue(value - 1)}>-</button>;
+};
+
+const Demo: FC = () => {
+  const [value] = useGlobalValue();
+  return (
+    <div>
+      <p>{value}</p>
+      <CompA />
+      <CompB />
+    </div>
+  );
+};
+
+storiesOf("State|createGlobalState", module)
+  .add("Docs", () => <ShowDocs md={require("../docs/createGlobalState.md")} />)
+  .add("Demo", () => <Demo />);


Test patch:
diff --git a/tests/createGlobalState.test.ts b/tests/createGlobalState.test.ts
new file mode 100644
index 00000000..9609dc29
--- /dev/null
+++ b/tests/createGlobalState.test.ts
@@ -0,0 +1,21 @@
+import { act, renderHook } from '@testing-library/react-hooks';
+import createGlobalState from '../src/createGlobalState';
+
+describe('useGlobalState', () => {
+  it('should be defined', () => {
+    expect(createGlobalState).toBeDefined();
+  });
+
+  it('both components should be updated', () => {
+    const useGlobalValue = createGlobalState(0);
+    const { result: result1 } = renderHook(() => useGlobalValue());
+    const { result: result2 } = renderHook(() => useGlobalValue());
+    expect(result1.current[0] === 0);
+    expect(result2.current[0] === 0);
+    act(() => {
+      result1.current[1](1);
+    });
+    expect(result1.current[0] === 1);
+    expect(result2.current[0] === 1);
+  });
+});