/amorphous

React state management, without the new concepts

Primary LanguageJavaScriptMIT LicenseMIT

About Amorphous

version license build status Coverage Status docs

Amorphous makes sharing state in react as easy as using setState.

Just as this.state is a component's state and can be updated with setState, this.appState is an app's state and can be updated with this.setAppState:

class Input extends AppComponent {
  render() {
    return (
      <input
        type="text"
        value={this.appState.text}
        onChange={(e) => this.setAppState({ text: e.target.value })}
      />
    );
  }
}

Amorphous is designed to:

  • get your app's state management working as quickly as possible
  • avoid unnecessary pitfalls while doing so

Usage

Amorphous has two main classes:

  • AppComponent: a class for components that use appState
  • RootAppComponent: a class for the root component of your app

To use AppComponent, you must have a RootAppComponent at the root of your app. (For library authors, see using Amorphous in a library.)

Both AppComponent and RootAppComponent have access to:

  • this.appState
  • this.setAppState
  • shouldComponentUpdate(nextProps, nextState, appState)
  • componentDidUpdate(nextProps, nextState, snapshot, appState)

Full Example:

import { AppComponent, RootAppComponent } from 'amorphous';

class Input extends AppComponent {

  render() {
    return (
      <input
        type="text"
        value={this.appState.text || 'null'}
        onChange={(e) => this.setAppState({ text: e.target.value })}
      />
    );
  }
}

class Output extends AppComponent {
  render() {
    return (
      <span>
        {'You typed: '}
        {this.appState.text}
      </span>
    );
  }
}

class App extends RootAppComponent {
  appState = { text: 'hi' };

  render() {
    return (
      <div>
        <Input />
        <Output />
      </div>
    );
  }
}

Getting started

First, install Amorphous by running:

npm install amorphous

Then, you can import AppComponent and RootAppComponent:

import { AppComponent, RootAppComponent } from 'amorphous';

At the root of your application (or subtree), extend RootAppComponent instead of React.Component:

class App extends RootAppComponent {
  // ...
}

And optionally initialize your appState:

class App extends RootAppComponent {
  constructor(props) {
    super(props);
    this.appState = {text: 'hi'};
  }
}

Then, in any component you want to access appState, extend AppComponent instead of React.Component:

class Input extends AppComponent {
  // ...
}

Inside this component, you can access this.appState and update app state with this.setAppState:

class Input extends AppComponent {
  render() {
    return <input
      type="text"
      value={this.appState.text}
      onChange={e => this.setAppState({text: e.target.value})}
    />;
  }
}

And you're ready to send shared state to anywhere your app needs it!

API

RootAppComponent

RootAppComponent creates a new appState, and should be extended by your app's root component. Any AppComponent must be a descendent of a RootAppComponent (that is, all AppComponents must have a RootAppComponent above them, but not necessarily directly above them, in their component tree).

Usage

RootAppComponent is a base component, so you should extend from it like you would React.Component.

class App extends RootAppComponent {
  // ...
}

To initialize appState, you should set appState either as an instance property or in the constructor, as you would with state:

class App extends RootAppComponent {

  state = {};
  appState = { someProperty: 0 };

  // or:
  constructor(props) {
    super(props);
    this.state = {};
    this.appState = { someProperty: 0 };
  }
}

API

this.appState

Initialize or access appState. this.appState should be initialized in your root component's constructor (or via appState = inside the class body).

this.setAppState(update, callback)

this.appStateContext

shouldComponentUpdate(nextProps, nextState, nextAppState)

componentDidUpdate(prevProps, prevState, snapshot, prevAppState)

And all React.Component methods

AppComponent

AppComponent is a replacement for React.Component for any component that needs access to appState. Any AppComponent must be a descendent of a RootAppComponent (that is, all AppComponents must have a RootAppComponent above them, but not necessarily directly above them, in their component tree).

Usage

AppComponent is a base component, so you should extend from it like you would React.Component.

class SomeComponent extends AppComponent {
  // ...
}

Your component can access this.appState in render(), as you would access this.state, and can call this.setAppState from within any event handlers, as you would for this.setState.

class SomeComponent extends AppComponent {

  render() {
    return (
      <input
        type="button"
        value={"clicked " + this.appState.buttonClickedCount + " times"}
        onClick={() => this.setAppState({
          buttonClickedCount: this.appState.buttonClickedCount + 1,
        })}
      />
    );
  }
}

API

this.appState

Access appState. this.appState should be initialized in your root component's constructor (or via appState = inside the class body).

this.setAppState(update, callback)

this.appStateContext

shouldComponentUpdate(nextProps, nextState, nextAppState)

componentDidUpdate(prevProps, prevState, snapshot, prevAppState)

And all React.Component methods

this.appState

appState allows all AppComponents (including the RootAppComponent) to share state. It works similarly to this.state, but is shared across all AppComponents.

Initializing appState

appState is initialized by the RootAppComponent's constructor. By default it is initialized to {}, but you should initialize it to a more reasonable default in your RootAppComponent class:

class App extends RootAppComponent {
  appState = { myToDos: [] };

  // or:
  constructor(props) {
    super(props);
    this.appState = { myToDos: [] };
  }
}

Using appState

Amorphous provides this.appState in all AppComponents (including your RootAppComponent).

NOTE: this.appState is not accessible in the constructor of AppComponents, or in any static methods.

