Socket
Socket
Sign inDemoInstall

constate

Package Overview
Dependencies
8
Maintainers
1
Versions
40
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    constate

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


Version published
Maintainers
1
Install size
61.4 kB
Created

Readme

Source

constate logo



Generated with nod NPM version Gzip size Dependencies Build Status Coverage Status



context + state = constate

React state management library built with scalability in mind. You can start simple with local state and scale up to global state with ease when needed.

👓 Read the introductory article
🎮 Play with the demo



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

const initialState = { count: 0 };

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

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

Example

Table of Contents

Installation

npm i constate

Container

In computer science, a container is a class, a data structure, or an abstract data type (ADT) whose instances are collections of other objects. In other words, they store objects in an organized way that follows specific access rules.

https://en.wikipedia.org/wiki/Container_(abstract_data_type)

initialState

type initialState = Object;

Use this prop to define the initial state of the container.

const initialState = { count: 0 };

const Counter = () => (
  <Container initialState={initialState}>
    {({ count }) => <button>{count}</button>}
  </Container>
);

Example

actions

type Actions = {
  [string]: () => ((state: Object) => Object) | Object
};

An action is a method that returns an updater function, which will be, internally, passed as an argument to React setState. Actions will be exposed, then, together with state within the child function.

You can also return the object directly if you don't need state.

const initialState = { count: 0 };

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

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

Example

selectors

type Selectors = {
  [string]: () => (state: Object) => any
};

A selector is a method that returns a function, which receives the current state and should return something (the thing being selected).

const initialState = { count: 0 };

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

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

const Counter = () => (
  <Container
    initialState={initialState}
    actions={actions}
    selectors={selectors}
  >
    {({ count, increment, getParity }) => (
      <button onClick={() => increment(1)}>{count} {getParity()}</button>
    )}
  </Container>
);

Example

effects

type Effects = {
  [string]: () => ({ state: Object, setState: Function }) => void
};

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

const initialState = { count: 0 };

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

const Counter = () => (
  <Container initialState={initialState} effects={effects}>
    {({ count, tick }) => (
      <button onClick={tick}>{count}</button>
    )}
  </Container>
);

Example

context

type Context = string;

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

import { Provider, Container } from "constate";

const CounterContainer = props => (
  <Container
    initialState={{ count: 0 }}
    actions={{ increment: () => state => ({ count: state.count + 1 }) }}
    {...props}
  />
);

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

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

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

Example

onMount

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

This is a function called inside Container's componentDidMount.

Note: when using context, all Containers of the same context behave as a single unit, which means that onMount will be called only for the first mounted Container of each context.

const initialState = { count: 0 };

const onMount = ({ setState }) => {
  const fn = () => setState(state => ({ count: state.count + 1 }));
  document.body.addEventListener("mousemove", fn);
};

const Counter = () => (
  <Container initialState={initialState} onMount={onMount}>
    {({ count }) => <button>{count}</button>}
  </Container>
);

Example

onUpdate

type OnUpdate = ({ 
  prevState: Object, 
  state: Object, 
  setState: Function,
  type: string
}) => void;

This is a function called every time setState is called, either internally with actions or directly with effects and lifecycle methods, including onUpdate itself.

Besides prevState, state and setState, it receives a type property, which can be either the name of the action, effect or one of the lifecycle methods that triggered it, including onUpdate itself.

Note: when using context, onUpdate will be triggered only once per setState call no matter how many Containers of the same context you have mounted.

const initialState = { count: 0 };

const onMount = ({ setState }) => {
  const fn = () => setState(state => ({ count: state.count + 1 }));
  setInterval(fn, 1000);
};

const onUpdate = ({ state, setState, type }) => {
  if (type === "onMount" && state.count === 5) {
    // reset counter
    setState({ count: 0 });
  }
};

const Counter = () => (
  <Container initialState={initialState} onMount={onMount} onUpdate={onUpdate}>
    {({ count }) => <button>{count}</button>}
  </Container>
);

Example

onUnmount

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

This is a function called inside Container's componentWillUnmount. It receives both current state and setState, but the latter will have effect only if you're using context. Otherwise, it will be noop. This is useful for making cleanups.

Note: when using context, all Containers of the same context behave as a single unit, which means that onUnmount will be called only when the last remaining Container of each context gets unmounted.

const initialState = { count: 0 };

const onMount = ({ setState }) => {
  const fn = () => setState(state => ({ count: state.count + 1 }));
  const interval = setInterval(fn, 1000);
  setState({ interval });
};

const onUnmount = ({ state }) => {
  clearInterval(state.interval);
};

const Counter = () => (
  <Container initialState={initialState} onMount={onMount} onUnmount={onUnmount}>
    {({ count }) => <button>{count}</button>}
  </Container>
);

Example

shouldUpdate

type ShouldUpdate = ({ state: Object, nextState: Object }) => boolean;

This is a function called inside Containers shouldComponentUpdate. It receives the current state and nextState and should return true or false. If it returns false, onUpdate won't be called for that change, and it won't trigger another render.

In the previous example using onUnmount, we stored the result of setInterval in the state. That's ok to do, but the downside is that it would trigger another render, even though our UI didn't depend on state.interval. We can use shouldUpdate to ignore state.interval, for example:

