41

GitHub - ctrlplusb/easy-peasy: Easy peasy global state for React

 5 years ago
source link: https://github.com/ctrlplusb/easy-peasy
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

README.md

Note: this package depends on the new Hooks feature of React. Currently available via 16.7.0-alpha.0 of React.

68747470733a2f2f692e696d6775722e636f6d2f4b4854675076412e706e67

Easy peasy global state for React

npm MIT License Travis Codecov

import { StoreProvider, createStore, useStore, useAction } from 'easy-peasy';

// ? create your store
const store = createStore({
  todos: {
    items: ['Install easy-peasy', 'Build app', 'Profit'],
    // ? define actions
    add: (state, payload) => {
      state.items.push(payload) // ? you mutate state to update (we convert
                                //    to immutable updates)
    }
  }
});

const App = () => (
  // ? surround your app with the provider to expose the store to your app
  <StoreProvider store={store}>
    <TodoList />
  </StoreProvider>
)

function TodoList() {
  // ? use hooks to get state or actions. you component will automatically
  //    receive updated state
  const todos = useStore(state => state.todos.items)
  const add = useAction(actions => actions.todos.add)
  return (
    <div>
      {todos.map((todo, idx) => <div key={idx}>{todo.text}</div>)}
      <AddTodo onAdd={add} />
    </div>
  )
}

Features

  • Quick to set up, easy to use
  • Update state via simple mutations (thanks immer)
  • Derived state
  • Async actions for remote data fetching/persisting
  • Provides React Hooks to interact with the store ?
  • Powered by Redux
  • Add custom Redux middleware
  • Supports Redux Dev Tools
  • Outputs a standard Redux store for easy integration

 

68747470733a2f2f692e696d6775722e636f6d2f327646537931792e706e67

 

TOCs


Introduction

Easy Peasy gives you the power of Redux and its tooling whilst avoiding the boilerplate. It allows you to create a full Redux store by defining a simple model (object) to describe your state and it's actions.

Easy Peasy outputs a Redux store, which means you can integrate with frameworks like React. Read the integration docs below.


Installation

Firsly, ensure you have the correct versions of React (i.e. a version that supports Hooks) installed.

npm install [email protected]
npm install [email protected]

Then install Easy Peasy.

npm install easy-peasy

Ok, thats it. No more "extra" dependencies for boosted features. It's an all-in-one package. Easy peasy.


Examples

React Todo List

A simple/naive implementation of a todo list.

https://codesandbox.io/s/woyn8xqk15


Core Concepts

The below will introduce you step by step to all the core concepts of Easy Peasy. At first we will interact with the store directly (remember we output a standard Redux store). After you gain this understanding we show you how to integrate Easy Peasy into your React application.

Setting up your store

Firstly you need to define your model. This represents the structure of your Redux store along with the default values. It can be as deep and complex as you like.

const model = {
  todos: {
    items: [],
  }
};

Then you provide your model to createStore.

import { createStore } from 'easy-peasy';

const store = createStore(model);

You will now have a Redux store. ?

Accessing state directly via the store

You can access your store's state using the getState API of the store.

store.getState().todos.items;

Modifying state via actions

In order to mutate your state you need to define an action against your model.

const store = createStore({
  todos: {
    items: [],
    // ? our action
    addTodo: (state, payload) => {
      //    Mutate the state directly. Under the hood we convert this to an
      //    an immutable update in the store, but at least you don't need to
      //    worry about being careful to return new instances etc. This also
      // ? makes it easy to update deeply nested items.
      state.items.push(payload)
    }
  }
});

The action will receive as it's first parameter the slice of the state that it was added to. So in the example above our action would receive { items: [] } as the value for state. It will also receive any payload that may have been provided when the action was triggered.

Dispatching actions directly via the store

Easy Peasy will bind your actions against the store's dispatch using a path that matches where the action lives within your model. You can dispatch your actions directly via the store, providing any payload that they may require.

