React and Redux

So, you've started a new project and built some React components. You don't have to get very far before needing to solve the problem of how to manage your frontend data. This isn't a particularly exciting problem to solve, but it's a necessity if you want to build a successful web app that is performant and has room to scale and grow.

React boasts excellent performance due to its hierarchical approach to data storage and rendering web elements. Unfortunately, this very benefit makes data management complicated and can quickly lead to code bloat. This is where Redux can help. Redux manages the data between components by existing separately from the React hierarchy.

Redux's architecture is built around unidirectional data flow, which pairs nicely with React's rendering pattern. Since the data flow is one direction, we don't have to worry about side effects and can trust that the component will render or re-render in a predictable, React way.

Most of the community agrees that Redux does an effective job of solving React's data management issues, but there are differing opinions on when you should implement it. One camp believes you shouldn't install Redux until you find yourself with a real data management problem. The second camp argues that, because you will likely need a state management tool at some point in the development of your app, you should use Redux from the beginning. Neither camp is necessarily right or wrong, but I definitely fall into the second, and here's the short answer why: It's is easier to build good patterns at the beginning of a project than it is to change your data management paradigm—and, by extension, your development patterns—after the app has grown. It is not always easy to see your project getting too complicated until it's too late. Nonetheless, no matter which camp you fall into, you will be able to use some of the patterns below, so let's jump in!

Below, I've built out a simple contact manager in React. You'll notice that I've stripped out some of the content of the functions, but don't worry: You can check out the code and see all the details at the end. For now, let's focus on structure.

// index.js

import React from 'react';
import ReactDOM from 'react-dom'; 
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
// App.js

import React, { Component } from 'react';
import { Contact } from './Contact';
 
class App extends Component {
 
  constructor(props) {
    super(props);

    this.state = {
      selectedIndex: 0,
      contactList: [ /* ... */ ]
    };
  }
 
  _onContactSelected = (contactId) => {
    // sets selectedIndex and contactId onto the state for quick access
    // ...
  };
 
  _onContactUpdated = (updatedContact) => {
    // updates the contact
    // ...
  };

  render() {
    const { contactList, selectedContactId, selectedIndex } = this.state;
    return (
      <div className="App">
        <header className="app-header">
          <img src={logo} className="app-logo" alt="logo" />
          <h1 className="app-title">Contact List</h1>
        </header>
        <Contacts
          contactList={contactList}
          selectedContactId={selectedContactId}
          selectedContact={this.state.contactList[selectedIndex]}
          onUpdate={this._onContactUpdated}
          onContactSelected={this._onContactSelected}
        />
      </div>
    );
  }
}

The Contacts component will display a list of contacts that the user can view and update if necessary. If this is the only functionality we plan to build, then our app definitely doesn't need Redux. But let's say we know that we're going to add a calendar feature, contact sharing, authentication and, if all goes well, integration with other messaging clients like Skype and Facebook Messenger. With features like these on the roadmap, we'll have lots of new functionality to build, and several of our new pages will need to have access to the same core data. Let's set up Redux now to avoid reworking it later.

First, we'll need to add a few new dependencies to our project:

npm install redux react-redux redux-thunk

React-Redux is the Redux binding for React. Redux Thunk will enable us to use promises in our actions instead of returning pure JSON objects.

Next, we'll need to modify index.js by creating the Redux store and adding the Redux Provider component. The Provider will make our Redux store accessible to all child components.

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux'; // redux bindings for react
import thunk from 'redux-thunk'; // to use promises for asynchronous actions
import { createStore, applyMiddleware, compose } from 'redux'; // to create the store and middleware
import reducers from './reducers/index.js';

const middleware = [thunk];
const store = createStore(reducers, {}, compose(applyMiddleware(...middleware)));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Now we're ready to start connecting components to the Redux store. We'll start in App.js by mapping actions to get started. We know that when our app loads, we'll want to dispatch an action that fetches and loads all of our existing contacts.

A quick note about dispatch: Dispatching is Redux's method for changing state. It is important to note that only actions called with Dispatch can modify state within Redux.

In order to do this, we'll have the componentDidMount lifecycle method call getContacts. The reason we are calling getContacts on App.js as opposed to inside Contact.js is that Contacts are global, so no matter what component gets called, we always want to have contacts loaded.

// App.js

// ...

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

// ...

class App extends Component {
  static mapDispatchToProps = (dispatch) => {
    return {
      getContacts: () => dispatch(getContacts())
    };
  };

  constructor(props) {
    super(props);
  }

  async componentDidMount() {
    const { getContact } = this.props;
    await getContacts();
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Contact List</h1>
        </header>
        <Contacts />
      </div>
    );
  }
}

const ConnectedApp = connect(null, App.mapDispatchToProps)(App);
export default ConnectedApp;

Now that App.js is connected, we can switch our focus to Contacts.js. We start with adding mapStateToProps and mapDispatchToProps, and then connecting them via the connect HOC (Higher Order Component).

// Contacts.js

import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import { updateContact } from '../actions';

class Contacts extends Component {
  static mapStateToProps = (state, ownProps) => {
    const { contacts } = state;
    const contactList = Object.values(contacts.byId);
    return {
      contactList,
      contactsById: contacts.byId
    };
  };