const shouldUpdate = ({ state, nextState }) => state.interval === nextState.interval;

Provider

You should wrap your app with Provider if you want to use context.

initialState

type InitialState = Object;

It's possible to pass initialState to Provider. In the example below, all Containers with context="counter1" will start with { count: 10 }.

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

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

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

onMount

type OnMount = ({ state: Object, setContextState: Function }) => void;

As well as with Container, you can pass an onMount prop to Provider. The function will be called when Provider's componentDidMount gets called.

const onMount = ({ setContextState }) => {
  setContextState("counter1", { count: 0 });
};

const MyProvider = props => (
  <Provider onMount={onMount} {...props} />
);

const App = () => (
  <MyProvider>
    ...
  </MyProvider>
);

onUpdate

type OnUpdate = ({ 
  prevState: Object,
  state: Object,
  setContextState: Function,
  context: string,
  type: string
}) => void;

onUpdate will be called every time Provider's setState gets called. If setContextState was called instead, onUpdate will also receive a context prop.

Containers, when the context prop is defined, use setContextState internally, which means that Provider's onUpdate will be triggered for every change on the context.

const initialState = { counter1: { incrementCalls: 0 } };

const onUpdate = ({ context, type, setContextState }) => {
  if (type === "increment") {
    setContextState(context, state => ({
      incrementCalls: state.incrementCalls + 1
    }));
  }
};

const MyProvider = props => (
  <Provider initialState={initialState} onUpdate={onUpdate} {...props} />
);

const CounterContainer = props => (
  <Container
    initialState={{ count: 0 }}
    actions={{ increment: () => state => ({ count: state.count + 2 }) }}
    {...props}
  />
);

const Counter = () => (
  <MyProvider>
    <CounterContainer context="counter1">
      {({ count, incrementCalls, increment }) => (
        <button onClick={increment}>
          count: {count}<br />
          incrementCalls: {incrementCalls}
        </button>
      )}
    </CounterContainer>
  </MyProvider>
);

Example

onUnmount

type OnUnmount => ({ state: Object }) => void;

onUnmount will be triggered in Provider's componentWillUnmount.

const onUnmount = ({ state }) => {
  console.log(state);
};

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

devtools

type Devtools = boolean;

Passing devtools prop to Provider will enable redux-devtools-extension integration, if that's installed on your browser. With that, you can easily debug the state of your application.

Note: It only works for context state. If you want to debug local state, add a context prop to Container temporarily.

const App = () => (
  <Provider devtools>
    ...
  </Provider>
);

Example

mount

type Mount = (Container: Function | ReactElement) => Object;

Note: this is an experimental feature

With mount, you can have a stateful object representing the Container:

import { Container, mount } from "constate";

const CounterContainer = props => (
  <Container
    initialState={{ count: 0 }}
    actions={{ increment: () => state => ({ count: state.count + 1 }) }}
    {...props}
  />
);

const state = mount(CounterContainer);

console.log(state.count); // 0
state.increment();
console.log(state.count); // 1

Composing

Since Container is just a React component, you can create Containers that accepts new properties, making them really composable.

For example, let's create a composable CounterContainer:

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

const CounterContainer = ({ initialState, actions, ...props }) => (
  <Container
    initialState={{ count: 0, ...initialState }}
    actions={{ increment, ...actions }}
    {...props}
  />
);

Then, we can use it to create a DecrementableCounterContainer:

const decrement = () => state => ({ count: state.count - 1 });

const DecrementableCounterContainer = ({ actions, ...props }) => (
  <CounterContainer actions={{ decrement, ...actions }} {...props} />
);

Finally, we can use it on our other components:

const CounterButton = () => (
  <DecrementableCounterContainer initialState={{ count: 10 }}>
    {({ count, decrement }) => <button onClick={decrement}>{count}</button>}
  </DecrementableCounterContainer>
);

Example

Testing

actions and selectors are pure functions and you can test them directly:

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

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

On the other hand, effects and lifecycle methods can be a little tricky to test depending on how you implement them.

You can also use mount to create integration tests. This is how we can test our CounterContainer with its tick effect:

import { mount } from "constate";
import CounterContainer from "./CounterContainer";

test("initialState", () => {
  const state = mount(CounterContainer);
  expect(state.count).toBe(0);
});

test("increment", () => {
  const state = mount(CounterContainer);
  expect(state.count).toBe(0);
  state.increment(1);
  expect(state.count).toBe(1);
  state.increment(-1);
  expect(state.count).toBe(0);
});

test("getParity", () => {
  const state = mount(<CounterContainer initialState={{ count: 1 }} />);
  expect(state.getParity()).toBe("odd");
});

test("tick", () => {
  jest.useFakeTimers();
  const state = mount(CounterContainer);
  
  state.tick();

  jest.advanceTimersByTime(1000);
  expect(state.count).toBe(1);

  jest.advanceTimersByTime(1000);
  expect(state.count).toBe(2);
});

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.

Run npm start to run examples.

License

MIT © Diego Haz

Keywords

FAQs

Last updated on 20 Jul 2018

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc