DEV Community

jsmegatools
jsmegatools

Posted on • Originally published at jsmegatools.com on

Adding Redux (with ducks), Material UI loader to a React app

This post is the Lesson 3 of React online course from JS Mega Tools. You can get the code for the previous lesson at the following address: https://github.com/jsmegatools/React-online-course Once you’ve cloned the repository you can go into Lesson-2 folder and edit files the way its done in this tutorial.

In this lesson we are going to add redux to our application and set up material-ui loader.

First let’s install necessary for redux modules. Run the following code in the root folder of our application:

npm install redux react-redux --save

The first module is the official redux module, the second one is for using react with redux.

The reason we run this command in the root folder and not in the react-ui folder, where the front end react code is located, is because it enables us to use redux with server rendering

Redux has 3 important concepts: store, actions and reducers.

The store is where an application’s state is stored. An application’s state is a single object. An application’s state is like a snapshot of the applications at a moment in time. Ideally you would not use React component state with redux, redux state would be a single source of truth for the entire application. This helps keep control of data flow in an application and avoid spaghetti code which leads to various bugs. But there are use cases where you may wanna use react store instead of/along with redux.

Actions in Redux are plain objects that represent an action that different parts of an application want to perform to modify state. They send various kinds data to a store and have a type. This sending of data to a store is called dispatch, that is you dispatch actions. The way you do this is you call a method of a store called dispatch. The only way to apply changes to the state must be actions and not direct modification.

Finally reducers are pure functions (that is, given the same arguments, they return the same result) that update a store with data sent in an action. Inside reducers if there are modifications to the state brought by actions, an old state is replaced with a new state with modifications applied to a new state.

We are going to create a redux store in a file named configureStore.js, which we are going to create in the root directory. Here are contents of configureStore.js:

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import rootReducer from './reducers';

export default function configureStore() {
  return createStore(
    rootReducer,
    applyMiddleware(
      thunkMiddleware
    )
  )
}

We are exporting configureStore from the module, which configures and returns a store. The main work is done by createStore function, which creates the store. There is also applyMiddleware function which adds middleware to the store. We already talked about Express middleware in previous lessons, redux middleware is a similar concept. Redux middleware has an access to a store, a dispatched action, and can dispatch actions itself.

We are using a thunkMiddleware from redux-thunk that enables dispatch to accept a function as an argument, while without thunkMiddleware dispatch accepts only objects. This allows us to have asynchronous actions, which enable putting http requests into actions, so all our component has to do is dispatch actions, without knowing various asynchronous APIs like fetch.

To add redux-thunk to our project, run the following command at the root folder of our application:

npm install redux-thunk --save

We run this in the root folder of the application to use it for server rendering (like redux and react-redux modules).

We also pass rootReducer function, which we are going to talk about in a moment.

Once we have created a configureStore module, we are ready to add the store to our application. We are going to add the store to our application with the help of Provider component from react-redux module (official redux bindings for react).

Replace contents of react-ui/index.js file with the following:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import configureStore from './configureStore';
import registerServiceWorker from './registerServiceWorker';

const rootElement = <Provider store={configureStore()}>
  <App />
</Provider>;

ReactDOM.render(rootElement, document.getElementById('root'));
registerServiceWorker();

We import Provider component to the module at the top. We also import configureStore. Then we create a root Provider element with it, we pass the store created by configureStore call as a store prop to Provider element. Then we wrap App element, created using App component in Provider element. Provider is now at the top of the element hierarchy of the app. The store is now available to every component instance in the component hierarchy of our app. You don’t have to pass it from a parent to a child via props.

Setting up reducers, actions, action types.

Earlier when using createStore, we passed rootReducer function to it. Redux allows us to separate responsibility of reducers so that they are only responsible for a particular slice of a state. For example in our application we have a main area and an admin area, it is quite logical to use different branches of state for those parts.

Reducers responsible for a part of a state can further distribute responsibility on that part of a state to other reducers. This distribution happens with a help of combineReducers function, which returns a reducer that gives responsibility for various parts of a part of a state that this reducer is responsible for to reducers passed to combineReducers function. There are a lot of reducers in the previous sentence:). Here is how our rootReducer is going to be created.

  1. Create a redux folder in react-ui/src directory
  2. In that directory create index.js file with the following contents:
import { combineReducers } from 'redux'
import mainArea from './modules/mainArea'
import adminArea from './modules/adminArea'

export default combineReducers({
  mainArea,
  adminArea
});

