Redux
Redux is a global state store, used to pass along state to containers/components irrespective of hierarchical position, enabling decoupling of props between parent and children components, and stabilishing a clear architecture for maintaining the project.
npm i --save redux
Redux
https://commons.wikimedia.org/wiki/File:Ngrx-redux-pattern-diagram.png
Main Concepts
- Reducer
- Store
- Actions
- Subscription
- Where should state be stored
- React integration
Reducer
A reducer is a function
that receives two parameters, state
and action
,
and returns a new state
.
Note: the state must be immutable, so whenever updating state, always remember to copy the old state and never directly mutate it.
// Define an initial state so your dependencies
// are always initialized with some defaults
const initialState = {
counter: 0
}
// Reducer
const rootReducer = (state = initialState, action) => {
if (action.type === 'INC_COUNTER') {
return {
// Using '...state' spreads the contents of
// the current state, and merges with whatever
// other properties set afterwards
...state,
counter: state.counter + 1
};
}
if (action.type === 'ADD_COUNTER') {
return {
...state,
counter: state.counter + action.value
};
}
return state;
};
Store
The store maintains the state accross the application, and is created receiving a root reducer.
const createStore = redux.createStore;
const store = createStore(rootReducer);
Actions
Actions are objects which are dispatched, containing a type
, which is a string
(which by convention is snake case and all in caps) and is always mandatory, followed by
whichever other properties you might want to send, and these are optional.
It’s a best practice to export constants with the same name as the action type, because when you mistype an exported constant, React will warn you of the error, whereas if you use a string literal, your app will fail silently:
export const INC_COUNTER = 'INC_COUNTER';
export const ADD_COUNTER = 'ADD_COUNTER';
store.dispatch( { type: INC_COUNTER } );
store.dispatch( { type: ADD_COUNTER, value: 10 }) ;
Subscription
Subscriptions are triggered whenever state is updated, that is, whenever actions dispatched trigger updates on the reducer, every subscription will be udated with the new data.
store.subscribe(() => {
console.log('[Subscription]', store.getState());;
});
Where should state be stored
How can you decide when and where should state be placed at?
-
Presentation logic:
state for controlling which components to show, which color should the be, animation triggers, that kind of state should be handled within the component itself.
-
Persistent state:
state based upon database tables/documents, information retrieved through APIs, should be partially stored within redux, that is, you shouldn’t fetch every single row of data from your backend, but just the part that will be presented to the user by the time of request.
-
Client state:
Filtering, current authentication/authorization status, should be stored within redux.
React integration
In order to integrate Redux with React, we’ll need another package:
npm i --save react-redux
This package includes a Provider
, which is what we use to connect our Redux store
to our React application.
Example integration
Create a folder which will contain your reducers
, usually named store
, and add your
reducers
and actions
there:
Example properties.js
:
import * as actionTypes from '../actions';
const initialState = {
properties: [],
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.STORE: return store(state, action);
case actionTypes.REMOVE: return remove(state, action);
default: return state;
}
};
const store = (state, action) => {
const property = {id: action.id, value: action.value};
// Use 'concat' for returning a new array with the element added
const updatedArray = state.properties.concat(property);
return {
...state,
properties: updatedArray,
}
}
const remove = (state, action) => {
const filterRules = result => result.id !== action.id;
// Use 'filter' for returning a new array with the element removed
const updatedArray = state.properties.filter(filterRules);
return {
...state,
properties: updatedArray,
}
}
export default reducer;
Example actions.js
:
export const INCREMENT = 'INCREMENT';
export const ADD = 'ADD';
export const STORE = 'STORE';
export const DELETE = 'DELETE';
Example someProperty.js
:
import * as actionTypes from '../actions';
const initialState = {
someProperty: 0,
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.INCREMENT: return increment(state, action);
case actionTypes.ADD: return add(state, action);
default: return state;
}
};
const increment = (state, action) => {
return {
...state,
counter: state.someProperty + 1,
}
};
const add = (state, action) => {
return {
...state,
counter: state.someProperty + action.value,
}
};
export default reducer;
On your index.js
file, add the following:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
// Redux - store, combiner and provider
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import App from './App';
import somePropertyReducer from './store/reducers/someProperty';
import propertiesReducer from './store/reducers/properties';
const rootReducer = combineReducers({
someProp: somePropertyReducer,
properties: propertiesReducer,
})
const store = createStore(rootReducer);
const app = (
<Provider store={store}>
<App />
</Provider>
);
ReactDOM.render(app, document.getElementById('root'));
registerServiceWorker();
To access the store
from within your components, you must add the connect
function, which
wraps state and action dispatched and return a higher order component containing your component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actionTypes from './actions';
// ... some implementation
// State usage examples
<p>
{this.props.someProperty}
</p>
{this.props.properties.map(property => {
return (
<div key={property.id}>
<span>ID: {property.id}</span>
<p>Value: {property.value}</p>
</div>
);
})}
// Dispatch usage examples
<button onClick={this.props.onIncrementProperty}>
Increment property
</button>
<button onClick={() => this.props.onAddToProperty(25)}>
Increment property by 25
</button>
<button onClick={() => this.props.onStoreProperty(propObj)}>
Store the propertyObject
</button>
<button onClick={() => this.props.onRemoveProperty(propObj)}>
Remove the propertyObject
</button>
// this function will map the store's state
// to props you can use within you component
const mapStateToProps = state => {
return {
someProperty: state.someProp.someProperty,
properties: state.properties.properties,
};
};
// this function will map dispatch functionallities
// to prop references you can use within your component
const mapDispatchToProps = dispatch => {
return {
onIncrementProperty: () => dispatch({ type: actionTypes.INCREMENT }),
onAddToProperty: (value) => dispatch({ type: actionTypes.ADD, value: value }),
onStoreProperty: (propObj) => dispatch({ type: actionTypes.STORE, id: propObj.id, value: propObj.value }),
onRemoveProperty: (propObj) => dispatch({ type: actionTypes.REMOVE, id: propObj.id }),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(YourComponent);
Leave a Comment