Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Building a React-Based Chat Client With Redux, Part 2: React With Redux and Bindings

DZone's Guide to

Building a React-Based Chat Client With Redux, Part 2: React With Redux and Bindings

We pick up where we left off last time by exploring how to use Redux and bindings in our React.js apps to make them more scalable.

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Welcome back! If you missed Part 1 on React and ReactDOM, you can check it out here.

The Full Monty: React With Redux and Bindings

Adding Redux introduces additional complexity, and greatly increases the number of files and folders in the project. It also requires some additional dependencies and a change in process for building and serving the client.

No longer is it sufficient to pull in the libraries in script tags and serve a template from our simple Node/Express server. Now, we'll define our dependencies in package.json and pull them in with npm install.

New Dependencies

I didn't introduce Babel, Webpack, and Browserify for this; the react-scripts library was sufficient. It not only gives us the ability to use JSX, but it also compiles all the code into a bundle.js file and serves our client, even triggering the browsers to reload when the code changes.

Another library I added was react-redux, the official Redux bindings for React. Redux can be used on its own or with other frameworks like Vue, but if you're using it with React, this library makes it much simpler to integrate the two.

Here are our dependencies now:

  "dependencies": {
    "react": "^16.4.0",
    "react-dom": "^16.4.0",
    "react-redux": "^5.0.7",
    "react-scripts": "1.1.4",
    "redux": "^4.0.0",
    "socket.io-client": "^1.5.1"
  },

Application Structure With Redux

Remember how all the code was in four files under one folder before? It's all spread out now, and the monolithic nature of the original app is no more. There's room for growth.

Source Folder (Top Level)
Source folder (top level)Components Folder
Components folderConstants FolderConstants folderStore Folder (Top Level)Store folder (top level)Store Folder (Functional Area)
Store folder functional area

Utils FolderUtils folder

As I describe the major changes to the app below, all these folders will make more sense. You can peruse the completely refactored code in the latest version, but if you just want to read on, don't worry, I'll provide links to both versions again at the bottom.

Data Flow Revisited

Actions and Action Creators

In Redux, all of the application's state is held in a store, and the only way to modify it is via an action. This means that all those setState() calls in the Client component are now verboten. Instead, we dispatch actions, which are just plain objects with a 'type' property and optionally some other arbitrary properties depending upon what we're trying to accomplish. The dispatch method actually lives on the store, but as we'll see in a bit, react-redux can inject that method into our components to make life easier. We never have to interact with the store directly.

With actions, we're delegating the change of state that was happening inside components to another part of the system. And since we might need to dispatch the same type of action object from multiple places in the app with different property values, we don't want to duplicate the effort of declaring it (and possibly have the two places get out of sync during ongoing maintenance), so there is the additional concept of action creators. Here's an example:

// Socket related actions
export const CONNECTION_CHANGED = 'socket/connection-changed';
export const PORT_CHANGED       = 'socket/port-changed';

// The socket's connection state changed
export const connectionChanged = isConnected => {
    return {
        type: CONNECTION_CHANGED,
        connected: isConnected,
        isError: false
    };
};

// The user selected a different port for the socket
export const portChanged = port => {
    return {
        type: PORT_CHANGED,
        port: port
    };
};

Reducers

A dispatched action will be handled by a reducer, which is just a pure function which returns a new value for state. The reducer is passed to the state and the action, and returns a new state object with all the same properties, though it may contain replacement values for all, part, or none of those properties, depending upon the type of action that was sent in. It never mutates the state but it can define the initial state if none is passed in. It can be said to reduce the combination of state and an action to some new object to represent the state. Here's an example that responds to the actions created in the code above:

// Socket reducer
import { CONNECTION_CHANGED, PORT_CHANGED } from '../../store/socket/actions';
import { UI } from '../../constants';

const INITIAL_STATE = {
    connected: false,
    port: String(UI.PORTS[0])
};

function socketReducer(state=INITIAL_STATE, action) {
    let reduced;
    switch (action.type)
    {
        case CONNECTION_CHANGED:
            reduced = Object.assign({}, state, {
                connected: action.connected,
                isError: false
            });
            break;

        case PORT_CHANGED:
            reduced = Object.assign({}, state, {
                port: action.port
            });
            break;

        default:
            reduced = state;
    }
    return reduced;
}

export default socketReducer;

Notice that the reducer switches on the action's type field and produces a new object to replace the state with. And if the action type doesn't match any of its cases, it simply returns the state object that was passed in.

Creating the Store

We've seen the action creators and the reducers, but what about the store that holds the state?

In the previous version of the app, the state was created in the Client component's constructor as one object with all its properties. Do we just create the store with that same monolithic state object? We could, but a more modular way is to let each reducer contribute the parts of state that it works with.

As you'll notice in the reducer above, the INITIAL_STATE object has two properties with their initial values. It manages the parts of state that are related to the socket. There are also reducers for the client status and for messaging. By decomposing the state into separate functional areas, we make the app easier to maintain and extend.

The first step to creating the overall state that the store will hold is to combine all the reducers:

import { combineReducers } from 'redux';
import socketReducer from './socket/reducer';
import messageReducer from './message/reducer';
import statusReducer from './status/reducer';

const rootReducer = combineReducers({
    socketState: socketReducer,
    messageState: messageReducer,
    statusState: statusReducer
});

The rootReducer is a single reducer which chains all the reducers together. So an action could be passed into the rootReducer and if its type matches a case in any of the combined reducers' switch statements, we may see some transformation, otherwise, no state change will occur.

