Automatically generate Redux stories and actions from your folder and file structure
MIT License
I created this utility to allow you to get up and running with Redux in a fraction of the time!
In Redux your reducer returns a state object. This is very straight forward, but makes dealing with asynchronous updates quite tricky (there are more than 60 different libraries tackling this problem).
redux-auto fixes this asynchronous problem simply by allowing you to create an "action" function that returns a promise. To accompany your "default" function action logic.
Steps:
Example layout:
βββ store/ (1)
βββuser/ (2)
βββ index.js (3)
βββ changeName.js (4)
βββ init.js (5)
...
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { auto, reducers } from 'redux-auto';
...
// load the folder that hold you store
const webpackModules = require.context("./store", true, /\.js$/);
...
// build 'auto' based on target files via Webpack
const middleware = applyMiddleware( auto(webpackModules, webpackModules.keys()))
const store = createStore(combineReducers(reducers), middleware );
...
...
import React from 'react'
import ReactDOMServer from 'react-dom/server
import { genStore, fsModules } from 'redux-auto';
import Main from './Main';
...
const webpackModules = fsModules("./store")
...
app.get('/', function (req, res) {
// Only load "user" in store and timeout 5 sec
genStore(webpackModules, ["user"], 5000)
.then( store => {
res.send(ReactDOMServer.renderToString(<Main store={store} />)))
}).catch( err => {
// check your init promise are completing
res.status(500).send("Problem in getting your page");
})
})
...
β‘ If you want to use Redux-auto in a React-Native project. You will just need to install the babel-plugin-redux-auto to allow to dynamic importing of your store.
npm i babel-plugin-redux-auto
Now back to the setup...
...
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { auto, reducers } from 'redux-auto';
...
// load the folder that hold you store
import nativeStore from './store/*'
...
const middleware = applyMiddleware( auto(nativeStore))
const store = createStore(combineReducers(reducers), middleware );
...
...
// import your exiting reducers
import reducers from './reducers';
// include mergeReducers
import { auto, mergeReducers } from 'redux-auto';
...
// pass into: reducers >> mergeReducers >> combineReducers
const store = createStore(combineReducers(mergeReducers(reducers)), middleware );
...
import logger from 'redux-logger';
import { auto, reducers } from 'redux-auto';
...
// pass all the middlewares in a normal arguments
const middleware = applyMiddleware( logger, auto(webpackModules, webpackModules.keys()))
const store = createStore(combineReducers(reducers), middleware );
import logger from 'redux-logger';
import { auto, reducers } from 'redux-auto';
...
// pass all the middlewares in a normal arguments
const middleware = applyMiddleware( logger, auto(nativeStore))
const store = createStore(combineReducers(reducers), middleware );
Just import "redux-auto" and the actions are automatically available by default
import actions from 'redux-auto'
...
//action[folder][file]( data )
action.apps.chageAppName({appId:123})
The action file lives within your attribute folder and becomes the exposed action. The default export should be a function that will take 1) your piece of the state 2) the payload data
Example: of an action to update the logged-in users name
// e.g. /store/user/changeUserName.js
export default function (user, payload) {
return Object.assign({}, user,{ name : payload.name } );
}
β Sometimes we want to talk to the server. This is done by action-middleware
This is done by exporting a function named "action" that returns a promise. The default function will now receive a 3rd argument "state". With the 2nd argument being the payload used to create the request
Example: saving the uses name to the server
// /store/user/changeUserName.js
export default function (user, payload, stage, data) {
switch(stage){
case 'FULFILLED':
// ...
break;
case 'REJECTED':
// ...
break;
case 'PENDING':
default :
// ...
break;
}
return user;
}
export function action (payload,user){
return fetch('/api/foo/bar/user/'+payload.userId)
}
An alternative declaration for the same as above
// /store/user/changeUserName.js
export function pending (posts, payload){
return posts
}
export function fulfilled (posts, payload, serverPosts){
return serverPosts
}
export function rejected (posts, payload, error){
return posts;
}
export function action (payload,posts){
return fetch('/api/foo/bar/user/'+payload.userId)
}
You chain actions back-to-back by setting an "chain" property on the exported function.
Attach a function as the "chain" property
Example: /store/user/getInfo
export function fulfilled (user, payload, userFromServer){
return userFromServer;
} fulfilled.chain = (user, payload, userFromServer) => actions.nav.move({page:"home"})
export function rejected (user, payload, userFromServer){
return userFromServer;
} rejected.chain = actions.user.reset
export function pending (user, payload){
return user
}
export function action (payload){
return fetch('/api/foo/bar/user/'+payload.userId)
}
If you pass your own function. Like with the 'fulfilled' example. It will be passed all the arguments, the same as the host function was.
Else you can pass thought an "redux-auto" action function. Like with the 'rejected' example. It will called without any arguments.
So calling "actions.user.getInfo({userId:1})" will automatically call actions.nav.move with the host arguments OR actions.user.reset *with out arguments.
Chained functions can call the dispatcher directly.To trigger the dispatcher from your chain you need to return an object
with a type
and payload
Example:
import { push, replace } from 'react-router-redux';
export default function highLightFirend(friendID, {id}) {
return id;
}
// This will call the 3rd party "router" reducer
highLightFirend.chain = (friendID, {id})=>{
const searchParams = new URLSearchParams(window.location.search);
if (!id) {
searchParams.delete("friend");
const url = window.location.pathname+"#"+searchParams.toString()
return replace(url) // { type: '@@router/LOCATION_CHANGE', payload: { ... } }
}else{
searchParams.set("resource", id);
const url = window.location.pathname+"#"+searchParams.toString()
return push(url) // { type: '@@router/LOCATION_CHANGE', payload: { ... } }
}
}
You can cancel an action from with-in the action .js file before it starts by not returning any value
Example:
export function action (payload,user){
if(payload.id === user.id)
return
else
return fetch('/api/foo/bar/user/'+payload.userId)
}
"index" files are need for each attribute folder you make.
This file can exposes three funtions
You can also istening for other actions from other parts of the store.
Fires on every action, to tweek the payload that will be passed to you logic functions.
// add a time stamp to the payload that will be recived by user reduced
export function before(user, action){
return Object.assign({},action.payload,{ timeStamp : new Date() })
}
This is a normal redux reducer function, being passed the previousState and the action.
export default function user(user = {name:"?"}, action) {
return user;
}
β This function will be fired on all actions, EXCEPT for actions that are handled by a specific action file in this reducer folder.
Lets understand this with an example:
Files:
store/
βββuser/
β βββ index.js
β βββ changeName.js
βββposts/
βββ index.js
βββ delete.js
code:
import actions from 'redux-auto'
...
actions.user.changeName({name:"brian"})
The default functions for store/user/changeName.js & store/post/index.js will be fired.
store/user/index.js was NOT called because there is a specific action file a to handle it for user.
Fires after every action, allowing you to change your piece of the state.
import actions from 'redux-auto'
// automatically keep a log of all actions against user
export function after(newUserValues, action, oldUserValues){
const changes = {}
if(action.type in actions.user) // log if this is a user action!
changes.log = newUserValues.concat(log,[{action.type:action.payload}])
return Object.assign({}, newUserValues, changes)
}
There are two built-in ways to detect other actions from within your index. 1)You can find if the current fire action that you have received matches a specific action and 2) You can find if their current action is part of another piece of the store.
Example: We want to have a count of how many post our user has done
import actions from 'redux-auto'
export default function user(user = {name:"?", posts:0}, action) {
// You can check on each state of an asynchronous action
if(actions.posts.save.fulfilled == action.type){
return Object.assign({},user,{posts:user.posts+1})
// And non-synchronous actions can be checked directly
} else if(actions.posts.something == action.type){
// ... do some work ...
}
return user;
}
in
keyword.Example: We wish to log all post actions
import actions from 'redux-auto'
export default function logging(log = [], action) {
// test if the action type is within the posts
if(action.type in actions.posts){
return [...log, action]
}
return log;
}
redux-auto has a built in mechanism to flag what stage an async action is in..
if the state that you returned from your reduce function is an object or array. redux-auto will transparently attach a "loading" property representing all async actions.
The "loading" flag can have 1 of 4 values
undefined
: the async action has not been fired yettrue
: the action is in progressfalse
: the action has completed successfullyerror
: an error occurred and here is the error object + a "clear" function to reset the async to undefined
Note: The async action will also have the clear function if at any time you want to reset the "loading" property.
actions.user.save()
is the async function and
actions.user.save.clear()
will clear the "loading" property.
example:
// user = { name:"tom" }
JSON.stringify(state.user) // "{ "name":"tom" }"
state.user.loading.save // = undefined
actions.user.save()
state.user.loading.save // = true
// when the request or promuse completed
state.user.loading.save // = false
// if the was a problem. it will be was to the error object
state.user.loading.save // = Error("some problem")
// + with an Error, there will also be a "clear" function to set the "loading" back to undefined
// e.g. state.user.loading.save.clear()
smart actions is an options flag that handly actions
function more intelligently.
Currently facilitates graphql and fetch responses returned by action's promises.
To enable:
import { auto } from 'redux-auto';
auto.settings({smartActions:true})
This will now parce fetch and graphQL errors into your rejected
function.
As well as parsing the json if available
If you want to use a testing frameworking. There is helper funcsion /test/fsModules
For jest example:
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { auto, reducers } from 'redux-auto';
import fsModules from 'redux-auto/test/fsModules'
import App from './Main';
import path from 'path';
import fs from 'fs';
const storePath = path.join(path.dirname(fs.realpathSync(__filename)), 'store');
const webpackModules = fsModules(storePath)
const middleware = applyMiddleware( auto(webpackModules, webpackModules.keys()))
const store = createStore(combineReducers(reducers), middleware );
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App store={store} />, div);
ReactDOM.unmountComponentAtNode(div);
});