react-reduxify

1.0.9 • Public • Published

react-reduxify

This library contains a single 'reduxify' function to ease using redux.

First Usage: reduxify() over connect()

Instead of using mapStateToProps, mapDispatchToProps and the cryptic and scary connect() function ...

import { connect } from 'react-redux'
import { increment, decrement } from './actions'

const Counter = (props) => {
	return (<div>
		Count = <span>{props.count}</span>
		<button onClick={() => props.increment()}>Increment</button>
		<button onClick={() => props.decrement()}>Decrement</button>
	</div>)
}

const mapStateToProps = state => ({
	count: state.count
})

const mapDispatchToProps = dispatch => ({
	increment: () => dispatch(increment()),
	decrement: () => dispatch(decrement())
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

Connect your component with redux using the simpler reduxify() function ...

import reduxify from 'react-reduxify'
...
export default reduxify(Counter, 'count', { increment, decrement })

Isn't it simpler and more readable?

API

The signature of reduxify() method is this way:

function reduxify(component, neededState, neededActions, mergeOptions, options) {
...
}
parameter type description
component React class or function component The component to be connected to redux
neededState object, Array, string List of state items to be retrieved from redux store. This could be a string containing comma separated list of state names (like "count,started"), an array of strings (like ['count', 'started']) or an object whose properties are state names (like { count: 0, started: true }).
neededActions function or object An object containing actions or a function that returns such object (similar to second parameter of connect(), i.e. mapDispatchToProps
mergeOptions function Optional. Similar to mergeOptions in connect()
options object Optional. Similar to options in connect()

neededStates parameter

As it is said , the argument specified for this parameter could be either of the followings:

  • string
  • array of string or object
  • object

string

We can specify a comma separated list of state items as string. Each item in the string could have the following format:

`{name?}:{key}={default_value?}`

name and default value are optional. If name is not specified, speciifed state item will be propagated into the props corresponding the way it is specified (this is explained in 'state name chain' section).

name

We can specify a name for the extracted state item to have in the props of our React component. For example reduxify(Counter, 'mycount:count') means to set a property named mycount in the props. This is similar to the following code:

const mapStateToProps = state => ({ mycount: state.count });
export default connect(mapStateToProps)(Counter);

default value

We can specify a default value for the prop item if the state item was not found or it was undefined, after an equal sign.

export default reduxify(Counter, 'mycount:count=0'); // if state.count was undefined,
                                                     // the default value of "0" would be
                                                     // used for props.mycount

state name chain

We can use dot character for the name of state items we want to specify in neededStates, like "parent.child.grandChild". reduxify() extracts the requested state item from redux store and sets a correct object in the props, even if the store does not contain the requested chain of state items.

reduxify(Counter, 'counters.socials.like_count');

After the above code, the props in Counter would be:

// props
{
  counters: {
    socials: {
       like_count: ... // value of state.counters.socials.like_count
    }
  }
}

Using an exclamation mark (!) we can negate this propagation behavior. In this case, the last property name will be assumed as the prop name for the state item.

Example:

export default reduxify(Counter, '!counters.socials.likes');

Here, instead of propagating requested state (state.counters.socials.likes) onto the props, a property named likes will be set for props with the value of state.counters.socials.likes.

Specifying list of state items

Here is an example with a list of state items:

export default reduxify(Counter, 'likes:counters.like_count=0,hits:counters.hit_count=0');

Array of string or object

We can specify an array of string or object for neededStates.

export default reduxify(Counter, ['likes:counter.like_count', 'hits:counters.hit_count']);

When using objects, the objects should follow this format:

{
   name: '...',       // string: prop name (optional).
                      // state will be used by default
                      // if name is not specified
   state: '...'       // string: state item in redux store
   default: #         // any: default value for prop item
                      // if store does not contain speicified
                      // state item or if it is undefined
}

Example:

export default reduxify(Counter, [
     { name: 'likes', state: 'counter.like_count', default: 0 },
     { name: 'hits', state: 'counters.hit_count', default: 1 }
]);

The benefit of using objects is to specify a default value of any type, while when using strings like 'likes:counters.hit_count=1' , the default value is always string (here, "1").

Object

Finally we can specify an object for neededStates whose property names are either our intended prop name and their values are either string or object with the format mentioned in previous sections. If values specified for neededStates property names are neither string nor object, they will be assumed as default value for the state item.

Example 1: string values

export default reduxify(Counter, {
     likes: 'counters.like_count',
     hits: 'counters.hit_count'}
});

Example 2: default values

export default reduxify(Counter, {
     likes: 0,	// state item = state.likes
     hits: 1    // state item = state.hits
});

This is -almost- similar to:

const mapStateToProps = state => ({ likes: state.likes, hits: state.hits })
export default connect(mapStateToProps)(Counter);

Example 3: object values (names in props would be property names by default)

export default reduxify(Counter, {
     likes: { state: 'counters.like_count', default: 0 },
     hits: { state: 'counters.hit_count', default: 1 }
});

A mixed of various values for property names could also be used.

dispatch

By default, reduxify() puts dispatch in the props as well. So, there's no need to specify neededActions. We can use dispatch directly without the need to set a property on the props and use those properties.

import reduxify from 'react-reduxify'
import { increment, decrement } from './actions'

const Counter = (props) => {
	return (<div>
		Count = <span>{props.count}</span>
		<button onClick={() => props.dispatch(increment())}>Increment</button>
		<button onClick={() => props.dispatch(decrement())}>Decrement</button>
	</div>)
}

export default reduxify(Counter, 'count')

This behavior is similar to connect(), but the difference is connect refrains from setting dispatch for the props if you specify mapDispatchToProps for it. Reduxify always sets dispatch for the props even if you specify neededActions (unless you specify a { dispatch: false } item in the neededActions and explicitly say that you don't want to have dispatch in the props).

Second Usage: reduxify() over manually defining actions and reducers

Instead of writing these boilerplate codes ...

// ------------ ./reducers/counter.js
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// actions
const increment = (x) => ({ type: INCREMENT })
const decrement = (x) => ({ type: DECREMENT })

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

export {
	INCREMENT,
	DECREMENT,
	increment,
	decrement,
	counterReducer
}

Simplify it by using reduxify() this way ...

import reduxify from 'react-reduxify'

const counterActionReducer = reduxify({
	state: { count: 0 },
	action: [
		increment: (s, a) => ({ ...s, count: s.count + 1 }),
		decrement: (s, a) => ({ ...s, count: s.count - 1 })
	]
});

export default counterActionReducer;

That's all!

The result of reduxify() in this usage is an object with the following format.

     {
          actions: [],        // array of actions (each action is a function),
          initialState: {},   // an object that holds the initial state for the reducer
          reducer: function   // the final reducer that is created
     }

There is no need to know about action type values. The generated 'reducer' internally uses custom action types.

Here is an example on how to use the reduxify result in this second usage.

// -------------- index.js ----------------
import counterActionReducer from './reducers/counter.js';
import { createStore } from 'redeux'

const store = createStore(counterActionReducer.reducer)

// -------------- ./Components/Counter.js ----------------
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import counterActionReducer from '../reducers/counter.js';
import reduxify from 'react-reduxify'

const Counter = () => {
     const count = useSelector(state => state.count);
     const dispatch = useDispatch();
     const { increment, decrement } = counterActionReducer.actions;

	return (<div>
		Count = <span>{count}</span>
		<button onClick={() => dispatch(increment())}>Increment</button>
		<button onClick={() => dispatch(decrement())}>Decrement</button>
	</div>)
}

export default Counter;

Action/Reducer definition

The array we pass to reduxify includes a list of action/reducer definition. Each item could be an object or an array.

When using an object, the object should follow the following format:

     {
          type: '',                // optional: string: type of action.
          state: ...,              // required: string or object: state item
          action: ...,             // optional: string | array | object: explicit action. name (default is 'set' + stateName in pascal-case).
          reducer: function        // optional: function : custom reducer.
     }

Examples:

{ state: 'count' }		// default state value = undefined, action = setCount
{
	state: { count: 0 }	// default state value = 0
}						// action = setCount
{ state: 'count', action: 'setClickCount' }		// default state value = undefined, action = setClickCount
{
	state: { count: 0 },
	action: 'setClickCount'	// action = setClickCount
}
{
	state: { count: 0 },
	action: x => ({ count: x + 2})	// custom action
}
{
	state: { count: 0 },
	action: x => ({ count: x + 2})	// custom action
	reducer: (state, action) => ({ ...state, count: action.count})	// custom reducer
}
{
	state: { count: 0 },
	action: {
		incClickCount: (state, action) => {
			// custom reducer
			return {...state, count: state.count + 1 }
		},
		decClickCount: (state, action) => {
			// custom reducer
			return {...state, count: state.count - 1 }
		}
	}
}

If we have a single definition, we can pass it right to reduxify() (there's no need to wrap it inside an array).

reduxify({ state: 'count' })

Or with default value:

reduxify({ state: { count: 0 } })

simpler default value:

reduxify({ count: 0 })

We can even pass a string. It is assumed as the name of the state item. action and reducer will be created based on default definition.

reduxify('count')

It is also possible to specify each definition as an array:

reduxify([
	['count'],						// state
	['SET_COUNT', 'count'],			// type, state
	['SET_COUNT', { count: 10 }],	// type, state with initial value
	['count', 'setClickCount' ],	// state, action name
	['count', ['inc', 'dec']],	// state, multiple action name
	['count', { 'setClickCount': (s, a) => ({ ...s, count: a.count }) }],	// state, action name, reducer
	['count', 'setClickCount', (s, a) => ({ ...s, count: a.count }) ], // state, action name, reducer
	['count', x => ({ count: x + 2}), , (s, a) => ({ ...s, count: a.count }) ],	// state, action, reducer
	['SET_COUNT', 'count', x => ({ count: x + 2}), , (s, a) => ({ ...s, count: a.count }) ]	// type, state, action, reducer
])

Various examples

reduxify([
	{
		state: { 'count': 0 },
		action: {
			'increment': (state, action) => ({ ...state, 'count': action.count + 1 }),
			'decrement': (state, action) => ({ ...state, 'count': action.count - 1 }) 
		}
	},
	[
		{ 'count': 0 },
		{
			'increment': (state, action) => ({ ...state, 'count': action.count + 1 }),
			'decrement': (state, action) => ({ ...state, 'count': action.count - 1 }) 
		}
	],
	[
		{ 'count': 0 },
		[
			{ 'increment': (state, action) => ({ ...state, count: action.count }) },
			{ 'decrement': (state, action) => ({ ...state, count: action.count }) }
		]
	],
	[
		'count',
		{
			'increment': (state, action) => ({ ...state, 'count': (action.count || 0) + 1 }),
			'decrement': (state, action) => ({ ...state, 'count': (action.count || 0) - 1 }) 
		}
	],
	[
		{ 'count': 0 },
		[ 'increment', 'decrement' ]
	],
	{
		state: { 'count': 0 },
		action: {
			'increment': { action: x => ( 'count': x + 1 ), reducer: (state, action) => ({ ...state, count: action.count }) },
			'decrement': { action: x => ( 'count': x - 1 ), reducer: (state, action) => ({ ...state, count: action.count }) }
		}
	},
	{
		state: 'count'
	},
	{
		state: 'count'
		action: x => ({ type: 'SET_COUNT', count: x}),
	},
	{
		state: 'count',
		action: x => ({ count: x}),
		reducer: (s, a) => ({ ...s, count: a.count })
	},
	{
		type: 'SET_COUNT',
		state: 'count',
	},
	{
		type: 'SET_COUNT',
		state: 'count',
		action: x => ({ type: 'SET_COUNT', count: x}),
	},
	{
		type: 'SET_COUNT',
		state: 'count',
		action: x => ({ type: 'SET_COUNT', count: x}),
		reducer: (s, a) => ({ ...s, count: a.count })
	},
	{
		state: { 'count': 0 },
		action: [
			{ 'increment': { x => ( 'count': x + 1 ): (state, action) => ({ ...state, count: action.count }) } },
			{ 'decrement': { x => ( 'count': x - 1 ): (state, action) => ({ ...state, count: action.count }) } }
		]
	},
	[
		{ 'count': 0 },
		{
			'increment': { action: x => ( 'count': x + 1 ), reducer: (state, action) => ({ ...state, count: action.count }) },
			'decrement': { action: x => ( 'count': x - 1 ), reducer: (state, action) => ({ ...state, count: action.count }) }
		}
	],
	[
		{ 'count': 0 },
		[
			{ 'increment': { action: x => ( 'count': x + 1 ), reducer: (state, action) => ({ ...state, count: action.count }) } },
			{ 'decrement': { action: x => ( 'count': x - 1 ), reducer: (state, action) => ({ ...state, count: action.count }) } }
		]
	],
	[
		{ 'count': 0 },
		[
			{ 'increment': { x => ( 'count': x + 1 ): (state, action) => ({ ...state, count: action.count }) } },
			{ 'decrement': { x => ( 'count': x - 1 ): (state, action) => ({ ...state, count: action.count }) } }
		]
	],
	{
		state: { layout: { type: 'boxed' } },
		action: { 'setTemplateLayout': x => ({ layout: { type: x }}) },
		reducer: (state, action) => ({ ...state, layout: { type: action.layout.type } })
	},
	[
		{ 'logAdded': [] },
		null,
		(state, action) => {
			const result = { ...state }

			result.logs = [...state.logs]
			result.logs.push(action.log)

			return log
		}
	],
	[
		{ 'session_ended': true },
		'setSessionEnd',
		(state, action) => ({ ...state, session_ended: action.session_ended })
	],
	[
		'session_ended',
		'setSessionEnd',
		(state, action) => ({ ...state, session_ended: action.session_ended })
	],
	[
		{ 'session_ended': true },
		'setSessionEnd'
	],
	[
		'session_ended', 'setSessionEnd'
	],
	[	'session_ended'	],
	'session_ended',
	{
		'SET_LOGGED_IN': 'loggedIn'
	},
	[
		'SET_LOGGED_IN',
		'loggedIn',
	],
	[
		'SET_LOGGED_IN',
		'loggedIn',
		(s, a) => ({ ...s, a.loggedIn})
	],
	[
		'SET_LOGGED_IN',
		{ 'loggedIn': false },
	],
	[
		'SET_LOGGED_IN',
		{ 'loggedIn': false },
		(s, a) => ({ ...s, a.loggedIn})
	]
]);

Readme

Keywords

Package Sidebar

Install

npm i react-reduxify

Weekly Downloads

10

Version

1.0.9

License

MIT

Unpacked Size

102 kB

Total Files

8

Last publish

Collaborators

  • mansoor-omrani