redux-easy v3.2.0
redux-easy
This is a set of utility functions that make it easier to use Redux.
Benefits
- No string constants are needed for action types.
- A reducer function that switches on action type is not needed.
- The dispatch function is accessed through a simple import rather than
using the react-redux
connectandmapDispatchToPropsfunctions. - Actions can be dispatched by providing just a type and payload rather than an action object.
- Each action type is handled by a single reducer function that is registered by action type and is simple to write.
- Simple actions that merely set a property value in the state
(the most common kind) can be dispatched without writing
reducer functions (see
dispatchSet). - Actions that modify a property based on its current value
can be dispatched without writing reducer functions
(see
dispatchTransform). - Actions that only delete a property
can be dispatched without writing reducer functions
(see
dispatchDelete). - Actions that only add elements to the end of an array
can be dispatched without writing reducer functions
(see
dispatchPush). - Actions that only remove elements from an array
can be dispatched without writing reducer functions
(see
dispatchFilter). - Actions that only modify elements in an array
can be dispatched without writing reducer functions
(see
dispatchMap). - All objects in the Redux state are automatically frozen to prevent accidental state modification.
- Asynchronous actions are handled in a simple way without requiring middleware or thunks.
- The complexity of nested/combined reducers can be bypassed.
- Redux state is automatically saved in
sessionStorage(on every state change, but limited to once per second). - Redux state is automatically loaded from
sessionStoragewhen the browser is refreshed to avoid losing state. - Integration with redux-devtools is automatically configured.
Example app
See https://github.com/mvolkmann/redux-easy-greeting-card. For comparison, the same app using standard Redux is at https://github.com/mvolkmann/redux-greeting-card.
Setup
In the topmost source file, likely named index.js,
add the following which assumes the topmost component is App:
import React from 'react';
import {reduxSetup} from 'redux-easy';
import App from './App';
import './reducers'; // if needed; see below
const initialState = {
user: {firstName: ''}
};
// The specified component is rendered in the element with
// id "root" unless the "target" option is specified.
reduxSetup({component: <App />, initialState});If you need to implement reducers, and you probably don't,
create reducers.js containing something like the following:
import {addReducer} from 'redux-easy';
// Call addReducer once for each action type, giving it the
// function to be invoked when that action type is dispatched.
// These functions must return the new state
// and cannot modify the existing state.
addReducer('addToAge', (state, years) => {
const {user} = state;
return {
...state,
user: {
...user,
age: user.age + years
}
};
});If the application requires a large number of reducer functions, they can be implemented in multiple files, perhaps grouping related reducer functions together.
In components that need to dispatch actions, do something like the following:
import React, {Component} from 'react';
import {dispatch, watch} from 'redux-easy';
class MyComponent extends Component {
onFirstNameChange = event => {
// assumes value comes from an input
const {value} = event.target;
dispatch('setFirstName', value);
// If the setFirstName action just sets a value in the state,
// perhaps user.firstName, the following can be used instead.
// There is no need to implement simple reducer functions.
dispatchSet('user.firstName', value);
};
render() {
const {user} = this.props;
return (
<div className="my-component">
<label>First Name</label>
<input
onChange={this.onFirstNameChange}
type="text"
value={user.firstName}
/>
</div>
);
}
}
// The second argument to watch is a map of property names
// to state paths where path parts are separated by periods.
// For example, zip: 'user.address.zipCode'.
// When the value for a prop comes from a top-level state property
// with the same name, the path can be an empty string, null, or
// undefined and `watch` will use the prop name as the path.
export default watch(MyComponent, {
user: '' // path will be 'user'
});Versions
By default redux-easy saves state data in sessionStorage
so it can be retrieved if the user refreshes the browser.
During development when the shape of the initial state changes, it is
desirable to replace what is in sessionStorage with the new initial state.
One way do to this is to close the browser tab and open a new one.
If this isn't done, the application may not work properly because it
is expecting different data than what will be used from sessionStorage.
A way to force the new initial state to be used is to supply a
version string or number in the options object passed to reduxSetup.
Whenever redux-easy sees a new version,
it replaces the data in sessionStorage with the current
initialState value in the options object passed to reduxSetup.
Sensitive Data
When the state contains sensitive data
such as passwords and credit card numbers,
it is a good idea to prevent that data from being
added to the Redux store or written to sessionStorage.
One way to do this is to add replacerFn and reviverFn functions
to the options object that is passed to reduxSetup.
These functions are similar to the optional replacer and reviver parameters
used by JSON.stringify and JSON.parse.
Both are passed a state object.
If they wish to change it in any way,
including deleting, modifying, and adding properties,
they should make a copy of the state object,
modify the copy, and return it.
Consider using the lodash function deepClone to create the copy.
Path Concerns
When the layout of the state changes, it is necessary
to change state paths throughout the code.
For small apps or apps that use a small number of state paths
this is likely not a concern.
For large apps, consider creating a source file that exports
constants for the state paths (perhaps named path-constants.js)
and use those when calling every redux-easy function that requires a path.
For example,
const GAME_HIGH_SCORE = 'game.statistics.highScore';
const USER_CITY = 'user.address.city';
...
import {GAME_HIGH_SCORE, USER_CITY} from './path-constants';
dispatchSet(USER_CITY, 'St. Louis');
dispatchTransform(GAME_HIGH_SCORE, score => score + 1);With this approach, if the layout of the state changes it is only necessary to update these constants.
Form Elements Tied to State Paths
It is common to have input, select, and textarea elements
with onChange handlers that get their value from event.target.value
and dispatch an action where the value is the payload.
An alternative is to use the provided Input, Select, MultiSelect,
and TextArea components as follows:
HTML input elements can be replaced by the Input component.
For example,
<Input path="user.firstName" />The type property defaults to 'text',
but can be set to any valid value including 'checkbox'.
The value used by the input is the state value at the specified path.
When the user changes the value, this component
updates the value at that path in the state.
To perform additional processing of changes such as validation,
supply an onChange prop that refers to a function.
HTML textarea elements can be replaced by the TextArea component.
For example,
<TextArea path="feedback.comment" />HTML select elements can be replaced by
the Select and MultiSelect components.
The path value for a Select is a single value and
the path value for a MultiSelect is an array of values.
For example,
<Select path="user.favoriteColor">
<option>red</option>
<option>green</option>
<option>blue</option>
</Select>
<MultiSelect path="user.favoriteColors">
<option>red</option>
<option>green</option>
<option>blue</option>
</Select>If the option elements have a value attribute, that value
will be used instead of the text inside the option.
For a set of radio buttons, use the RadioButtons component.
For example,
<RadioButtons className="flavor" list={radioButtonList} path="favoriteFlavor" />where radioButtonList is set as follows:
const radioButtonList = [
{text: 'Chocolate', value: 'choc'},
{text: 'Strawberry', value: 'straw'},
{text: 'Vanilla', value: 'van'}
];When a radio button is clicked the state property favoriteFlavor
will be set the value of that radio button.
For a set of checkboxes, use the Checkboxes component.
For example,
<Checkboxes className="colors" list={checkboxList} />where checkboxList is set as follows:
const checkboxList = [
{text: 'Red', path: 'color.red'},
{text: 'Green', path: 'color.green'},
{text: 'Blue', path: 'color.blue'}
];When a checkbox is clicked the boolean value at the corresponding path will be toggled between false and true.
All of these components take an action prop in addition to a path prop.
Both of these props are optional.
Using the path prop causes a specified state path
to be updated with the value of the component.
Using the action prop causes an action with a given name to be dispatched.
The action is typically defined using the addReducer function.
The payload for this action is an object that has path and value properties.
Using the action prop is useful when a change must cause multiple state paths to be updated.
For example, the Checkboxes component could use this to cause
a change to one checkbox to update the state of other checkboxes.
Asynchronous Actions
If a function passed to addReducer returns a Promise
and a matching action is dispatched,
it will wait for that Promise to resolve and then
update the state to the resolved value of the Promise.
Here's an example of such a reducer function:
addReducer('myAsyncThing', (state, payload) => {
return new Promise(async (resolve, reject) => {
try {
// Data in payload would typically be used
// in the following call to an asynchronous function.
const result = await fetch('some-url');
// Build the new state using the current state
// obtained by calling getState() rather than
// the state passed to the reducer function
// because it may have changed
// since the asynchronous activity began.
const newState = {...getState(), someKey: result};
resolve(newState);
} catch (e) {
reject(e);
}
});
});Tests
In Jest tests, do something like the following:
import {reduxSetup} from 'redux-easy';
const initialState = {
user: {firstName: ''}
};
describe('MyComponent', () => {
test('handle firstName change', () => {
// Create and register a mock store which allows
// retrieving an array of the dispatched actions in a test.
setStore(configureStore([])(initialState));
const store = reduxSetup({initialState, silent: true});
const jsx = (
<Provider store={store}>
<Login />
</Provider>
);
const wrapper = mount(jsx);
const firstNameInput = wrapper.find('.first-name-input');
const firstName = 'Joe';
firstNameInput.simulate('change', {target: {value: firstName}});
const actions = store.getActions();
expect(actions.length).toBe(1);
const [action] = actions;
expect(action.type).toBe('setFirstName');
expect(action.payload).toBe(firstName);
});
});reduxSetup Options
The reduxSetup function takes the following options,
all specified as properties in the object passed to it:
- component: top component to render
- target: element where component should be rendered (defaults to element with id "root")
- initialState: required object
- sessionStorageOptOut: optional boolean (true to not save state to session storage)
- silent: optional boolean (true to silence expected error messages in tests)
Final Words
That's everything to you need to know to use redux-easy. Code simply!
If you like this, also check out https://www.npmjs.com/package/react-hash-route.
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago