Redux with React JS

Understanding how redux works has been difficult for a beginner like me in general. so I’ve prepared a simple example to build on my other post that uses i18n package to internationalize your SPA using React JS.

This example maintains the state of my previously selected language in Redux store. You can find the completed code here in my Github profile.

Redux is quite an excellent State Managment Framework. I’m using the official react-redux package that takes care of a lot of stuff like one component that wraps your entire app also called higher-order component. This component will subscribe to your store. The rest of your components will be children to this wrapper component and will only get the parts of the state that they need.

You will then have at least one main component that listens to state changes passed down by the Provider from the store. You can specify which parts of the state it should listen to, and those pieces of the state will be passed down as props to that component (and then of course, it can pass those down to its own children). You can specify that by using the connect() function on your Parent level component and using the mapStateToPropsfunction as a first parameter.

The entire state of the Application is represented by a State tree which is read-only. And every time you need to modify the state, you need to dispatch an Action.

Action object structure should always have a type property. Any other custom property can be added.
Type property e.g. “INCREMENT”, “DECREMENT”

Pure functions are predictable. They always give the same output for the same set of arguments.
Impure functions have side-effects. They may make changes to the data with database or network calls.
Redux takes the previous state, dispatches an action and returns the next state as a new object. This function that describes the state mutation is called the reducer.
Reducers expect 2 arguments: state and action.

e.g.
const counter = (state=0, action) => {
	switch(action.type) {
		case 'INCREMENT':
			return state + 1;
		case 'DECREMENT':
			return state - 1;
		default:
			return state;
	}
}

The store binds together the 3 principles of Redux:

  • It holds the applications’s current state object.
  • It lets you dispatch actions.
  • When you create it, you need to specify the reducer that tells you how state is updated with actions.

Below is the code for the store initialization:

import { createStore, applyMiddleware } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
import { composeWithDevTools } from 'redux-devtools-extension';
import Immutable from 'immutable';

const persistConfig = {
  key: 'root',
  storage : storage,
}

const persistedReducer = persistReducer(persistConfig, rootReducer);

const initialState = {};

const middleware = [thunk];

const composeEnhancers = composeWithDevTools({
  serialize: {
    immutable: Immutable
  }
});

export const store = createStore(persistedReducer, initialState, composeEnhancers(applyMiddleware(...middleware)));
export const persistor = persistStore(store)

Below is the code for App Component wrapped under the Provider component:

import React, { Component, Suspense } from "react";
import { Route, Switch, BrowserRouter as Router } from "react-router-dom";
import { Provider } from 'react-redux';
import { store, persistor } from '../store';
import { PersistGate } from 'redux-persist/integration/react'
import HomePage from "./home/HomePage";
import AboutPage from "./about/AboutPage";
import Header from "./common/Header";
import PageNotFound from "./PageNotFound";
import CoursesPage from "./courses/CoursesPage";
import '../config/i18n'

export class App extends Component {
  
  render() {
    let data = localStorage.getItem('persist:root') && JSON.parse(JSON.parse(localStorage.getItem('persist:root')).langOptionsReducer).option.value;
    console.log(data)
    console.log(this.props)
    this.props.props.changeLanguage(data)

    return (
      < Provider
        store={store} >
        {/* https://github.com/rt2zz/redux-persist/blob/master/docs/PersistGate.md */}
        < PersistGate
          loading={<div>loading app...</div>}
          // onBeforeLift={onBeforeLift}
          persistor={persistor} >
      <div className="container-fluid">
        <Header />
        <Switch>
          <Route exact path="/" component={HomePage} />
          <Route path="/courses" component={CoursesPage} />
          <Route path="/about" component={AboutPage} />
          <Route component={PageNotFound} />
        </Switch>
      </div>
      </PersistGate >
      </Provider >
    );
  }
}

export default App;

Below is the code for the action method, changeLang that is dispatched when a language option is selected:

export const changeLang = (option)=> (dispatch) => {
    dispatch({type : 'langOptions',
    payload : option})
}

This is the code for the langOptions reducer that is fired by the action method:

const initialState={option:{}}

export default function(state=initialState, action){
    switch (action.type) {
        case 'langOptions':
            return {
                ...state,
                option:action.payload
            }
        default:
            return state
    }
}

All the reducers in your application need to be combined into a single reducer as in the index.js file under the reducers folder:

import { combineReducers } from 'redux';
import langOptionsReducer from './langOptions'

export default combineReducers({
    langOptionsReducer
})

React Redux provides a connect function for you to read values from the Redux store (and re-read the values when the store updates). The connect function takes two arguments, both optional: mapStateToProps : called every time the store state changes. It receives the entire store state, and should return an object of data this component needs. 2nd parameter is mapDispatchToProps which can either be a function or a full object of action creators.

Code for the HomePage with connect function using mapStateToProps argument as below:

import React, { Component } from "react";
import { Link } from "react-router-dom";
//import "../../node_modules/bootstrap/dist/css/bootstrap.css";
import Select from "react-select";
import { options } from "../../config/options";
import "../../config/i18n";
import { withTranslation } from "react-i18next";
import { connect } from "react-redux";
import { changeLang } from "../../actions/langOptions";

export class HomePage extends Component {
  constructor(props) {
    super(props);
    console.log("inside constructor");
    this.state = {
      lang: options[0]
    };
  }

  changeLang = lang => {
    const { i18n } = this.props;
    const { value } = lang;
    this.props.changeLang(lang);
    this.setState({ lang });
    i18n.changeLanguage(value);
  };

  componentWillReceiveProps(props) {
    console.log(props);
  }

  componentDidMount() {
    this.setState({
      lang: this.props.option.value ? this.props.option : options[0]
    });
    // this.props.i18n.changeLanguage(options[2].value);
  }

  render() {
    const { lang } = this.state;
    return (
      <React.Fragment>
        <div className="jumbotron">
          <h1>Courses Administration</h1>
          <Link to="about" className="btn btn-primary btn-lg">
            Learn more
          </Link>
        </div>
        <br />
        <div style={{ width: "200px" }}>
          <Select
            defaultValue={options[0]}
            options={options}
            value={lang}
            onChange={this.changeLang}
            className="App-Select"
          />
        </div>
      </React.Fragment>
    );
  }
}

const mapStateToProps = state => {
  return {
    option: state.langOptionsReducer.option
  };
};

export default connect(
  mapStateToProps,
  { changeLang }
)(withTranslation("translations")(HomePage));
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.