Also, note that the object we define and pass to the combineReducers function has properties like:

messageState: messageReducer

Recall that the result of a reducer function is a slice of application state, so we'll name that object appropriately.

With the rootReducer assembled, we can now create the store, like so:

import { createStore } from 'redux';

const store = createStore(rootReducer); 

export default store;

The Redux library's createStore function will invoke the rootReducer with no state (since it doesn't yet exist) causing all the reducers to supply their own INITIAL_STATE objects, which will be combined, creating the final state object that is held in the store.

Now we have a store that holds all the application state that was previously created in the Client component's constructor and passed down to its child components as props. And we have action creators that manufacture our action objects, which when dispatched will be handled by reducers, which in turn produce a new state for the application. Wonderful. Only two questions remain:

  1. How is a component notified when the state changes?

  2. How does a component dispatch an action in order to trigger a state change?

Injection

This is where the react-redux library really shines. Let's have a look at the MessageInput component, which manages the text field where a user enters their chat handle:

import React, { Component } from 'react';
import { connect } from 'react-redux';

// CONSTANTS
import { Styles } from '../../constants';

// ACTIONS
import { outgoingMessageChanged } from '../../store/message/actions';

// Text input for outgoing message
class MessageInput extends Component {

    // The outgoing message text has changed
    handleOutgoingMessageChange = event => {
        this.props.dispatch(outgoingMessageChanged(event.target.value));
    };

    render() {
        return <span>
            <label style={Styles.labelStyle} htmlFor="messageInput">Message</label>
            <input type="text" name="messageInput"
                   value={this.props.outgoingMessage}
                   onChange={this.handleOutgoingMessageChange}/>
        </span>;
    }
}

// Map required state into props
const mapStateToProps = (state) => ({
    outgoingMessage: state.messageState.outgoingMessage
});

// Map dispatch function into props
const mapDispatchToProps = (dispatch) => ({
    dispatch: dispatch
});

// Export props-mapped HOC
export default connect(mapStateToProps, mapDispatchToProps)(MessageInput);

First, notice that the MessageInput class itself doesn't have an export keyword on it.

Next, direct your attention to the bottom of the file, where you'll notice two functions 'mapStateToProps' and 'mapDispatchToProps.' These functions are then passed into the imported react-redux function 'connect,' which returns a function that takes the MessageInput class as an argument. That function returns a higher-order component which wraps MessageInput. Ultimately, the HOC is the default export.

The magic that's provided by this HOC is that when the state changes, the mapStateToProps function will be invoked, returning an object that contains the parts of state that this component cares about. Those properties and values will now show up in the component's props.

Earlier, when we combined the reducers, remember how we named the slices of state? Now you can see where that comes into play. When mapping state to props inside a component, we can see that the application's state object holds the output of each reducer in a separate property.

const mapStateToProps = (state) => ({
    outgoingMessage: state.messageState.outgoingMessage
});

Finally, the mapDispatchToProps function is called, causing the store's dispatch method to show up as a component prop called 'dispatch.'

This is a much better way for components to receive parts of application state than having it passed down a component hierarchy where intervening components may not care about the values, but must nevertheless traffic in them since their children do.

Remember earlier, what the Client component's render function looked like after refactoring to JSX? It was definitely easier to read than the version based on nested React.createElement() calls, but it still had to pass a ton of state down into those children. I promised we'd streamline that and with react-redux, this is what it looks like now:

 render() {
        return <div style={clientStyle}>
            <UserInput/>
            <PortSelector/>
            <RecipientSelector/>
            <MessageTransport socket={this.socket}/>
            <MessageHistory/>
            <Footer socket={this.socket}/>
        </div>;
    }

The Socket utility class instance is the only thing that the Client manages now, aside from rendering the other components. The MessageTransport needs a reference to the socket so it can be included with the SEND_MESSAGE action that it dispatches, and the Footer needs the socket instance so the ConnectButton can call its 'connect' and 'disconnect' methods.

So we've answered the first question: How is a component notified when the state changes?  What about the second one: How does a component dispatch an action?

We know that the dispatch function has been added to the component as a prop by react-redux. Now, all it needs is the appropriate action creator. In the case of the MessageInput component above, it imports the action creator function 'outgoingMessageChanged' and uses it to create the action to dispatch when the text input changes:

    // The outgoing message text has changed
    handleOutgoingMessageChange = event => {
        this.props.dispatch(outgoingMessageChanged(event.target.value));
    };

And that brings us full circle. Components now have the necessary bits of application state injected into their props, and are able to easily create and dispatch actions that trigger reducers to transform the state. Application state has been decomposed into functional areas along with corresponding action creators and reducers.

Conclusion

If you're just starting with React, you certainly don't need to absorb all that toolchain fatigue just to get your feet wet. With just React and ReactDOM, you can make something happen and get to joy pretty quickly.

However, for anything even moderately ambitious, you're probably going to be well-served by upping your application's state-management game. There are plenty of libraries out there to help you with this, Flux, Redux, MobX, RxJS, etc., so you may want to study up on the pros and cons of each. You're definitely going to see an apparent increase in complexity when you refactor how your app handles state, but that's necessary for the app to grow in a maintainable way.

I hope you've enjoyed this little exercise and now have a better understanding of what's involved in refactoring a React-only app to use Redux. Here again, for reference, are the two versions of the project:

Are there things I could've done better in either the codebase or this article? Whether React and Redux are new to you or old hat, I'd love to hear your feedback on this exercise in the comments. 

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
web dev ,react.js ,redux ,bindings ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}