Giter Site home page Giter Site logo

redux-dsm's Introduction

Redux DSM

Declarative State Machines for Redux: Reduce your async-state boilerplate.

Redux-dsm takes a nested array of transitions and states and automatically generates:

  • Reducers to manage all state transitions, complete with logic that will not let your app get into an invalid state (according to the graph you supply)
  • Action creators and action types to trigger all transitions

Status

In production use on DevAnywhere.io.

Install

npm install --save redux-dsm

And then in your file :

import dsm from 'redux-dsm';

Or using CommonJS modules :

var dsm = require('redux-dsm');

Why?

Your state isn't always available synchronously all the time. Some state has to be loaded asynchronously, which requires you to cycle through UI states representing concepts like fetching, processing, error, success, and idle states. In fact, a simple ajax fetch might have up to 7 transitions leading into 4 different states:

Transition        Next Status
['initial',           'idle']
['fetch',         'fetching']
['cancel',            'idle']
['report error',     'error']
['handle error',      'idle']
['report success', 'success']
['handle success',    'idle']

Your view code will look at the status and payload to determine whether or not to render spinners, success messages, error messages, empty states, or data. I don't know about you, but I sometimes forget some of those transitions or states.

Every app I've ever written needs to do this a bunch of times. Since I switched to Redux, I handle all of my view state transitions by dispatching action objects, and that requires writing a bunch of boilerplate, such as action types (e.g., myComponent::FETCH_FOO::FETCH), and action creators (which your view or service layers can call to create actions without forcing you to import all those action type constants everywhere).

This little library takes a few declarative inputs and spits out all of the boilerplate for you, including a mini reducer that you can combine with your feature-level reducers.

Can I Use it With x?

This library is not just for ajax, though that will be a very common use-case, and it doesn't care how you handle your async I/O. You can use it with Sagas, redux-thunks, action creators with side-effects, etc..., or just use it by itself and manually wire up your async I/O.

You don't even have to use it with Redux -- anything that uses reducer-based state is fine, including ngrx/store or even Array.prototype.reduce().

Usage Example

import dsm from 'redux-dsm';

// ['action', 'next state',
//   ['scoped action', 'another state']
// ]
// e.g., in the following example, from 'fetching' state, we can:
// * cancel
// * report an error
// * report success
const fetchingStates = ['initial', 'idle',
  ['fetch', 'fetching',
    ['cancel', 'idle'],
    ['report error', 'error',
      ['handle error', 'idle']
    ],
    ['report success', 'success',
      ['handle success', 'idle']
    ]
  ]
];

// ({
//   component?: String,
//   description?: String,
//   actionStates: Array,
//   delimiter?: String
// }) => { actions: Object, actionCreators: Object, reducer: Function }
const foo = dsm({
  component: 'myComponent',
  description: 'fetch foo',
  actionStates: fetchingStates
});

.actions: Object

actions is an object with camelCased keys and strings corresponding to your state transitions. If you use the returned .actionCreators, you probably don't need to use these, but it's handy for debugging. For the above example, it returns:

  "actions": {
    "fetch": "myComponent::FETCH_FOO::FETCH",
    "cancel": "myComponent::FETCH_FOO::CANCEL",
    "reportError": "myComponent::FETCH_FOO::REPORT_ERROR",
    "handleError": "myComponent::FETCH_FOO::HANDLE_ERROR",
    "reportSuccess": "myComponent::FETCH_FOO::REPORT_SUCCESS",
    "handleSuccess": "myComponent::FETCH_FOO::HANDLE_SUCCESS"
  }

.actionCreators: Object

actionCreators will be an object with camelCased keys and function values corresponding to your state transitions. For each transition, an action creator is created which will automatically fill in the correct action type, and pass through payload to the state.

The example fetch state machine will produce the following actionCreators:

fetch()
cancel()
reportError()
handleError()
reportSuccess()
handleSuccess()

.reducer: (state, action) => state

