Skip to content

Commit

Permalink
feat(createGlobalState)!: Align API with React.useState
Browse files Browse the repository at this point in the history
This commit adds support for functional updates to align with the React.useState
API. Type definitions are updated to match useState.

This might break existing code that expects functions to be held in the global
state, rather than applied as an update.

This change may also cause new type errors.
  • Loading branch information
Nathan Spaeth committed Mar 5, 2020
1 parent 3fa2517 commit c10ad48
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 33 deletions.
51 changes: 41 additions & 10 deletions docs/createGlobalState.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,59 @@ A React hook which creates a globally shared state.

## Usage


```ts
const useGlobalState = createGlobalState<T>(defaultValue: T)
...
const [ value, setValue ] = useGlobalState(initialValue?: T)
```

Here you can see that there is both a `defaultValue` and an `initialValue`. Together, they dictate what the starting value will be.
* `defaultValue` is the value returned on the first use of the `useGlobalState()` hook when no `initialValue` is provided, and allows you to ensure that there will always be valid value available.
* `initialValue` allows overriding the `defaultValue` on the very first rendering of the hook within the app. This also allows the hook to be a drop in replacement for `React.useState`. If no `initialValue` is provided, the `defaultValue` will be used.

### Example:

In the following example, the starting value with be `1`, because it overrides the `defaultValue`, and because that invocation is rendered before the `useGlobalValue(2)`, and `useGlobalValue(3)`, whose `initialValue` parameters are ignored because they are not the first invocations.

