react-shared-state-hook
Simple React shared state hook based on useState
- Installation
- Basic usage
- Example application setup
- Transforming data structures
- Mixing local state with shared state
Installation
npm i react-shared-state-hook
Basic usage
import React from 'react';
import { createSharedStateHook } from 'react-shared-state-hook';
const [useSharedState, setSharedState] = createSharedStateHook({ a: 1, b: 2 });
// setSharedState({ a: 1, b: 3 }); // update state and trigger re-render
const ComponentA = () => {
const state = useSharedState(); // re-render on any change
return (
<div>
{state.a} + {state.b}
</div>
);
};
const ComponentB = () => {
const b = useSharedState(o => o.b); // only re-render when b changes
return <div>{b}</div>;
};
Example application setup
// index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(React.createElement(App), document.getElementById('root'));
// App.tsx
import React from 'react';
import { useActions, useAppState } from './state';
export const App = () => (
<>
<UsernameControl />
<CountControl />
</>
);
const UsernameControl = () => {
const username = useAppState(o => o.username);
const actions = useActions();
return (
<div>
<p>Hello {username || '...'}</p>
<input value={username ?? ''} onChange={e => actions.setUsername(e.target.value)} />
</div>
);
};
const CountControl = () => {
const count = useAppState(o => o.count);
const actions = useActions();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => actions.increaseCount()}>+1</button>
</div>
);
};
// state.ts
import { createSharedStateHook } from 'react-shared-state-hook';
import * as actions from './actions';
export interface AppState {
username: string | null;
count: number;
}
let appState: AppState = {
username: null,
count: 1,
};
const [useAppStateHook, setAppState] = createSharedStateHook(appState);
const [useActionsHook] = createSharedStateHook(actions);
export const useAppState = useAppStateHook;
export const useActions = useActionsHook;
export const getState = () => appState;
export const setState = (update: Partial<AppState>) => {
const newState: AppState = { ...appState, ...update };
appState = newState;
setAppState(appState);
};
// actions.ts
import { setState, getState } from './state';
export function setUsername(username: string) {
setState({ username });
}
export function increaseCount() {
setState({ count: getState().count + 1 });
}
Transforming data structures
When the selector function returns a new object, the component will re-render on every state update. To prevent this, either split up into multiple hook calls, pass an isEqual function or memoize state selections.
Renders on each state update:
const Component = () => {
const myState = useAppState(o => ({ a: o.count, b: o.username, t: '...' }));
return <div>..</div>
};
Using multiple hook calls (and create objects outside of selectors):
const Component = () => {
const a = useAppState(o => o.count);
const b = useAppState(o => o.username);
const myState = { a, b, t: '...' };
return <div>..</div>
};
Passing an isEqual function:
import { shallowEqual } from 'other-npm-module';
const Component = () => {
const myState = useAppState(o => ({ a: o.count, b: o.username, t: '...' }), shallowEqual);
return <div>..</div>
};
Or memoize selections using something like reselect:
const myStateSelector = createSelector<AppState>(
o => o.count,
o => o.username,
(a, b) => ({ a, b, t: '...' })
);
const Component = () => {
const myState = useAppState(myStateSelector);
return <div>..</div>
};
Mixing local state with shared state
const CountControl = () => {
const [ownCount, setOwnCount] = useState(0);
const countText = useAppState(o => `Count: ${o.count} (current ownCount: ${ownCount})`);
const ownCountText = `Own count: ${ownCount}`;
const actions = useActions();
return (
<div>
<p>{countText}</p>
<p>{ownCountText}</p>
<button onClick={() => actions.increaseCount()}>+1 shared</button>
<button onClick={() => setOwnCount(ownCount + 1)}>+1 own</button>
</div>
);
};