5.1.0 • Published 6 years ago

preact-easy-state v5.1.0

Weekly downloads
2
License
MIT
Repository
github
Last release
6 years ago

React Easy State

Simple React state management. Made with :heart: and ES6 Proxies.

Build Coverage Status JavaScript Style Guide Package size Version dependencies Status License

Breaking change in v5: the auto bind feature got removed. See the alternatives for your components at the official React docs and for you stores at the FAQ docs section.

Introduction

Easy State has two rules.

  1. Always wrap your components with view.
  2. Always wrap your state store objects with store.
import React from 'react';
import { store, view } from 'react-easy-state';

const clock = store({ time: new Date() });
setInterval(() => (clock.time = new Date()), 1000);

export default view(() => <div>{clock.time.toString()}</div>);

This is enough for it to automatically update your views when needed - no matter how exotically you mutate your state stores. With this freedom you can invent and use your personal favorite state management patterns.

Installation

npm install react-easy-state

Easy State supports Create React App without additional configuration. Just run the following commands to get started.

npx create-react-app my-app
cd my-app
npm install react-easy-state
npm start

You need npm 5.2+ to use npx.

Usage

Creating stores

store creates a state store from the passed object and returns it. State stores are just like normal JS objects. (To be precise, they are transparent reactive proxies of the original object.)

import { store } from 'react-easy-state';

const user = store({
  name: 'Rick'
});

// stores behave like normal JS objects
user.name = 'Bob';

Creating reactive views

Wrapping your components with view turns them into reactive views. A reactive view re-renders whenever a store's property - used inside its render - changes.

import React, { Component } from 'react';
import { view, store } from 'react-easy-state';

const user = store({ name: 'Bob' });

class HelloComp extends Component {
  onChange = ev => (user.name = ev.target.value);

  // the render is triggered whenever user.name changes
  render() {
    return (
      <div>
        <input value={user.name} onChange={this.onChange} />
        <div>Hello {user.name}!</div>
      </div>
    );
  }
}

// the component must be wrapped with `view`
export default view(HelloComp);

Make sure to wrap all of your components with view - including stateful and stateless ones. If you do not wrap a component, it will not properly render on store mutations.

view can also be used as a class decorator with the @view syntax. You can learn more about decorators here.

import React, { Component } from 'react';
import { view, store } from 'react-easy-state';

const user = store({
  name: 'Bob'
});

@view
class HelloComp extends Component {
  onChange = ev => (user.name = ev.target.value);

  // the render is triggered whenever user.name changes
  render() {
    return (
      <div>
        <input value={user.name} onChange={this.onChange} />
        <div>Hello {user.name}!</div>
      </div>
    );
  }
}

Decorators are not a standardized JavaScript feature and create-react-app does not support them yet.

Creating local stores

A singleton global store is perfect for something like the current user, but sometimes having local component states is a better fit. Just create a store as a component property in these cases.

import React, { Component } from 'react';
import { view, store } from 'react-easy-state';

class ClockComp extends Component {
  clock = store({ time: new Date() });

  componentDidMount() {
    setInterval(() => (this.clock.time = new Date()), 1000);
  }

  render() {
    return <div>{this.clock.time}</div>;
  }
}

export default view(ClockComp);

That's it, You know everything to master React state management! Check some of the examples and articles for more inspiration or the FAQ section for common issues.

Examples with live demos

Beginner

Advanced

Articles

FAQ and Gotchas

What triggers a re-render?

Easy State monitors which store properties are used inside each component's render method. If a store property changes, the relevant renders are automatically triggered. You can do anything with stores without worrying about edge cases. Use nested properties, computed properties with getters/setters, dynamic properties, arrays, ES6 collections and prototypal inheritance - as a few examples. Easy State will monitor all state mutations and trigger renders when needed. (Big cheer for ES6 Proxies!)

When do renders run?

Triggered renders are passed to React for execution, there is no forceUpdate behind the scenes. This means that component lifecycle hooks behave as expected and that React Fiber works nicely together with Easy State. On top of this, you can use your favorite testing frameworks without any added hassle.

My component renders multiple times unnecessarily

If you mutate your stores inside React event handlers, this will never happen.

