A simple, lightweight (3kb), dependency-free state manager for React, built using hooks.
Note: requires react and react-dom @ 16.7.0-alpha.2 or higher
Install the package using yarn or npm:
yarn add use-simple-state
npm install use-simple-state --save
Before we get started, we first need an initial state, as well as some actions and at least one reducer:
const initialState = { count: 0 };
const addOne = () => ({ type: 'ADD_ONE' });
const minusOne = () => ({ type: 'MINUS_ONE' });
const countReducer = (state, action) => {
switch (action.type) {
case 'ADD_ONE':
return { count: state.count + 1 };
case 'MINUS_ONE':
return { count: state.count - 1 };
}
Lastly, we simply import SimpleStateProvider
, pass our reducers and initial state, then wrap our app's root component:
import React from 'react';
import { SimpleStateProvider } from 'use-simple-state';
import App from './App';
export default function Root () {
return (
<SimpleStateProvider initialState={initialState} reducers={[countReducer]}>
<App />
</SimpleStateProvider>
);
}
And that's it.
Now whenever we want to access or update our state, we just import the useSimpleState
hook:
import React from 'react';
import { useSimpleState } from 'use-simple-state';
import { addOne, minusOne } from './store';
export default function Counter () {
const [state, dispatch] = useSimpleState();
return (
<>
<h1>Count: {state.count}</h1>
<button onClick={() => dispatch(addOne())}> +1 </button>
<button onClick={() => dispatch(minusOne())}> -1 </button>
</>
);
}
Hooks don't yet provide a way for us to bail out of rendering, although it currently looks as though this may be added in a future release (you can follow the dicussion here).
In the meantime I've provided a SimpleStateConsumer
to consume our state using a consumer similar to the default one returned by React.createContext
. This means our connected components won't re-render on
every state change, but rather will only update when the specific part of the store they're subscribed to changes.
import { SimpleStateConsumer } from 'use-simple-state';
export default function Counter () {
return (
<SimpleStateConsumer mapState={({ count }) => ({ count })}>
{({ state, dispatch }) => (
<>
<h1>Count: {state.count}</h1>
<button onClick={() => dispatch(addOne())}> +1 </button>
<button onClick={() => dispatch(minusOne())}> -1 </button>
</>
)}
</SimpleStateConsumer>
);
}
Comes with built-in support for asynchronous actions by providing an API similar to redux-thunk
.
If a function is passed to dispatch
it will be called with dispatch
and state
as parameters. This allows us to handle async tasks, like the following example of an action used to authenticate a user:
// Some synchronous actions
const logInRequest = () => ({ type: 'LOG_IN_REQUEST' });
const logInSuccess = ({ user }) => ({ type: 'LOG_IN_SUCCESS', payload: user });
const logInError = ({ error }) => ({ type: 'LOG_IN_ERROR', payload: error });
// Our asynchronous action
const logIn = ({ email, password }) => async (dispatch, state) => {
dispatch(logInRequest());
try {
const user = await api.authenticateUser({ email, password });
dispatch(logInSuccess({ user }));
} catch (error) {
dispatch(logInError({ error }));
}
};
// Dispatch logIn like any other action
dispatch(logIn({ email, password }));
Note: dispatch
will return the result of any async actions, opening up possibilities like chaining promises from dispatch
:
dispatch(logIn({ email, password })).then(() => {
// Do stuff...
});
A custom React hook that lets us access our state and dispatch
function from inside components.
useSimpleState(mapState?: Function, mapDispatch?: Function): Array<mixed>
const [state, dispatch] = useSimpleState();
Returns an array containing a state
object and a dispatch
function.
useSimpleState
has two optional parameters: mapState
and mapDispatch
:
If mapState
is passed, it will be used to compute the output state and the result will be passed to the first element of the array returned by useSimpleState
.
mapState(state: Object): Object
const mapState = state => ({ total: state.countA + state.countB });
const [computedState, dispatch] = useSimpleState(mapState);
Note: null
can also be passed if you want to use mapDispatch
but have no use for a mapState
function.
mapDispatch
can be used to pre-wrap actions in dispatch
. If mapDispatch
is passed, the result will be given as the second element of the array returned by useSimpleState
.
mapDispatch(dispatch: Function): *
const mapDispatch = dispatch => ({
dispatchA: () => dispatch(actionA()),
dispatchB: () => dispatch(actionB()),
dispatchC: () => dispatch(actionC())
});
const [state, computedDispatch] = useSimpleState(null, mapDispatch);
computedDispatch.dispatchA();
A React component that wraps an app's root component and makes state available to our React app.
const Root = () => (
<StateProvider state={initialState} reducers={[reducer]} middleware={[middleware]}>
<App/>
</StateProvider>
);
Has two mandatory props: initialState
and reducers
, as well as an optional prop: middleware
An object representing the initial state of our app.
An array of reducers.
Reducers take an action as well as the current state and use these to derive a new state. If a reducer returns undefined
there will be no state update.
Reducers should have the following API:
(state, action) => nextState
An array of middleware functions.
Middleware functions are used to handle side effects in our app.
A middleware function is given two parameters: state
and action
.
If any middleware returns null
, the triggering action
will be blocked from reaching our reducers
and the state will not be updated.
function myMiddleware (action, state) {
if (action.type === 'ADD') {
console.log(`${state.count} + ${action.payload} = ${state.count + action.payload}`);
}
}
A React component that is used to access the state context with a similar API to the useSimpleState
hook.
Note: this component is a temporary workaround to be used until hooks are able to bail us out of the rendering process.
const Greeting = () => (
<SimpleStateConsumer>
{({ state, dispatch }) => (
<>
<h1>{state.greeting}</h1>
<button onClick={() => dispatch(setGreeting('hello'))}> Change greeting </button>
</>
)}
</SimpleStateConsumer>
);
Has two optional props: mapState
and mapDispatch
. Use of mapState
is strongly encouraged so that each consumer only
subscribes to specific changes in the state. If no mapState
is passed, your consumer will re-render on every single state change.
The following props are identical to those of useSimpleState
.
If mapState
is passed, it will be used to compute the output state and the result will be passed to the state
key of SimpleStateConsumer
's render prop.
mapState(state: Object): Object
const mapState = state => ({ total: state.countA + state.countB });
const Total = () => (
<SimpleStateConsumer mapState={mapState}>
{({ state }) => (
<span>Total: {state.total}</span>
)}
</SimpleStateConsumer>
);
mapDispatch
can be used to pre-wrap actions in dispatch
. If mapDispatch
is passed, the result will be passed to the dispatch
property of SimpleStateConsumer
's render prop.
mapDispatch(dispatch: Function): *
const mapDispatch = dispatch => ({
dispatchA: () => dispatch(actionA()),
dispatchB: () => dispatch(actionB()),
dispatchC: () => dispatch(actionC())
});
const Dispatcher = () => (
<SimpleStateConsumer mapDispatch={mapDispatch}>
{({ dispatch }) => (
<>
<button onClick={dispatch.dispatchA}>Dispatch A</button>
<button onClick={dispatch.dispatchB}>Dispatch B</button>
<button onClick={dispatch.dispatchC}>Dispatch C</button>
</>
)}
</SimpleStateConsumer>
);