.reducer() is a normal Redux reducer function that takes the current state and an action object, and returns the new state. Matching action objects can be created using the .actionCreators. The reducer can be combined with a parent reducer using combineReducers(). Any payload passed into an action creator will be passed through to state.payload.

State: { status: String, payload: Any }

The state object will have two keys, status and payload. In the example above, status will be one of idle, fetching, error, or success.

By default, the payload key is an object with type: 'empty'.

redux-dsm's People

Contributors

dash- avatar ericelliott avatar kennylavender avatar renovate-bot avatar zouhir avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-dsm's Issues

Remove Babel dependencies.

I'm pretty sure that Node 6 supports generators and other ES6 features we use natively. Now that we've switched our module format from ESM to CommonJS, we can probably remove the babel dependencies safely.

Should provide selector

Here's some code from one of the projects I contributed.

// selectors for dsm
const getApplicationSubmitStatus = state => state.application.submitStatus.status;

const submitStatus = dsm({
  component: 'application',
  description: 'submit application',
  actionStates: submittingStatus
});

export default combineReducers({
  currentApplication: reducer,
  submitStatus: submitStatus.reducer
});

Maybe redux-dsm can provide a selector to get the status.

Fix imports

This is an ES6 module, meaning that you can't just import it in Node and start using it. You have to transpile or use babel-core/register prior to attempting to import it. Since this is a general-purpose library, I think the right approach is to switch to CommonJS module format, which AFAIK, is supported natively by Node, browserify, webpack, and JSPM, which I think covers all of the most common package managers in use today.

Todo:

  • 1. package.json needs to point to source/dsm.js
  • 2. Swap out import/export with require/module.exports. (example).
  • 3. No build step needed! Compiling for the intended targets is a job for the module users, not for us. We don't know the intended targets, so we don't know the right options to build for. If we give them the raw source, it gives users the flexibility and freedom they need to decide things like whether or not they need to bundle a generator runtime, etc...

Add snyk

Add snyk to watch for and automatically patch dependency vulnerabilities.

Create middleware to assure actions dispatched in right order

I worked on simple application which uses the redux-dsm library and I stumbled upon one problem.
Example code: #21

The app is very simple. I asynchronously load data from the server and then I render the component with the list of items fetched from the server. I have 4 states defined: IDLE, SUCCESS, ERROR and FETCHING. I basically copied the configuration from https://github.com/ericelliott/redux-dsm#usage-example. I use the reducer and action creators provided by the library (https://github.com/bognix/redux-dsm/blob/example-usage/examples/simpleFetch/dsm.js).

There is one part I feel uncomfortable with in my example:

const mapStateToProps = (state) => {
    const {list: {payload: {messages} = {}, status} = {}} = state;

    switch (status) {
        case STATUS.IDLE:
        case STATUS.SUCCESS:
            return {
                messages
            };
        case STATUS.ERROR: {
            return {
                error: true
            };
        }
        case STATUS.FETCHING:
        default:
            return {
                loading: true
            };
    }
};

When mapping state to props in redux connect I ended up with something that looks like a part of code that belongs to the reducer. However, because I use the reducer created by the library there is no obvious way (for me at least) to combine two reducers so they work on the same part of store. The reducer created by redux-dsm is responsible for assuring that actions are dispatched in proper order and update state only in such case. My reducer would be responsible for populating the state with correctly shaped data.

I thought about it and for me it seems that the redux-dsm reducer responsibility could be replaced with a middleware. So the redux-dsm library would return a middleware which when applied would assure that actions are dispatched in right order.

Before I dig into the implementation I would like to know what you think about it and maybe how to solve the problem I mentioned with different means.

Restrict state transitions according to graph

Should switch reducers based on the current state in order to ensure that the program can't enter bad states.

  • For each state, build a reducer that only responds to transitions listed for that state in the state graph
  • Create a parent/meta reducer that switches the invoked child reducer based on the current state