If you mutate your stores multiple times synchronously from outside event handlers, it can happen though. You can wrap the mutating code with ReactDOM.flushSync to batch the updates and trigger a single re-render only. (It works similarly to MobX's actions.)

import React from 'react';
import ReactDOM from 'react-dom';
import { view, store } from 'react-easy-state';

const user = store({ name: 'Bob', age: 30 });

function mutateUser() {
  user.name = 'Ann';
  user.age = 32;
}

// this renders the component 2 times
mutateUser();
// this renders the component only once, after all the mutations
ReactDOM.flushSync(mutateUser);

// clicking on the inner div renders the component only once,
// because mutateUser is invoked as an event handler
export default view(() => (
  <div onClick={mutateUser}>
    name: {user.name}, age: {user.age}
  </div>
));

This will not be necessary once React's new scheduler is ready. It currently batches setState calls inside event handlers only, but this will change soon.

We realize it's inconvenient that the behavior is different depending on whether you're in an event handler or not. This will change in a future React version that will batch all updates by default (and provide an opt-in API to flush changes synchronously). Until we switch the default behavior (potentially in React 17), there is an API you can use to force batching.

You can find the whole post by Dan Abramov here. Once the new default React scheduler is ready, you won't have to worry about multiple renders. Until then you can use ReactDOM.flushSync or just let it go, if you do not experience performance issues.

How do I derive local stores from props (getDerivedStateFromProps)?

Components wrapped with view have an extra static deriveStoresFromProps lifecycle method, which works similarly to the vanilla getDerivedStateFromProps.

import React, { Component } from 'react';
import { view, store } from 'react-easy-state';

class NameCard extends Component {
  userStore = store({ name: 'Bob' });

  static deriveStoresFromProps(props, userStore) {
    userStore.name = props.name || userStore.name;
  }

  render() {
    return <div>{this.userStore.name}</div>;
  }
}

export default view(NameCard);

Instead of returning an object, you should directly mutate the passed in stores. If you have multiple local stores on a single component, they are all passed as arguments - in their definition order - after the first props argument.

My store methods are broken

You should not use the this keyword in the methods of your state stores.

const counter = store({
  num: 0,
  increment() {
    this.num++;
  }
});

export default view(() => <div onClick={counter.increment}>{counter.num}</div>);

The above snippet won't work, because increment is passed as a callback and loses its this. You should use the direct object reference - counter in this case - instead of this.

const counter = store({
  num: 0,
  increment() {
    counter.num++;
  }
});

This works as expected, even when you pass store methods as callbacks.

My views are not rendering

You should wrap your state stores with store as early as possible to make them reactive.

const person = { name: 'Bob' };
person.name = 'Ann';

export default store(person);

The above example wouldn't trigger re-renders on the person.name = 'Ann' mutation, because it is targeted at the raw object. Mutating the raw - none store wrapped object - won't schedule renders.

Do this instead of the above code.

const person = store({ name: 'Bob' });
person.name = 'Ann';

export default person;

Naming local stores as state

Naming your local state stores as state may conflict with React linter rules, which guard against direct state mutations. Please use a more descriptive name instead.

Platform support

  • Node: 6 and above
  • Chrome: 49 and above
  • Firefox: 38 and above
  • Safari: 10 and above
  • Edge: 12 and above
  • Opera: 36 and above
  • React Native: iOS 10 and above and Android with community JSC
  • IE is not supported and never will be

This library is based on non polyfillable ES6 Proxies. Because of this, it will never support IE.

React Native is supported on iOS and Android is supported with the community JavaScriptCore. Learn how to set it up here. It is pretty simple.

Performance

You can compare Easy State with plain React and other state management libraries with the below benchmarks. It performs a bit better than MobX and a bit worse than Redux.

How does it work?

Under the hood Easy State uses the @nx-js/observer-util library, which relies on ES6 Proxies to observe state changes. This blog post gives a little sneak peek under the hood of the observer-util.

Alternative builds

This library detects if you use ES6 or commonJS modules and serve the right format to you. The exposed bundles are transpiled to ES5 to support common tools - like UglifyJS minifying. If you would like a finer control over the provided build, you can specify them in your imports.

  • react-easy-state/dist/es.es6.js exposes an ES6 build with ES6 modules.
  • react-easy-state/dist/es.es5.js exposes an ES5 build with ES6 modules.
  • react-easy-state/dist/cjs.es6.js exposes an ES6 build with commonJS modules.
  • react-easy-state/dist/cjs.es5.js exposes an ES5 build with commonJS modules.

If you use a bundler, set up an alias for react-easy-state to point to your desired build. You can learn how to do it with webpack here and with rollup here.

Contributing

Contributions are always welcome. Just send a PR against the master branch or open a new issue. Please make sure that the tests and the linter pass and the coverage remains decent. Thanks!