  static mapDispatchToProps = (dispatch) => {
    return {
      updateContact: (params) => dispatch(updateContact(params))
    };
  };

  constructor(props) {
    super(props);
    this.state = {
      selectedContactId: null
    };
  }

  _onContactSelected = (contactId) => {
    this.setState({selectedContactId: contactId});
  };

  _onContactUpdated = (contact) => {
    const { updateContact } = this.props;
    updateContact({contact});
  };

  render() {
    const { contactList, contactsById } = this.props;
    const { selectedContactId } = this.state;
    let selectedContact = {};
    if (selectedContactId) {
      selectedContact = contactsById[selectedContactId];
    }
    return (
      <Fragment>
        <div>
          <ContactList contactList={contactList}
                       onContactSelected={this._onContactSelected}
                       selectedContactId={selectedContactId}/>
        </div>
        <hr />
        <EditContact contact={selectedContact}
                     onUpdate={this._onContactUpdated} />
      </Fragment>
    );
  }
}

const ConnectedContacts = connect(Contacts.mapStateToProps, Contacts.mapDispatchToProps)(Contacts);
export default ConnectedContacts;

Up to this point, Contacts.js is the first component to implement both mapStateToProps and mapDispatchToProps. Redux passes both state and the current component's props to the mapStateToProps function. This allows for retrieval and mapping of data to the current component's props. mapDispatchToProps allows us to send actions to Redux to store data or make HTTP calls that we have defined in actions.

As a side note, we have implemented mapStateToProps by including it as a static method inside of the component. This is a non-standard method of implementing Redux functions. But, one of the key benefits is that this allows mapStateToProps be unit-testable without explicitly exporting it.

We introduced the concept of actions in our discussion of the ConnectedContacts component, but we didn't really talk about them. So let's do that now. The best way to think of an action is any operation that can modify Redux state. The most bulk of these actions will be HTTP calls, calls to retrieve data from local storage, or even calls to read from cookies. The reason writing good, clear actions is essential to the creation of a good web app is that it encourages you to modularize your code in a way that facilitates reuse of code between components and allows your code to be self-documenting. That said, let's take a look at our actions.

// actions.js

// ...

export const updateContact = (params) => {
  const { contact } = params;
  return (dispatch) => {
    const updatedContact = fetch(/* ... */);
    dispatch({
      type: 'UPDATE_CONTACT',
      payload: {
        contact: updatedContact
      }
    });
  };
};

In Redux, all actions must return an object with a type property. Thanks to the Redux-Thunk middleware, we can perform more complex operations, like asynchronous calls, within a function that dispatches an action. This allows us to move HTTP calls from components into actions and keep our component code clean.

// reducers/index.js

import { combineReducers } from 'redux';
import { ContactReducer } from './ContactReducer';

const reducers = combineReducers({
  contacts: ContactReducer
});

export default reducers;
// reducers/ContactReducer.js

const initializeState = function() {
  return {
    byId: {}
  };
};

const ContactReducer = (state = initializeState(), action) => {
  let newById = {};
  switch(action.type) {
    case 'UPDATE_CONTACT': {
      const { contact = {} } = action.payload;
      newById = {
        ...state.byId
      };
      if (contact) {
        newById[contact.id] = contact;
      }
      return {
        ...state,
        byId: newById
      };
    }
    case 'GET_CONTACTS': {
      // ...
    }
    default: {
      return state;
    }
  }
};

export { ContactReducer };

Actions don't modify Redux state directly, though. That's the reducer's job. The type value that we passed from the action tells the reducer exactly what to do. The reducer then handles the payload passed by the action by storing the data in a specified shape. We're not going to get into the specifics of state shape or data access here; it's a pretty lengthy topic and would require a blog post all its own.

Throughout this post, I've written about "modifying" state. In truth, this is a bit of a misnomer. We never actually want to modify the Redux state directly. Instead, we always want to return a modified copy of the state tree. This concept of immutable state is a crucial—and often overlooked—detail when writing reducers.

With all the pieces in place, we have laid the foundation for our app to use Redux to manage state. Because Redux allows you to have access to the entire state tree from any component in your project, it's easy to want to connect every component. This is a mistake. One of the most significant downsides to using Redux for all storage is the performance issues from having all components re-render based on global state. A good rule of thumb is that you want to have a single connected component with many unconnected components below the container level. It's the container's job to pass props to these unconnected components, just as you would in a typical React app. There are always exceptions, but you should strive to keep your components from being connected until it makes sense to connect them.

At this point, I hope you feel that Redux is not an overly complicated thing to implement and you feel comfortable throwing Redux into the simplest of web apps. Even if there is not a ton of data to manage, it helps break apart code into separate pieces that allows for more readable, maintainable code.

For More on React

For more information on React, check out All Things React, which features current updates, resources, tips and techniques, history, and other useful React information, including links to the Kendo UI for React component library.


Richard Reedy
About the Author

Richard Reedy

Richard Reedy has been working in the software field for over 12 years. He has worked on everything from operations to backend server development to really awesome frontend UI. He enjoys building great products and the teams around them. His latest venture is enabling technology to better serve food trucks around the United States.

Related Posts

Comments

Comments are disabled in preview mode.