We import combineReducers from redux module. We import reducers mainArea and adminArea reducers from modules directory (more on that later). Then we use combineReducers to create the root reducer which delegates responsibility on mainArea property of state to mainArea reducer and adminArea property of state to adminArea reducer. This root reducer is then passed to createStore as we saw earlier. mainArea or adminArea reducers can either be a result of a similar combineReducers call, or be defined as a function by a developer. If they are a result of combineReducers call, then they distribute the responsibility on the part of state they are responsible for (for example mainArea) to other reducers.

We are going to set up our application structure with ducks. What in the world is that? Here is a story. When redux came out, everybody was following an application structure used in redux official tutorial. Which put folders like components, containers, reducers, actions, constants in a root folder of an application. This approach does not scale, as you end up with many files inside each directory as you add more features to your application.

Then there came out another approach to structuring a react and redux application, by grouping components, containers, reducers, actions, constants by a feature that they represent and putting them into a folder with a name of that feature. That approach had a better scaling, but there were no separation between React and Redux. It would require you to do a lot of moving and editing once you decided to switch your state management solution to some other library.

Finally a solution came from https://github.com/erikras/ducks-modular-redux which encourages separation of a React part from a Redux part, and grouping React code by feature in folders and Redux code by feature in modules inside files.

For now we are going to have mainArea and adminArea modules. We are going to put these modules in a folder named modules. The default exports from those modules are reducers (thats why we pass imports from those modules to combine reducers function), but those modules also contain actions and action types.

Let’s create a modules folder in react-ui/src/redux and in modules folder let’s create mainArea.js file with the following contents:

import fetch from 'cross-fetch';

const GET_LOCATIONS = 'rta/mainArea/GET_LOCATIONS';
const GET_LOCATIONS_SUCCESS = 'rta/mainArea/GET_LOCATIONS_SUCCESS';
const GET_LOCATIONS_FAILURE = 'rta/mainArea/GET_LOCATIONS_FAILURE';

export const requestLocations = () => ({ type: GET_LOCATIONS });
export const receiveLocations = locations => ({ type: GET_LOCATIONS_SUCCESS, locations });
export const receiveLocationsFail = error => ({ type: GET_LOCATIONS_FAILURE, error });

export const fetchLocations = () => (dispatch) => {
  dispatch(requestLocations());
  return fetch('/api/locations').then(
    res => res.json(),
    err => dispatch(receiveLocationsFail(error))
  )
    .then(locations => dispatch(receiveLocations(locations)))
};

const initialState = {
  locations: [],
  isFetching: false,
  error: false
};

export default (state = initialState, action) => {
  switch(action.type) {
    case GET_LOCATIONS:
      return {
        ...state,
        isFetching: true
      };
    case GET_LOCATIONS_SUCCESS:
      return {
        ...state,
        locations: action.locations,
        isFetching: false
      };
    case GET_LOCATIONS_FAILURE:
      return {
        ...state,
        error: action.error,
        isFetching: false
      };
    default:
      return state;
  }
};

First we import fetch from cross-fetch(a library, that implements fetch API, which allows to make asynchronous http requests). After that we have 3 action type definitions. It is a good practice to define action types as constants, because as your app scales its easier to add modifications to a definition rather than replace every action type in a module.

Actions types are of a form ‘npm-module-or-app/reducer/ACTION_TYPE’. rta stands for react travel accommodations. mainArea is the name of the reducer, though we have it as an anonymous function, when we import it in another file we call it mainArea, finally there is an action type. GET_LOCATIONS corresponds to a server request for accommodations locations, GET_LOCATIONS_SUCCESS corresponds to a successful http request, GET_LOCATIONS_FAILURE corresponds to a failed http request.

Next we have action creators functions, they create actions. They are quite common in redux and often also referred to as actions. The purpose of action creators is portability and easiness of testing. The third action creator returns a function rather than an object and that is made possible by thunk middleware we talked about earlier. When fetchLocation action creator is called, GET_LOCATIONS action is dispatched from within it through requestLocations and upon successful request completion GET_LOCATIONS_SUCCESS action is dispatched through receiveLocations creator (that action has locations as a payload).

In the previous lesson we had a fetch call inside componentDidMount of the MainArea component, now that call is moved to fetchLocations action and is handled by redux.

Next we have an initial state for the mainArea part of the app state. Initial state is required for a reducer initialization, as reducers are passed undefined as a first argument when they are called for the first time by redux. Initial state is also a good way to get a visual representation of the state for a particular reducer.

The default export of the module is a reducer. It takes an existing state and an action and returns a new state based on that action, or a default state if there is no matching case in the switch statement.