Sample state graph:

const fetchingStates = [
  ['initialize', 'idle',
    ['fetch', 'fetching',
      ['cancel', 'idle'],
      ['report error', 'error',
        ['handle error', 'idle']
      ],
      ['report success', 'success',
        ['handle success', 'idle']
      ]
    ]
  ]
];

Export statuses

It's useful to have the statuses available to import into components and unit tests.

Initial status is ignored.. 'idle' is always used.

This graph:

const actionStates = [
  ['initialize', 'signed out',
    ['sign in', 'authenticating'
      // ['report error', 'error',
      //   ['handle error', 'signed out']
      // ],
      // ['sign-in success', 'signed in']
    ]
  ]
];

should initialize to the 'signed out' state. Instead, it initializes to idle:

not ok 1 Given ["initialize", "signed out", /*...*/: should use "signed out" as initialized state
  ---
    operator: deepEqual
    expected: |-
      { status: 'signed out', payload: { type: 'empty' } }
    actual: |-
      { status: 'idle', payload: { type: 'empty' } }

I created a failing test that demonstrates the problem:

https://github.com/ericelliott/redux-dsm/blob/bug-initialState-ignored/source/test/authenticate-flow-test.js#L28

Accessing actions from custom middleware

Working on an example app, I'm wondering how I should best access a specific action? (Like from const action = new dsm(...).actions()). They could be hard-coded, but that seems like a fragile anti-pattern. I'd think there would be a way of using my original states (initialize, fetch, etc) but there doesn't really seem to be a good way to do this. If actions was exposed as a map, it could be actions.initialize or actions.fetch, for example.

Thoughts?

Code:

const dsm = require('redux-dsm');

const fetchingStates = [
  ['initialize', 'idle',
    ['fetch', 'fetching',
      ['cancel', 'idle'],
      ['report error', 'error',
        ['handle error', 'idle']
      ],
      ['report success', 'success',
        ['handle success', 'idle']
      ]
    ]
  ]
];

const myDsm = dsm({
  component: 'myComponent',
  description: 'fetch foo',
  actionStates: fetchingStates
});

const actions = myDsm.actions;

const customMiddleware = store => next => action => {

Assuming I want to handle a specific action, how do I access it?

}```

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Edited/Blocked

These updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

npm
package.json
  • lodash.camelcase 4.3.0
  • lodash.snakecase 4.1.1
  • @std/esm 0.26.0
  • babel-cli 6.26.0
  • babel-preset-es2015 6.24.1
  • colors 1.3.2
  • eslint 5.6.1
  • lodash 4.17.11
  • riteway 3.0.0
  • snyk 1.101.1
  • tape 4.9.1
  • updtr 3.1.0
  • watch 1.0.2
  • node >=6.0.0
travis
.travis.yml
  • node 8

  • Check this box to trigger a request for Renovate to run again on this repository

Port to ES5?

As mentioned elsewhere ES6 is going to be troublesome because many platforms won't support ES6.

This library uses a generator, which means that compiling to ES5 will mean that the targets will need to have a generator runtime installed.

We can avoid all the mess of compiling if we just write the module in ES5 to begin with. =)

Pros? Cons?

Can't use the same action type in more than one graph state

Problem: Graphs that need multiple handlers for the same action type fail, e.g.:

const actionStates = ['initial', SIGNED_OUT,
  ['sign in', AUTHENTICATING,
    ['report sign in failure', ERROR,
      ['handle sign in failure', SIGNED_OUT]
    ],
    ['report sign in success', SIGNED_IN,
      ['sign out', SIGNED_OUT]
    ]
  ],
  ['report sign in success', SIGNED_IN,
    ['sign out', SIGNED_OUT]
  ]
];

In this case, reportSignInSuccess() should be valid in multiple states, "signed out" or "authenticating". Currently, there can only be one handler per action type. The last one to be processed by the graph wins, causing the previous one to stop working.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.