```tsx
const useGlobalValue = createGlobalState<number>(0);
const useMyGlobalState = createGlobalState<number>(0);

const CompA: FC = () => {
const [value, setValue] = useGlobalValue();
const SetValueDirectly: FC = () => {
const [value, setValue] = useMyGlobalState(2);

return <button onClick={() => setValue(value + 1)}>+</button>;
return (
<div>
<p>{value}</p>
<div>
<button onClick={() => setValue(value + 1)}>+</button>
<button onClick={() => setValue(value - 1)}>-</button>
</div>
</div>
);
};

const CompB: FC = () => {
const [value, setValue] = useGlobalValue();
const SetValueWithFunctionalUpdate: FC = () => {
const [value, setValue] = useMyGlobalState(1);

return <button onClick={() => setValue(value - 1)}>-</button>;
return (
<div>
<p>{value}</p>
<div>
<button onClick={() => setValue(val => val + 1)}>+</button>
<button onClick={() => setValue(val => val - 1)}>-</button>
</div>
</div>
);
};

const Demo: FC = () => {
const [value] = useGlobalValue();
const [value] = useGlobalValue(1);
return (
<div>
<p>{value}</p>
<CompA />
<CompB />
<SetValueDirectly />
<SetValueWithFunctionalUpdate />
</div>
);
};
Expand Down
34 changes: 27 additions & 7 deletions src/createGlobalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,37 @@
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;
export type useStateReturnType<S> = [ S , React.Dispatch<React.SetStateAction<S>> ]
interface StateStore<S> {
initialized: boolean,
state: S
setState: React.Dispatch<React.SetStateAction<S>>
setters: React.Dispatch<React.SetStateAction<S>>[]
}
export type useGlobalStateType<S> = (initialState?: S | (() => S)) => useStateReturnType<S>
export function createGlobalState<S = any>(defaultState: S): useGlobalStateType<S> {
const store: StateStore<S> = {
initialized: false,
state: defaultState,
setState(newState) {
store.state = newState instanceof Function
? newState(store.state)
: newState;
store.setters.forEach(setter => setter(store.state));
},
setters: [],
};

return (): [S | undefined, (state: S) => void] => {
const [globalState, stateSetter] = useState<S | undefined>(store.state);
// unlike in useState, here initialState is optional because there will always
// be a valid state provided by defaultState
return function(initialState?: S | (() => S)): useStateReturnType<S> {
// Prevent clobbering defaultState or existing state if no argument was passed
if (!store.initialized && arguments.length > 0) {
store.state = initialState instanceof Function
? initialState()
: initialState as S
}
const [globalState, stateSetter] = useState<S>(store.state);

useEffectOnce(() => () => {
store.setters = store.setters.filter(setter => setter !== stateSetter);
Expand All @@ -25,6 +44,7 @@ export function createGlobalState<S = any>(initialState?: S) {
}
});

store.initialized = true
return [globalState, store.setState];
};
}
Expand Down
36 changes: 26 additions & 10 deletions stories/createGlobalState.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,43 @@ import React, { FC } from "react";
import { createGlobalState } from "../src";
import ShowDocs from "./util/ShowDocs";

const useGlobalValue = createGlobalState<number>(0);
const useMyGlobalState = createGlobalState<number>(0);

const CompA: FC = () => {
const [value, setValue] = useGlobalValue();
const SetValueDirectly: FC = () => {
const [value, setValue] = useMyGlobalState(2);

return <button onClick={() => setValue(value + 1)}>+</button>;
return (
<div>
<p>{value}</p>
<div>
<button onClick={() => setValue(value + 1)}>+</button>
<button onClick={() => setValue(value - 1)}>-</button>
</div>
</div>
);
};

const CompB: FC = () => {
const [value, setValue] = useGlobalValue();
const SetValueWithFunctionalUpdate: FC = () => {
const [value, setValue] = useMyGlobalState(3);

return <button onClick={() => setValue(value - 1)}>-</button>;
return (
<div>
<p>{value}</p>
<div>
<button onClick={() => setValue(val => val + 1)}>+</button>
<button onClick={() => setValue(val => val - 1)}>-</button>
</div>
</div>
);
};

const Demo: FC = () => {
const [value] = useGlobalValue();
const [value] = useMyGlobalState(1);
return (
<div>
<p>{value}</p>
<CompA />
<CompB />
<SetValueDirectly />
<SetValueWithFunctionalUpdate />
</div>
);
};
Expand Down
57 changes: 51 additions & 6 deletions tests/createGlobalState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,61 @@ describe('useGlobalState', () => {
expect(createGlobalState).toBeDefined();
});

it('both components should be updated', () => {
it('both components should be updated directly', () => {
const useGlobalValue = createGlobalState(0);
const { result: result1 } = renderHook(() => useGlobalValue());
const { result: result2 } = renderHook(() => useGlobalValue());
expect(result1.current[0] === 0);
expect(result2.current[0] === 0);
let [ state1, setState1 ] = result1.current;
let [ state2 ] = result2.current;
expect(state1 === 0);
expect(state2 === 0);
act(() => {
result1.current[1](1);
setState1(1);
});
expect(result1.current[0] === 1);
expect(result2.current[0] === 1);
[ state1, setState1 ] = result1.current;
[ state2 ] = result2.current;
expect(state1 === 1);
expect(state2 === 1);
});

it('both components should be updated via update function', () => {
const useGlobalValue = createGlobalState(0);
const { result: result1 } = renderHook(() => useGlobalValue());
const { result: result2 } = renderHook(() => useGlobalValue());
let [ state1, setState1 ] = result1.current;
let [ state2 ] = result2.current;
expect(state1 === 0);
expect(state2 === 0);
act(() => {
setState1(x => x + 1);
});
[ state1, setState1 ] = result1.current;
[ state2 ] = result2.current;
expect(state1 === 1);
expect(state2 === 1);
});

it('passing initialState should override defaultState on first use', () => {
const useGlobalValue = createGlobalState(0);
const { result } = renderHook(() => useGlobalValue(1));
let [ state ] = result.current;
expect(state === 1)
})

it('passing initialState should be ignored after first use', () => {
const useGlobalValue = createGlobalState(1);
const { result: result1 } = renderHook(() => useGlobalValue(2));
const { result: result2 } = renderHook(() => useGlobalValue(3));
let [ state1 ] = result1.current;
let [ state2 ] = result2.current;
expect(state1 === 2);
expect(state2 === 2);
})

it('an unpassed initialState should not clobber defaultState', () => {
const useGlobalValue = createGlobalState(1);
const { result } = renderHook(() => useGlobalValue());
let [ state ] = result.current;
expect(state === 1)
})
});

0 comments on commit c10ad48

Please sign in to comment.