use-store-hooks

1.0.7 • Public • Published

use-store-hooks

Create a redux-like store using hooks. Supports middleware.

Demo available here.

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
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!

  1. Wrap your React tree with <Provider>
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
  1. 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!

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.

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!

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:

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.

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.

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
const store = invokeStore(reducer, undefined, undefined);

Provider

React-like node, which takes a store as single prop!

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.

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:

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.

const devTools = createDevTools({
  env: process.env.NODE_ENV === "development",
  name: "Wow"
});

useProvider

Given a store:

const store = { reducer, initialState, middlewares };

Returns the state of the store, a dispatcher and whether or not the store is ready to be used!

function Main() {
  const [state, dispatch, ready] = useProvider(store);
  return [state, dispatch, ready];
}

useMiddleware

Takes a store of shape:

const store = { reducer, initialState, middlewares };

Returns a state and enhanceDispatch, which runs throught the middleware!

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.

const rootReducer = combineReducers({
  auth,
  counter,
  uiState
});
 
const store = invokeStore(rootReducer);

compose

Naive implementation.

const double = x => x * 2;
 
console.log(
  compose(
    double,
    double
  )(2) === double(double(2))
); // true

Package Sidebar

Install

npm i use-store-hooks

Weekly Downloads

1

Version

1.0.7

License

MIT

Unpacked Size

41 kB

Total Files

15

Last publish

Collaborators

  • icyjoseph