davidtheclark / react-rootless-state

A React state container that lives outside the component tree

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

react-rootless-state

EXPERIMENT!! PROOF OF CONCEPT!! NOT READY FOR ACTION!!

The goal

An API for shareable state that is as small, idiomatic, and simple as possible, can be connected to arbitrary React components, and is open to standard React & Redux optimizations.

"As simple and idiomatic as possible" means that rootless state should work in pretty much the same way as regular React component state. You define some initial state; and you define some functions that, when triggered, will update that state by invoking setState() ( here called "actions").

That means no ambitious optimizations by default — but opportunity for them if & when you want them. You can incorporate the same optimizations that you would for regular component state: things like shouldComponentUpdate, immutable data structures, and selector memoization.

It seems to be universally accepted that shareable state like this must rely on React's context. I don't understand why this is taken for granted. (I must be missing something?) It means that every connected component needs a Provider ancestor, and this usually seems like an unnecessary formality that does not provide additional protection or clarity. (Maybe there are some low-level optimizations involved?)

So this state container is "rootless" because it's not part of a React component tree. You can connect it to whatever component you want, wherever it is in whatever tree.

Usage

Create a rootless state instance by defining its initial state and its actions.

// counter-state.js
import createRootlessState from 'react-rootless-state';

// Define your initial state.
const initialState = {
  count: 0,
  thinking: false
};

// Define your actions.
//
// Every action function is defined with the first
// argument as the rootless state instance, which
// exposes setState, getState, and all the actions.
// When using these action functions after the state
// has been initialized, you do not pass that
// first instance argument.
//
// As an optimization, you could have your state
// use immutable data structures, which would help with
// usage of PureComponent or shouldComponentUpdate in
// your connected components.
const actions = {
  increment(rootless, amount = 1) {
    rootless.setState(state => ({ count: state.count + amount }));
  },
  decrement(rootless, amount = 1) {
    rootless.setState(state => ({ count: state.count - amount }));
  },
  // The instance argument exposes the other actions,
  // which you can use to create compound actions.
  // You can also do asynchronous things: just call
  // rootless.setState() whenever it's time to update
  // some portion of the state.
  startThinking(rootless) {
    rootless.setState({ thinking: true });
  },
  stopThinking(rootless) {
    rootless.setState({ thinking: false });
  },
  incrementAfterThinking(rootless, amount = 1) {
    rootless.actions.startThinking();
    // Think for 1 second, then update the count.
    setTimeout(() => {
      rootless.actions.stopThinking();
      rootless.actions.increment(amount);
    }, 1000);
  }
};

const counterState = createRootlessState({ initialState, actions });
export default counterState;

Connect your rootless state to a component like this: rootlessState.connect(selectProps)(Component).

// incrementer.js
import React from 'react';
import counterState from './counter-state';

class Incrementer extends React.Component {
  constructor(props) {
    super(props);
    this.addOne = this.addOne.bind(this);
  }

  addOne() {
    this.props.increment();
  }

  render() {
    return (
      <div>
        <h2>Incrementer</h2>
        <p>Count: {this.props.count}</p>
        <button onClick={this.addOne}>increment</button>
      </div>
    );
  }
}

// Use the two arguments to determine which props you want to
// inject into your connected component.
//
// You could always write selector functions and memoize them
// if you run into performance problems.
function selectProps(state, actions) {
  return {
    count: state.count,
    increment: actions.increment
  };
}

export default counterState.connect(selectProps)(Incrementer);

About

A React state container that lives outside the component tree

License:MIT License


Languages

Language:JavaScript 100.0%