JSPM

  • Created
  • Published
  • Downloads 129246
  • Score
    100M100P100Q167854F
  • License MIT

Yet another React state management library that lets you work with local state and scale up to global state with ease

Package Exports

  • constate

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (constate) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

constate logo



Generated with nod NPM version Build Status Coverage Status



context + state = constate

~2kB React state management library that lets you work with local state and scale up to global state with ease when needed.

👓 Read the introductory article
🎮 Play with the demo



If you find this useful, please don't forget to star ⭐️ the repo, as this will help to promote the project.
Follow me on Twitter and GitHub to keep updated about this project and others.



Install 📦

npm i constate

Quick start 💥

import React from "react";
import { State } from "constate";

const initialState = { count: 0 };

const actions = {
  increment: () => state => ({ count: state.count + 1 })
};

const Counter = () => (
  <State initialState={initialState} actions={actions}>
    {({ count, increment }) => (
      <button onClick={increment}>{count}</button>
    )}
  </State>
);

Example

Guide 📖

Table of Contents

Local state

You can start by creating your State component:

import React from "react";
import { State } from "constate";

export const initialState = {
  count: 0
};

export const actions = {
  increment: amount => state => ({ count: state.count + amount })
};

export const selectors = {
  getParity: () => state => (state.count % 2 === 0 ? "even" : "odd")
};

const CounterState = props => (
  <State
    initialState={initialState}
    actions={actions}
    selectors={selectors}
    {...props}
  />
);

export default CounterState;

Note: the reason we're exporting initialState, actions and selectors is to make testing easier.

Then, just use it elsewhere:

const CounterButton = () => (
  <CounterState>
    {({ count, increment, getParity }) => (
      <button onClick={() => increment(1)}>{count} {getParity()}</button>
    )}
  </CounterState>
);

Example

Global state

Whenever you need to share state between components and/or feel the need to have a global state, you can pass a context property to State and wrap your app with Provider:

const CounterButton = () => (
  <CounterState context="counter1">
    {({ increment }) => <button onClick={() => increment(1)}>Increment</button>}
  </CounterState>
);

const CounterValue = () => (
  <CounterState context="counter1">
    {({ count }) => <div>{count}</div>} 
  </CounterState>
);

const App = () => (
  <Provider>
    <CounterButton />
    <CounterValue />
  </Provider>
);

Example

Composing state

This is still React, so you can pass new properties to CounterState, making it really composable.

First, let's change our CounterState so as to receive new properties:

const CounterState = props => (
  <State
    {...props}
    initialState={{ ...initialState, ...props.initialState }}
    actions={{ ...actions, ...props.actions }}
    selectors={{ ...selectors, ...props.selectors }}
  />
);

Now we can pass new initialState, actions and selectors to CounterState:

export const initialState = {
  count: 10
};

export const actions = {
  decrement: amount => state => ({ count: state.count - amount })
};

const CounterButton = () => (
  <CounterState initialState={initialState} actions={actions}>
    {({ count, decrement }) => (
      <button onClick={() => decrement(1)}>{count}</button>
    )}
  </CounterState>
);

Example

Those new members will work even if you use context.

Effects

An effect is a method that receives both state and setState. This is useful if you need to perform side effects, like async actions, or just want to use setState.

export const effects = {
  tick: () => ({ setState }) => {
    setTimeout(() => {
      setState(state => ({ count: state.count + 1 }));
      effects.tick()({ setState })
    }, 1000);
  }
};

const AutomaticCounterButton = () => (
  <CounterState effects={effects}>
    {({ count, tick }) => (
      <button onClick={tick}>{count}</button>
    )}
  </CounterState>
);

Global initial state

It's possible to pass initialState to Provider:

const initialState = {
  counter1: {
    count: 10
  }
};

const App = () => (
  <Provider initialState={initialState}>
    ...
  </Provider>
);

This way, all State with context="counter1" will start with { count: 10 }

Note: while using context, only the initialState of the first State in the tree will be considered. Provider will always take precedence over State.

State in lifecycle methods

As stated in the official docs, to access state in lifecycle methods you can just pass the state down as a prop to another component and use it just like another prop:

class CounterButton extends React.Component {
  componentDidMount() {
    this.props.state.increment(1);
  }

  render() {
    const { increment } = this.props.state;
    return <button onClick={() => increment(1)}>Increment</button>;
  }
}

export default props => (
  <CounterState context="counter1">
    {state => <CounterButton {...props} state={state} />}
  </CounterState>
);

Another alternative is to use https://github.com/reactions/component:

import Component from "@reactions/component";

const CounterButton = () => (
  <CounterState context="counter1">
    {({ increment }) => (
      <Component didMount={() => increment(1)}>
        <button onClick={() => increment(1)}>Increment</button>
      </Component>
    )}
  </CounterState>
);

Call selectors in actions

This is just JavaScript:

export const selectors = {
  isEven: () => state => state.count % 2 === 0
};

export const actions = {
  increment: () => state => ({
    count: state.count + (selectors.isEven()(state) ? 2 : 1)
  })
};

Call actions in effects

Aren't you already convinced that this is JavaScript?

const increment = amount => state => ({ count: state.count + amount })

export const effects = {
  tick: amount => ({ setState }) => {
    setTimeout(() => {
      setState(increment(amount));
      effects.tick(amount)({ setState })
    }, 1000);
  }
};

Testing

actions and selectors are pure functions. Testing is pretty straightfoward:

import { initialState, actions, selectors } from "./CounterState";

test("initialState", () => {
  expect(initialState).toEqual({ count: 0 });
});

test("actions", () => {
  expect(actions.increment(1)({ count: 0 })).toEqual({ count: 1 });
  expect(actions.increment(-1)({ count: 1 })).toEqual({ count: 0 });
});

test("selectors", () => {
  expect(selectors.getParity()({ count: 0 })).toBe("even");
  expect(selectors.getParity()({ count: 1 })).toBe("odd");
});

Testing effects can be a little tricky depending on how you implement them. This is how we can test our tick effect with Jest:

import { effects } from "./CounterState";

test("tick", () => {
  jest.useFakeTimers();

  let state = { count: 0 };
  const setState = fn => {
    state = fn(state);
  };

  effects.tick()({ state, setState });

  jest.advanceTimersByTime(1000);
  expect(state).toEqual({ count: 1 });

  jest.advanceTimersByTime(1000);
  expect(state).toEqual({ count: 2 });
});

API 🧐

type Action = () => (state: Object) => Object;

type Selector = () => (state: Object) => any;

type Effect = () => ({ state: Object, setState: Function }) => void;

type StateProps = {
  children: (state: Object) => React.Node,
  initialState: Object,
  actions: { [string]: Action },
  selectors: { [string]: Selector },
  effects: { [string]: Effect },
  context: string
};

type ProviderProps = {
  children: React.Node,
  initialState: Object
};

Contributing 👥

If you find a bug, please create an issue providing instructions to reproduce it. It's always very appreciable if you find the time to fix it. In this case, please submit a PR.

If you're a beginner, it'll be a pleasure to help you contribute. You can start by reading the beginner's guide to contributing to a GitHub project.

TODO 📝

  • Middlewares? (create an issue if you find a use case for this)
  • Debugger/devtools
  • Memoize selectors
  • Global actions/selectors

License ⚖️

MIT © Diego Haz