store.dispatch.todos.addTodo('Install easy-peasy');
//            |-------------|
//                  |-- path matches our model (todos.addTodo)

Check your state and you should see that it is updated.

store.getState().todos.items;
// ['Install easy-peasy']

Creating an effect action

If you wish to perform side effects, such as fetching or persisting from your server then you can use the effect helper to declare an effectful action.

import { effect } from 'easy-peasy'; // ? import the helper

const store = createStore({
  todos: {
    items: [],

    //          ? define an action surrounding it with the helper
    saveTodo: effect(async (actions, payload) => {
      //                      ?
      // Notice that an effect will receive the actions allowing you to dispatch
      // other actions after you have performed your side effect.
      const saved = await todoService.save(payload);
      // ? Now we dispatch an action to add the saved item to our state
      actions.todos.todoSaved(saved);
    }),

    todoSaved: (state, payload) => {
      state.items.push(payload)
    }
  }
});

As you can see in the example above you can't modify the state directly within an effect action, however, the effect action is provided actions, allowing you dispatch actions to update the state where required.

Dispatching an effect action directly via the store

You dispatch an effectful action in the same manner as a normal action. However, an effect action always returns a Promise allowing you to chain commands to execute after the effect action has completed.

store.dispatch.todos.saveTodo('Install easy-peasy').then(() => {
  console.log('Todo saved');
})

Deriving state

If you have state that can be derived from state then you can use the select helper. Simply attach it to any part of your model.

import { select } from 'easy-peasy'; // ? import then helper

const store = createStore({
  shoppingBasket: {
    products: [{ name: 'Shoes', price: 123 }, { name: 'Hat', price: 75 }],
    totalPrice: select(state =>
      state.products.reduce((acc, cur) => acc + cur.price, 0)
    )
  }
}

The derived data will be cached and will only be recalculated when the associated state changes.

This can be really helpful to avoid unnecessary re-renders in your react components, especially when you do things like converting an object map to an array in your connect. Typically people would use reselect to alleviate this issue, however, with Easy Peasy it's this feature is baked right in.

You can attach selectors to any part of your state. Similar to actions they will receive the local state that they are attached to and can access all the state down that branch of state.

Accessing Derived State directly via the store

You can access derived state as though it were a standard piece of state.

store.getState().shoppingBasket.totalPrice

Note! See how we don't call the derived state as a function. You access it as a simple property.

Final notes

Now that you have gained an understanding of the store we suggest you read the section on Usage with React to learn how to use Easy Peasy in your React apps.

Oh! And don't forget to install the Redux Dev Tools Extension to visualise your actions firing along with the associated state updates. ?


Usage with React

Wrap your app with StoreProvider

Firstly we will need to create your store and wrap your application with the StoreProvider.

import { StoreProvider, createStore } from 'easy-peasy';
import model from './model'

const store = createStore(model);

const App = () => (
  <StoreProvider store={store}>
    <TodoList />
  </StoreProvider>
)

Consuming state in your Components

In order to use state within your components you can use the useStore hook.

import { useStore } from 'easy-peasy';

const TodoList = () => {
  const todos = useStore(state => state.todos.items);
  return (
    <div>
      {todos.map((todo, idx) => <div key={idx}>{todo.text}</div>)}
    </div>
  );
};

Firing actions in your Components

In order to fire actions in your components you can use the useAction hook.

import { useState } from 'react';
import { useAction } from 'easy-peasy';

const AddTodo = () => {
  const [text, setText] = useState('');
  const addTodo = useAction(actions => actions.todos.add);
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={() => addTodo(text)}>Add</button>
    </div>
  );
};

Alternative usage via react-redux

As Easy Peasy outputs a standard Redux store it is entirely possible to use Easy Peasy with the official react-redux package.

First, install the react-redux package

npm install react-redux

Then wrap your app with the Provider

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'easy-peasy';
import { Provider } from 'react-redux'; // ? import the provider
import model from './model';
import TodoList from './components/TodoList';

