All Projects → shakacode → Redux Tree

shakacode / Redux Tree

An alternative way to compose Redux reducers

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Redux Tree

atomic-state
A decentralized state management library for React
Stars: ✭ 54 (+134.78%)
Mutual labels:  state-management, state
Govern
Component-based state management for JavaScript.
Stars: ✭ 270 (+1073.91%)
Mutual labels:  state-management, state
Nanny-State
simple state management
Stars: ✭ 68 (+195.65%)
Mutual labels:  state-management, state
closeable-map
Application state management made simple: a Clojure map that implements java.io.Closeable.
Stars: ✭ 42 (+82.61%)
Mutual labels:  state-management, state
Machinery
State machine thin layer for structs (+ GUI for Phoenix apps)
Stars: ✭ 367 (+1495.65%)
Mutual labels:  state-management, state
react-wisteria
Managing the State with the Golden Path
Stars: ✭ 18 (-21.74%)
Mutual labels:  state-management, state
alveron-old
Opinionated Elm-inspired Redux Component Architecture for React
Stars: ✭ 17 (-26.09%)
Mutual labels:  state-management, state
knockout-store
State management for Knockout apps.
Stars: ✭ 37 (+60.87%)
Mutual labels:  state-management, state
Effector
The state manager ☄️
Stars: ✭ 3,572 (+15430.43%)
Mutual labels:  state-management, state
Redux Orm
A small, simple and immutable ORM to manage relational data in your Redux store.
Stars: ✭ 2,922 (+12604.35%)
Mutual labels:  state-management, state
xstate-viz
Visualizer for XState machines
Stars: ✭ 274 (+1091.3%)
Mutual labels:  state-management, state
Pullstate
Simple state stores using immer and React hooks - re-use parts of your state by pulling it anywhere you like!
Stars: ✭ 683 (+2869.57%)
Mutual labels:  state-management, state
stoxy
Stoxy is a state management API for all modern Web Technologies
Stars: ✭ 73 (+217.39%)
Mutual labels:  state-management, state
Little State Machine
📠 React custom hook for persist state management
Stars: ✭ 654 (+2743.48%)
Mutual labels:  state-management, state
eventrix
Open-source, Predictable, Scaling JavaScript library for state managing and centralizing application global state. State manage system for react apps.
Stars: ✭ 35 (+52.17%)
Mutual labels:  state-management, state
k-ramel
State manager for your components apps, the safe and easy way
Stars: ✭ 20 (-13.04%)
Mutual labels:  state-management, state
PageStatusTransformer
A low invasive state management on Android
Stars: ✭ 12 (-47.83%)
Mutual labels:  state-management, state
RxReduxK
Micro-framework for Redux implemented in Kotlin
Stars: ✭ 65 (+182.61%)
Mutual labels:  state-management, state
React Recomponent
🥫 Reason-style reducer components for React using ES6 classes.
Stars: ✭ 272 (+1082.61%)
Mutual labels:  state-management, state
Westore
更好的小程序项目架构
Stars: ✭ 3,897 (+16843.48%)
Mutual labels:  state-management, state

redux-tree

npm version build status dependencies status license

An alternative way to compose Redux reducers.

Interactions pattern

redux-tree is a part of the interactions pattern. Check it out to get more context.

Table of Contents

State as a Tree

Application state can be represented as a tree, which is a combination of branches and leaves. A branch doesn’t hold any state itself but is a grouping of leaves that each hold chunks of the application state. If state is flat, then tree consists only of leaves.

state:
  entities:
    posts: { index, entities }
    comments: { entities }
  ui:
    postsList: { processingPosts }
    ...

For example, the branch state.entities groups the states of the posts leaf, comments leaf, etc.

Let’s say a user manages his posts and removes one of them by clicking the “Delete” button. What’s happening under the hood? The state of this UI part is stored in the state.ui.postsList leaf. Clicking on the button, a user triggers an action creator and the app starts a request to the server. In response to this action, postId is added to the processingPosts set to show the spinner in the UI. It requires a change of the single ui.postsList leaf. Let’s describe it in the interaction module:

// Action creator: returns request action
const requestAction = postId => ({
  type: 'POST_DELETE_REQUESTED',
  postId,
});

// Action handler: reduces the state of the single leaf
const onRequest = {
  POST_DELETE_REQUESTED:
    (state, { postId }) =>
      state.update('processingPosts', processingPosts => processingPosts.add(postId)),
};

When a server responds with a success:

  • postId must be removed from the processingPosts
  • post entity must be removed from the entities.posts leaf. 

This action entails changing 2 different leaves:

// Action creator: returns success action
const successAction = postId => ({
  type: 'POST_DELETE_SUCCEEDED',
  postId,
});

// Action handlers: passing array of the reducers for this action type
//                  to apply sequence of the changes to the state tree
const onSuccess = {
  POST_DELETE_SUCCEEDED: [
    // 1. hide spinner
    (state, { postId }) =>
      state.update('processingPosts', processingPosts => processingPosts.delete(postId)),

    // 2. remove post entity
    {
      leaf: ['entities', 'posts'], // <= keypath to the leaf of the state
      reduce:
        (postsEntitiesState, { postId }) =>
          postsEntitiesState
            .updateIn(['index'], index => index.delete(postId))
            .updateIn(['entities'], entities => entities.delete(postId)),
    },
  ],
};