This means that in render() or other methods, you can access this.appState to read your app's current state, and display something based on that:

class MyToDoList extends AppComponent {
  render() {
    return (
      <div>
        {this.appState.myToDos.map((todoItem) => (
          <MyToDo item={todoItem} />
        ))}
      </div>
    );
  }
}

Updating appState

AppState can be updated from any AppComponent or RootAppComponent using this.setAppState().

class MyToDoList extends AppComponent {
  render() {
    return (
      <div>
        {this.appState.myToDos.map((todoItem) => (
          <MyToDo item={todoItem} />
        ))}
        <input
          type="button"
          value="Add To-Do"
          onClick={() => this.setAppState({
            myToDos: this.appState.myToDos.concat({
              text: '',
              completed: false,
            }),
          })}
        />
      </div>
    );
  }
}

Read more about setAppState

Comparing previous/next appState in lifecycle methods

Amorphous provides an additional appState parameter to shouldComponentUpdate and componentDidUpdate for AppComponents and RootAppComponents. This allows components to compare this.appState to previous/next versions of appState.

See shouldComponentUpdate and componentDidUpdate for more information.

this.setAppState

this.setAppState(update, callback)

Like this.setState but for app state instead of component state.

update may be an object or a function.

If update is an object:

  • setAppState will merge update into this.appState

If update is a function:

  • update must have the form (prevAppState) => newAppState
  • setAppState will call update with the current appState value, and will merge the returned newAppState value into appState.

setAppState is not synchronous, and will call callback after it has completed merging update into appState.

shouldComponentUpdate

React provides a shouldComponentUpdate method for optimizing components by preventing unnecessary renders.

In Amorphous, this method continues to do the same thing, but is given an extra parameter, prevAppState, so that shouldComponentUpdate can compare differences in appState as well as differences in props or state:

class LengthOutput extends MyAppComponent {

  shouldComponentUpdate(prevProps, prevState, prevAppState) {
    return this.appState.text.length !== prevAppState.text.length;
  }

  render() {
    return (
      <span>
        You have typed {this.appState.text.length} characters
      </span>
    );
  }
}

NOTE: both this.appState and prevAppState are accessible in shouldComponentUpdate.

See React's docs for more information about the shouldComponentUpdate and how to best use it.

componentDidUpdate

Amorphous provides this.appState and this.setAppState during and after your component's first render. They are not accessible in the constructor.

Additionally, Amorphous provides an appState parameter for the following React lifecycle methods:

  • shouldComponentUpdate(nextProps, nextState, nextAppState)
  • componentDidUpdate(prevProps, prevState, snapshot, prevAppState)

You may use either of these methods to monitor changes to appState and update your AppComponent properly, like you would for this.state.

Amorphous AppComponents and RootAppComponents provide a fourth parameter to componentDidUpdate: prevAppState, which holds the value of appState before the most recent render, and may be useful for comparing with the new this.appState value to perform non-react updates after the component has rendered. See lifecycle methods for more details and examples.

Note: snapshot is the return value of getSnapshotBeforeUpdate(), or undefined if no getSnapshotBeforeUpdate() is specified.

getDerivedAppState

static getDerivedAppState(appState)

Similar to getDerivedStateFromProps, Amorphous supports a static getDerivedAppState method on the RootAppComponent only. This function may be used to trigger additional modifications of appState when appState is modified, which can be useful for caching expensive calculations or time-unique values.

Example:

Using Amorphous in a Library

If you are a library author using Amorphous, it is important to make sure your library's appState does not conflict with the client's appState.

Amorphous uses React context to control which components have access to which appStates. To make a new context for your library, use:

import { createAppStateContext } from 'amorphous';

const MyAppStateContext = createAppStateContext();

Then, to specify that your components use MyAppStateContext instead of the default appState context, set the appStateContext property on those components:

class MyApp extends RootAppComponent {
  appStateContext = MyAppStateContext;

  // ...
}

class MyComponent extends AppComponent {
  appStateContext = MyAppStateContext;

  // ...
}

To make this less reduntant, I suggest making your own RootAppComponent and AppComponent classes for your library with extension:

Making Amorphous classes for your library

import { AppComponent, RootAppComponent, createAppStateContext } from 'amorphous';

const MyAppStateContext = createAppStateContext();

export class MyAppComponent extends AppComponent {
  appStateContext = MyAppStateContext;
}
export class MyRootAppComponent extends RootAppComponent {
  appStateContext = MyAppStateContext;
}

Then everywhere you would use AppComponent or RootAppComponent, you can instead use MyAppComponent or MyRootAppComponent from that file's exports.

Using Amorphous with Flow

Amorphous has full support for flow types.

The relevant type information for RootAppComponent and AppComponent is:

class RootAppComponent<Props, State, AppState: Object>
  extends React.Component<Props, State> {

}

class AppComponent<Props, State, AppState: Object>
  extends React.Component<Props, State> {

}

To use these types, you can specify Props, State, and AppState types, and use these in your component declarations. We recommend creating your AppState type in its own file, which can be included from all your components.

import { RootAppComponent } from 'amorphous';
import type { AppState } from './my-app-state-type.js';

type Props = {
  mode: string,
};

type State = { };

class App extends RootAppComponent<Props, State, AppState> {

  appState: AppState = {
    // ...
  };

  render() {
    // ...
  }
}