// ? then create your store
const store = createStore(model);

const App = () => (
  // ? then pass it to the Provider
  <Provider store={store}>
    <TodoList />
  </Provider>
)

render(<App />, document.querySelector('#app'));

Finally, use connect against your components

import React, { Component } from 'react';
import { connect } from 'react-redux'; // ? import the connect

function TodoList({ todos, addTodo }) {
  return (
    <div>
      {todos.map(({id, text }) => <Todo key={id} text={text} />)}
      <AddTodo onSubmit={addTodo} />
    </div>
  )
}

export default connect(
  // ? Map to your required state
  state => ({ todos: state.todos.items }
  // ? Map your required actions
  dispatch => ({ addTodo: dispatch.todos.addTodo })
)(EditTodo)

API

Below is an overview of the API exposed by Easy Peasy.

createStore(model, config)

Creates a Redux store based on the given model. The model must be an object and can be any depth. It also accepts an optional configuration parameter for customisations.

Arguments

  • model (Object, required)

    Your model representing your state tree, and optionally containing action functions.

  • config (Object, not required)

    Provides custom configuration options for your store. It supports the following options:

    • devTool (bool, not required, default=false)

      Setting this to true will enable the Redux Dev Tools Extension.

    • initialState (Object, not required, default=undefined)

      Allows you to hydrate your store with initial state (for example state received from your server in a server rendering context).

    • middleware (Array, not required, default=[])

      Any additional middleware you would like to attach to your Redux store.

Example

import { createStore } from 'easy-peasy';

const store = createStore({
  todos: {
    items: [],
    addTodo: (state, text) => {
      state.items.push(text)
    }
  },
  session: {
    user: undefined,
  }
})

action

A function assigned to your model will be considered an action, which can be be used to dispatch updates to your store.

The action will have access to the part of the state tree where it was defined.

It has the following arguments:

  • state (Object, required)

    The part of the state tree that the action is against. You can mutate this state value directly as required by the action. Under the hood we convert these mutations into an update against the Redux store.

  • payload (Any)

    The payload, if any, that was provided to the action.

When your model is processed by Easy Peasy to create your store all of your actions will be made available against the store's dispatch. They are mapped to the same path as they were defined in your model. You can then simply call the action functions providing any required payload. See the example below.

Example

import { createStore } from 'easy-peasy';

const store = createStore({
  todos: {
    items: [],
    add: (state, payload) => {
      state.items.push(payload)
    }
  },
  user: {
    preferences: {
      backgroundColor: '#000',
      changeBackgroundColor: (state, payload) => {
        state.backgroundColor = payload;
      }
    }
  }
});

store.dispatch.todos.add('Install easy-peasy');

store.dispatch.user.preferences.changeBackgroundColor('#FFF');

effect(action)

Declares an action on your model as being effectful. i.e. has asynchronous flow.

Arguments

  • action (Function, required)

    The action function to execute the effects. It can be asynchronous, e.g. return a Promise or use async/await. Effectful actions cannot modify state, however, they can dispatch other actions providing fetched data for example in order to update the state.

    It accepts the following arguments:

    • dispatch (required)

      The Redux store dispatch instance. This will have all the Easy Peasy actions bound to it allowing you to dispatch additional actions.

    • payload (Any, not required)

      The payload, if any, that was provided to the action.

    • additional (Object, required)

      An object containing additional helpers for the action when required. It has the following properties:

      • getState (Function, required)

        When executed it will provide the root state of your model. This can be useful in the cases where you require state in the execution of your effectful action.

When your model is processed by Easy Peasy to create your store all of your actions will be made available against the store's dispatch. They are mapped to the same path as they were defined in your model. You can then simply call the action functions providing any required payload. See the example below.

Example

import { createStore, effect } from 'easy-peasy'; // ? import then helper

const store = createStore({
  session: {
    user: undefined,
    // ? define your effectful action
    login: effect(async (dispatch, payload) => {
      const user = await loginService(payload)
      dispatch.session.loginSucceeded(user)
    }),
    loginSucceeded: (state, payload) => {
      state.user = payload
    }
  }
});

// ? you can dispatch and await on the effectful actions
store.dispatch.session.login({
  username: 'foo',
  password: 'bar'
})
// ? effectful actions _always_ return a Promise
.then(() => console.log('Logged in'));

select(selector)

Declares a section of state that is derived via the given selector function.

Arguments

  • selector (Function, required)

    The selector function responsible for resolving the derived state. It will be provided the following arguments:

    • state (Object, required)

      The local part of state that the select property was attached to.

  • dependencies (Array, not required)

    If this selector depends on other selectors your need to pass these selectors in here to indicate that is the case. Under the hood we will ensure the correct execution order.

Select's have their outputs cached to avoid unnecessary work, and will be executed any time their local state changes.

Example

import { select } from 'easy-peasy'; // ? import then helper

const store = createStore({
  shoppingBasket: {
    products: [{ name: 'Shoes', price: 123 }, { name: 'Hat', price: 75 }],
    // ? define your derived state
    totalPrice: select(state =>
      state.products.reduce((acc, cur) => acc + cur.price, 0)
    )
  }
};

// ? access the derived state as you would normal state
store.getState().shoppingBasket.totalPrice;

Example with Dependencies

import { select } from 'easy-peasy';

const totalPriceSelector = select(state =>
  state.products.reduce((acc, cur) => acc + cur.price, 0),
)

const netPriceSelector = select(
  state => state.totalPrice * ((100 - state.discount) / 100),
  [totalPriceSelector] // ? declare that this selector depends on totalPrice
)

const store = createStore({
  discount: 25,
  products: [{ name: 'Shoes', price: 160 }, { name: 'Hat', price: 40 }],
  totalPrice: totalPriceSelector,
  netPrice: netPriceSelector // price after discount applied
});

StoreProvider

Initialises your React application with the store so that your components will be able to consume and interact with the state via the useStore and useAction hooks.

Example

import { StoreProvider, createStore } from 'easy-peasy';
import model from './model'

const store = createStore(model);

const App = () => (
  <StoreProvider store={store}>
    <TodoList />
  </StoreProvider>
)

useStore

A hook granting your components access to the store's state. You need to provide it with a function which is used to resolved the piece of state that your component requires.

#### Example

import { useStore } from 'easy-peasy';

const TodoList = () => {
  const todos = useStore(state => state.todos.items);
  return (
    <div>
      {todos.map((todo, idx) => <div key={idx}>{todo.text}</div>)}
    </div>
  );
};

If you wish to access multiple pieces of state in the same component you can make multiple calls to useStore.

import { useStore } from 'easy-peasy';

const BasketTotal = () => {
  const totalPrice = useStore(state => state.basket.totalPrice);
  const netPrice = useStore(state => state.basket.netPrice);
  return (
    <div>
      <div>Total: {totalPrice}</div>
      <div>Net: {netPrice}</div>
    </div>
  );
};

useAction

A hook granting your components access to the store's actions. You need to provide it with a function which is used to resolved the action that your component requires.

Example

import { useState } from 'react';
import { useAction } from 'easy-peasy';

const AddTodo = () => {
  const [text, setText] = useState('');
  const addTodo = useAction(actions => actions.todos.add);
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={() => addTodo(text)}>Add</button>
    </div>
  );
};

If you wish to access multiple actions in the same component you can make multiple calls to useAction.


Prior art

This library was massively inspired by the following awesome projects. I tried to take the best bits I liked about them all and create this package. Huge love to all contributors involved in the below.

  • rematch

    Rematch is Redux best practices without the boilerplate. No more action types, action creators, switch statements or thunks.

  • react-easy-state

    Simple React state management. Made with ❤️ and ES6 Proxies.

  • mobx-state-tree

    Model Driven State Management


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK