Reducerless Redux
Redux middleware for intuitively mapping urls to state
Installation
npm install --save reducerless-redux
This assumes that you’re using npm package manager with a module bundler like Webpack or Browserify to consume CommonJS modules.
The following ES6 functions are required:
Check the compatibility tables (Object.assign
, Promise
, fetch
) to make sure all browsers and platforms you need to support have these, and include polyfills as necessary.
Motivation
This project was inspired by React-Refetch. I like the intuitive way that react-refetch maps url's to props, but I found myself wanting at the same time to leverage redux and react-redux connect's easy way of spreading data around nested heirarchies of components. The thing that turns me off about redux, however, is the proliferation of reducers. They always felt cumbersome, especially since the vast majority of the time all I want to is modify a particular piece of state. To that end I've created a redux middleware and store enhancer that maps url's to state, using a single "invisible" reducer. I also like the PromiseState that react-refetch uses, so all actions result in PromiseStates. The state to props mapping can be done by react-redux connect, which already does a great job at that. I thought about wrapping connect with something that would give you automatic fetching and refetching, but decided against it because the same thing can be achieved with recompose in a more explicit way, without that much more code.
Example
Imagine we have a list of foos we want to display. Each foo has details, which we want to display next to the list for the selected foo.
// index.jsimport React Component from 'react';import render from 'react-dom';import Provider form 'react-redux';import createStore applyMiddleware compose from 'redux';import reducerlessMiddleware reducerlessEnhancer from 'reducerless-redux';import im from 'object-path-immutable';import App from './my-app'; const store = ; ;
// App.js - use lifecycle methodsimport React Component from 'react';import connect from 'react-redux'; { thisprops; } { if thispropsfooId !== nextPropsfooId thisprops; } { const foos = thisprops; if foospending return <Loading /> else if foosrejected return <Error .../> else return <div> <FoosList = /> <FooDetail /> </div> ; } state props foos: 'foos' selFoo: 'selectedFoo' url: '/api/foos' key: 'foos' url: `/api/foos/` key: 'selectedFoo' FoosPage;
// App.js - use recompose to automatically fetch and updateimport compose lifecycle from 'recompose'; const FoosPage = foos if foospending return <Loading /> else if foosrejected return <Error .../> else return <div> <FoosList = /> <FooDetail ... /> </div> ; FoosPage
Middleware options
// how do we map a key to state? This gives you flexibilty in what you set the 'key' property // to in your actions (See action api below). Any function that can immutably update state // can work here, for example: updeep, dot-prop, or object-path-immutable // This is optional, but the key can only be a single identifier if this isn't defined. { } // how do we get state from a key? => { } // modify fetch options for all requests. Return the modified options. // Usefull for setting auth headers, etc. { }
Action API
// the url for the request. If this url is pending, it will not be fetched again. url: '/api/foos' // the key of our state in which to put the result of the http request. // The result will be stored inside of a PromiseState. See react-refetch for // more documentation about PromiseStates. // Will be used by the 'setKey' function (see above) defined in the middleware key: 'path' 'to' 'prop' // synchronously update state however you want. // Meant to be used on its own without url or key. => im.set(state, ['hello'], 'kitty') // transform the result of a request. Return the transformed result transform: data => { datamyprop = 'hello'; return data; } // http method method: 'GET' | 'POST' | 'PUT' | 'DELETE' // etc. // body of request body: ... // do other stuff when the request is fulfilled { // update a piece of state with the result // or re-fetch a piece of state ; } // override how the response itself is handled. Default is response.json() response // poll the url at the given interval. // Any actions that specify the same url as this one will not be fetched // while this action is in the refresh queue refreshInterval: 5000 // every 5 seconds // clear all polling actions clearRefresh: true // retry an action with the given backoff maxRetry: 3 // retry 3 times, then dispatch the error from the last failed attempt retryBackoff: 1000 // wait for this long before trying again: attempt number * 1000 // cache an key-url combination for the specified ammount of time. cacheFor: 1000 * 60 * 10 // don't fetch the action url if already fetched within 10 mins