Redux made easy (with a plug and play approach)

Removing the boilerplate code in setting up a store & actions

why

I created this utility to allow you to get up and running with Redux in a fraction of the time!

Plug & Play

No wiring together of actions & reduces

No hardcoding actions types

No action creator or dispatcher to worry about

Easy Async for calling APIs

Easy initialize for parts of your store

Easy install = works as the same as other redux middleware

Pure JS no external dependencies!

SuperSmall: under 3k (minify + gzip)

Have an existing project? No worries. Drop it in, to work along side the traditional redux way.

asynchronous

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.

No need for other Redux async middleware. e.g. thunk, promise-middleware, saga Easily allows you to pass a promise into redux and have it managed for you Allows you to co-locate external service calls with where they will be transformed Naming the file "init.js" will have it called once at app start. This is good for loading data from the server to warm up you client cache.

Overview

*Redux-Auto was created to work with Webpacks

Steps:

Create a folder to represent your store This is where the data, logic & flow control of the application lives. This can be named whatever, just point to it with webpacks - require.context In this folder you will create folders to represent each attribute on the store For example. the "user" folder will create an attribute of 'user'

the JS files within the folder are actions that can be fired to change the shape of user. Create an index.js file to set default values export default is a catch all reducer function (if an action cant be found)

is a catch all reducer function (if an action cant be found) export "before" & "after" as lifecycle functions Create js files with the name of the action you want it mapped to export default is the reducer function

is the reducer function export "action" function. Is an action-middleware that will allow you to create promises You can create an init.js It will be automatically run once after store created using this to initialize from an API

Example layout:

└── store/ (1) └──user/ (2) └── index.js (3) └── changeName.js (4) └── init.js (5)

setup

Example of setup file

Inside your setup file *Web-App

... 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 ); ...

Inside your setup file *React-Native

➡ 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 Add 'babel-plugin-redux-auto' to your plugins within your babel config

Now back to the setup...

... 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 ); ...

Using along side an existing Redux setup.

... // 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 ); ...

Using along side other Redux middleware. *Web-App

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 );

Using along side other Redux middleware. *React-Native

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 );

actions are available in the UI

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})

action files

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) }

chaining actions together

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.

chaining to dispatcher

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: { ... } } } }

cancel an action

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

"index" files are need for each attribute folder you make.

This file can exposes three funtions

before default after

You can also istening for other actions from other parts of the store.

before

Fires on every action, to tweek the payload that will be passed to you logic functions.

export function before ( user , action ) { return Object . assign ( { } , action . payload , { timeStamp : new Date ( ) } ) }

default

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.

after

Fires after every action, allowing you to change your piece of the state.

import actions from ' redux-auto ' export function after ( newUserValues , action , oldUserValues ) { const changes = { } if ( action . type in actions . user ) changes . log = newUserValues . concat ( log , [ { action . type : action . payload } ] ) return Object . assign ( { } , newUserValues , changes ) }

listening for other actions

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.

To find if the correct action is a specific action. Import the actions as you normally would and do a loose equality check.

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 ) { if ( actions . posts . save . fulfilled == action . type ) { return Object . assign ( { } , user , { posts : user . posts + 1 } ) } else if ( actions . posts . something == action . type ) { } return user ; }

If you wish to listen to all actions from a specific part of the store. You can use the in keyword.

Example: We wish to log all post actions

import actions from ' redux-auto ' export default function logging ( log = [ ] , action ) { if ( action . type in actions . posts ) { return [ ... log , action ] } return log ; }

handling async actions in your UI

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 yet true : the action is in progress false : the action has completed successfully error : 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

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

Testing

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); });

Resources