Redux

5 minute read

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

Redux Pattern Diagram 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?

  1. 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.

  2. 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.

  3. 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:

Reducer folder structure example

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

Updated:

Leave a Comment