To make this code work, few internal changes are required in how Redux iterates over the reducers. Under the hood, redux-tree is an alternative version of Redux’s combineReducers, which makes it possible to represent changes to the state as a sequence of functions. This allows describing interactions in a very concise and consistent manner.

It’s super easy to integrate redux-tree into existing codebases as it supports classic reducers (so incremental adoption is absolutely possible, see Migration) and it should be compatible with the most of the packages from Redux ecosystem. The main change it introduces is how Redux internally iterates over the reducers.

In the initial release of redux-tree, state is represented as an Immutable Record. We use Immutable a lot in our apps; it makes it easier to handle deep updates and prevent state mutations, Record allows access to properties using dot notation (as opposed to getters), and it’s possible to strongly type the state tree with flow. So, immutable-js is required (at least for now).

Installation

# yarn / npm
yarn add redux-tree
npm install --save redux-tree

# don't forget to install redux & immutable
yarn add redux immutable
npm install --save redux immutable

Examples

  • Counter
import { createStore } from 'redux';
import { createTree, createLeaf } from 'redux-tree';

const tree = createTree({
  counter: createLeaf(0, {
    INCREMENT: state => state + 1,
    DECREMENT: state => state - 1,
  }),
});

const store = createStore(tree);

store.subscribe(() => console.log(store.getState()));

store.dispatch({ type: 'INCREMENT' }); // => { counter: 1 }
store.dispatch({ type: 'INCREMENT' }); // => { counter: 2 }
store.dispatch({ type: 'DECREMENT' }); // => { counter: 1 }

API

redux-tree exposes 3 modules:

createTree

createTree receives 1 argument: object with branches and/or leaves. Returns a tree, in fact this is a root reducer.

import { createTree } from 'redux-tree';

const tree = createTree({
  auth: authLeaf,
  entities: entitiesBranch,
  ui: uiBranch,
});

type Tree = (state: State, action: Action) => State;
type CreateTree = ({ [key: string]: Branch | Leaf }) => Tree;

// Then pass it to redux's `createStore` instead of reducer
const store = createStore(tree, initialState, enhancers);

createBranch

createBranch also receives 1 argument: object with branches and/or leaves. You don't need this method if your state tree is 1 level deep. Returns a branch.

import { createBranch } from 'redux-tree';

const branch = createBranch({
  posts: postsLeaf,
  comments: commentsLeaf,
})

type CreateBranch = ({ [key: string]: Branch | Leaf }) => Branch;

createLeaf

You can pass 2 types of arguments to createLeaf:

  1. Single argument: classic reducer function, which takes state of the leaf and action and returns next leaf state.
  2. Two arguments: initial state of the leaf and object with action handlers.
import { createLeaf } from 'redux-tree';

const leaf = createLeaf(initialState, actionHandlers);
const leaf = createLeaf(reducer);

type CreateLeaf =
  | (initialState: LeafState, actionHandlers?: ActionHandlers) => Leaf
  | (reducer: Reducer) => Leaf
;

Action handlers

Action handlers are stored in an object. Its keys are action types, and values are action handlers (reducers). You can define reducers in the following ways:

As a function. It receives the state of the leaf to which it was initially passed and dispatched action. Must return a state of the leaf.

{ ACTION_TYPE: (state, action) => state }

As an object with:

  • leaf: keypath to the leaf in the state
  • reduce: reducer, which receives state of the leaf at provided keypath and dispatched action. Must return a state of the leaf.
{
  ACTION_TYPE: {
    leaf: ['path', 'to', 'leaf'],
    reduce: (state, action) => state,
  },
}

As an array of the previous two. Useful when you need to change the state of the multiple leaves in response to single action.

{
  ACTION_TYPE: [
    (state, action) => state, // function receives state of the local leaf

    {
      leaf: ['path', 'to', 'leaf'],
      reduce: (state, action) => state, // receives the state of the leaf at the keypath
    },
  ],
}

Migration

To integrate redux-tree into existing codebase, no need to refactor all the reducers at once. You can pass existing ones to the createLeaf and refactor them incrementally:

import { createLeaf } from 'redux-tree';

function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

export default createLeaf(counterReducer);

Usage with combineReducers

It is possible to define tree as a child of vanilla reducer, created with combineReducers.

const rootReducer = combineReducers({
  vanillaReducers: combineReducers({
    vanillaReducer: (state, action) => state,
  }),
  tree: createTree({
    entities: createBranch({
      posts: createLeaf(initialState, actionHandlers),
    }),
  }),
});

Keep in mind that tree doesn't know anything about parents, and in action handler you can't change a state of an external leaf at keypath outside of the tree. All keypaths must be provided relative to the tree's root node. Basically, keypaths must be the same as if child tree would be a root reducer. But you still can respond to any action in any reducer in case you need this.

Thanks

To Alberto Leal for handing over the redux-tree NPM package name.

License

It's MIT.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].