If an action is of a type GET_LOCATIONS, we copy previous state properties to a new state, with ES6 object spread operator. Then we set isFetching property to true, which allows us to show a loader. With GET_LOCATIONS_SUCCESS we do the same, but we are setting locations property of the state to the value we received in an action, and setting isFetching property to false to hide the loader. With GET_LOCATIONS_ERROR we copy the previous state, set isFetching to false and set an error to an error that happened during the request. And finally if no type matches an action’s type we return the state that was passed to reducer as an argument (this can happen for example when an action that reached this reducer was meant for another reducer).

We are not working on admin area right now, thus you can put just a placeholder reducer into react-ui/src/reducers/modules/adminArea.js for now:

export default (state = {}, action) => {
  return state;
};

Now that we use ducks let’s create the react project structure that we want. Right now we have our components in components folder in react-ui/src. Let’s create features directory and add MainArea and Admin folders to it. Then we should move MainArea.js from components/MainArea to features/MainArea and AdminArea.js from comopents/AdminArea to features/AdminArea. We can delete components folder after that.

When you use redux it beneficial to think of your components as presentational components and container components. Presentational components handle the ui and container components pass data between a store and presentational components. Lets create container components for Main area and Admin area. We are going to put container components to their respective feature folders: features/MainArea and features/AdminArea.

Here is the content of features/MainArea/MainAreaContainer.js:

import { connect } from 'react-redux';
import MainArea from './MainArea';
import * as actions from '../../redux/modules/mainArea';

const mapStateToProps = ({ mainArea }) => ({
  locations: mainArea.locations,
  isFetching: mainArea.isFetching,
  error: mainArea.error
});

export default connect(mapStateToProps, actions)(MainArea);

We import connect function from react-redux, which connects redux store to MainArea component. Then we import MainArea component and we import actions as an object from mainArea redux module. mapStateToProps receives the whole state as an argument and creates an object to merge into presentational component’s props. You can choose names of properties the object, select whatever values from the state you want and assign those values to properties. The properties will be the names of props and values will be values of props of a component.

Here we use object destructuring of the function parameter to extract mainArea property of the state and return an object with the locations, isFetching and error properties to merge into MainArea props.Then we call connect with mapStateToProps.

connect function has a second parameter which is called mapDispatchToProps, which, if it is a function, also returns an object to merge into a component props, but it has dispatch as an argument. The function can use dispatch the following way:

const mapDispatchToProps = dispatch => {
  return {
    prop: data => {
      dispatch(someAction(data));
    }
    
  };
}

Your component can then call props as functions and those functions will call dispatch.

If you pass an object as mapDispatchToProps (as we are doing by passing actions, which have imported from mainArea module), the object merged into a component’s props will be an object with the same property names and values wrapped into dispatch for you.

For features/AdminArea/AdminAreaContainer.js you can use placeholder code for now:

import { connect } from 'react-redux';
import AdminArea from './AdminArea';

const mapStateToProps = state => ({});

export default connect(mapStateToProps)(AdminArea);

Now that we’ve created MainAreaContainer, it is time for MainArea component to make use of redux. Change react-ui/src/features/MainArea/MainArea.js to the following:

import React, { Component } from 'react';
import RefreshIndicator from 'material-ui/RefreshIndicator';

class MainArea extends Component {
  componentDidMount() {
    this.props.fetchLocations();
  }

  render() {
    const content = this.props.isFetching ? <RefreshIndicator
      size={50}
      top={0}
      left={0}
      loadingColor="#FF9800"
      status="loading"
      style={{
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%,-50%)'
      }}
    /> :
      this.props.locations.map(location =>
        <li key={location.id}>
          <img src={location.image} alt={location.name} />
          {location.name}
        </li>)

    return (
      <div className="home-page-container">
        {content}
      </div>
    );
  }
}

export default MainArea;

We got rid of the constructor for now. Now we don’t use fetch in this component, we rather call this.props.fetchLocations. In render we check for isFetching value from the app state, and if it is true we show a RefreshIndicatior loader from material-ui (We are going to setup material-ui after in a minute), otherwise we render a list of the locations, store the result of the ternary operator in content constant, which we then put in JSX.

Now our MainArea component uses Redux. Let’s install and setup material-ui for the loader to work.

Run the following in the root directory of the main project (not in react-ui directory):

npm install material-ui --save.

Add the following import to react-ui/index.js:

import MuiThemeProvider from ‘material-ui/styles/MuiThemeProvider’;

Then in index.js replace the expression involving const root with the following:

const root = <Provider store={configureStore()}>
  <MuiThemeProvider>
    <App />
  </MuiThemeProvider>
</Provider>;

Now material-ui is available in our application and the loader will work.

That is it for the lesson 3. We’ve learned how to set up and use Redux in your React project and how to create a material-ui loader. The complete source code for this lesson can be found at the following address.

https://github.com/jsmegatools/React-online-course

Latest comments (0)