use-store-hooks
Version:
Create a redux-like store using hooks. Supports middleware
408 lines (287 loc) • 10.1 kB
Markdown
# use-store-hooks
Create a redux-like store using hooks. Supports middleware.
Demo available [here](https://rare-channel.surge.sh/).
## Test Coverage
Work in progress. No changes have been made to the API up to this point.
Update March 29, 2019:
Files left to test:
- `createDevTools.js`
- `withDevTools.js`
```
65.71% Statements 46/70
45.83% Branches 11/24
67.74% Functions 21/31
67.19% Lines 43/64
```
Update March 28, 2019:
```
50% Statements 35/70
29.17% Branches 7/24
32.26% Functions 10/31
51.56% Lines 33/64
```
## Motivation
Redux is a very powerful concept. This document aims to share how one could still use the concept without having to ever install `redux` and `react-redux`.
In addition, this package provides a bunch of methods to setup a redux-like global store, which connects to Redux Dev Tools and also consumes middleware!
## How to use?
1. Invoke a store
```jsx
const store = invokeStore(reducer);
```
Unlike createStore, this method simply does a dry run of your reducer to get the initial state. Optionally, `invokeStore` can take an `initialState` as second parameters.
If you wish to apply middleware, `invokeStore` takes them as third argument. Middleware must be an array of redux valid middlewares.
> Enhancers are not supported!
2. Wrap your React tree with `<Provider>`
```jsx
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
```
3. Consume the store
This is where this library is fundamentally different than `redux` and `react-redux`.
You have two options:
### Connect
This library exposes `connect` which behaves almost like `react-redux`'s. The main difference is that the second parameter to connect should always be a function!
```jsx
export function WithConnect({ count, dispatch }) {
return (
<Counter
count={count}
inc={() => dispatch({ type: INC })}
dec={() => dispatch({ type: DEC })}
/>
);
}
export default connect(store => ({ count: store }))(GlobalStoreExample);
```
This is a slightly annoying method, which involves Higher Order Components, sometimes testing these is cumbersome.
> `connect` can take zero, one or two arguments!
### useContext
React 16.8.1, exposes the `useContext` API, which can be used to replace `connect`.
```jsx
import React, { useContext } from "react";
import Counter from "../components/Counter";
import { State } from "use-global-store";
import { INC, DEC } from "../ducks/counter";
export function WithoutConnect() {
const { state: count, dispatch } = useContext(State);
return (
<Counter
count={count}
inc={() => dispatch({ type: INC })}
dec={() => dispatch({ type: DEC })}
/>
);
}
export default WithoutConnect;
```
This is a much better approach, as it isolates completely the component from external props. The benefits of using context are already well known.
> Notice how `WithoutConnect` does not need pre-defined props!
## Why?
You could've set this up yourself, what is the big gain?
> This library has no additional dependencies other than React 16.1+ being present in your project!
The biggest gain is the possiblity to use middleware. Furthermore, you can to enable this anywhere in your application.
For example, Redux Dev Tools is a good extension to debug your React-Redux applications, but it relies on enhancing Redux. How could you still use it in your application?
### Vanilla Approach
Let's say you have a React component. Notice that this class component has been structured in such a way that it dispatches actions to a reducer, which updates the state. Redux is a way of coding, not just a library!
```jsx
import React, { Component } from "react";
import Counter from "../components/Counter";
import reducer, { INC, DEC } from "../ducks/counter";
export class Managed extends Component {
state = {
count: 0
};
dispatch = action => reducer(this.state.count, action);
increase = () => this.setState({ count: this.dispatch({ type: INC }) });
decrease = () => this.setState({ count: this.dispatch({ type: DEC }) });
render() {
const { count } = this.state;
return <Counter count={count} inc={this.increase} dec={this.decrease} />;
}
}
```
In order to use it you'd have set your component as shown here:
```jsx
import React, { Component } from "react";
import Counter from "../components/Counter";
import reducer, { INC, DEC } from "../ducks/counter";
const useDevTools =
process.env.NODE_ENV === "development" &&
typeof window !== "undefined" &&
window.__REDUX_DEVTOOLS_EXTENSION__;
export class ReactComponentDevTools extends Component {
state = {
count: 0
};
devTools = null;
extension = null;
componentDidMount() {
if (useDevTools) {
this.extension = window.__REDUX_DEVTOOLS_EXTENSION__;
this.devTools = this.extension.connect({
name: "Managed Dev Tools"
});
this.devTools.send("@INIT", this.state.count);
}
}
componentWillUnmount() {
if (useDevTools) {
this.extension.disconnect();
}
}
dispatch = action => {
const nextState = reducer(this.state.count, action);
if (useDevTools) {
this.devTools.send(action.type, nextState);
}
return nextState;
};
increase = () => this.setState({ count: this.dispatch({ type: INC }) });
decrease = () => this.setState({ count: this.dispatch({ type: DEC }) });
render() {
const { count } = this.state;
return <Counter count={count} inc={this.increase} dec={this.decrease} />;
}
}
export default ReactComponentDevTools;
```
Now your component reports to Redux Dev Tools. However it is now much more verbose!
#### `withDevTools`
Instead, you could just use `withDevTools`, which enhancers your reducer.
```jsx
import React, { useReducer } from "react";
import Counter from "../components/Counter";
import reducer, { INC, DEC } from "../ducks/counter";
import { withDevTools } from "../../../src/";
const enhanced = withDevTools(reducer, { name: "Enhanced" });
export function ReactHookDevToolsEnhancer() {
const [count, dispatch] = useReducer(enhanced, 0);
const inc = () => dispatch({ type: INC });
const dec = () => dispatch({ type: DEC });
return <Counter count={count} inc={inc} dec={dec} />;
}
export default ReactHookDevToolsEnhancer;
```
And now your the `Counter` state is up in the Redux Dev Tools.
#### `useMiddleware`
You can also make local redux store which connects wraps a section of your application.
```jsx
import React, { useReducer } from "react";
import Counter from "../components/Counter";
import reducer, { INC, DEC } from "../ducks/counter";
import {
useMiddleware,
useProvider,
createDevTools,
invokeStore
} from "../../../src/";
// You define your own Context
import CustomContext from "./YourCustomContext";
const middlewares = [createDevTools({ name: "Local Redux" })];
const store = invokeStore(reducer, undefined, middlewares);
export function LocalRedux({ children }) {
const [state, dispatch, ready] = useProvider(store);
return (
<CustomContext.Provider value={{ dispatch, state }}>
{ready ? children : null}
</CustomContext.Provider>
);
}
export default LocalRedux;
```
Further down the three just invoke `useContext` and pass your `CustomContext` as argument!
## API
These are the API's exposed by the package.
### `invokeStore`
This function takes three arguments.
- reducer
- initialState - optional
- middlewares - array of middlewares - optional
```js
const store = invokeStore(reducer, undefined, undefined);
```
### `Provider`
React-like node, which takes a store as single prop!
```jsx
const App = () => (
<Provider store={store}>
<AwesomeApp />
</Provider>
);
```
### `connect`
React-Redux like function. Takes two arguments, `mapStateToProps` and `mapDispatchToProps` to props. Both must be functions! Both could also be undefined.
It returns a Wrapper, which can consume a React Component. The Wrapper passes `props` and the results of `mapStateToProps(state, props)` and `mapDispatchToProps(dispatch, props)` as props to the React Component.
This connect function also passes dispatch down to the React Component.
```js
export default connect(
store => ({ store }),
dispatch => ({ inc: () => dispatch({ type: INC }) })
)(Counter);
```
> Eventually you should move away from `connect`!
### `State`
The actual global state. To move away from `connect` import this instead, and pass it to `useContext` from React's main API.
> `useContext` returns an object!
In this case:
```js
const { state, dispatch } = useContext(State);
```
### `createDevTools`
Easily setup dev tools as middleware by invoking this function. It optionally takes an object, with an environment flag, and a name to be used in the dev tools extension.
The environment flag, could simply be whether or not you are in development environment.
```js
const devTools = createDevTools({
env: process.env.NODE_ENV === "development",
name: "Wow"
});
```
### `useProvider`
Given a store:
```js
const store = { reducer, initialState, middlewares };
```
Returns the state of the store, a dispatcher and whether or not the store is ready to be used!
```js
function Main() {
const [state, dispatch, ready] = useProvider(store);
return [state, dispatch, ready];
}
```
### `useMiddleware`
Takes a store of shape:
```js
const store = { reducer, initialState, middlewares };
```
Returns a `state` and `enhanceDispatch`, which runs throught the middleware!
```js
function Main() {
const [state, enhancedDispatch] = useMiddleware(store);
return [state, enhancedDispatch];
}
```
### `combineReducers`
If you have more than one reducer, you can make a plain object out of them and pass it to combineReducers. The result is your `rootReducer` and what you should pass to `invokeStore`.
```js
const rootReducer = combineReducers({
auth,
counter,
uiState
});
const store = invokeStore(rootReducer);
```
### `compose`
Naive implementation.
```js
const double = x => x * 2;
console.log(
compose(
double,
double
)(2) === double(double(2))
); // true
```