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.
Join the DZone community and get the full member experience.
Join For FreeWelcome 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)
Components Folder
Constants Folder
Store Folder (Top Level)
Store Folder (Functional Area)

Utils 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
Most dispatched actions 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:
How is a component notified when the state changes?
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.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) => ({
outgoingMessageChanged: message => outgoingMessageChanged(message)
});
// 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, which creates a 'dispatcher function' that shows up as a component prop called 'outgoingMessageChanged
.'
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/>
<MessageHistory/>
<Footer/>
</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 a dispatcher function has been added to the component as a prop by react-redux. That dispatcher calls the appropriate action creator and dispatches the action returned. 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.outgoingMessageChanged(event.target.value);
};
And that brings us nearly 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.
The final architectural concern is the socket and its management. Where do we instantiate it, and how communicate with it?
Middleware
It isn’t a view component, but a couple of view components (ConnectButton and SendButton), need to initiate communications with it. Actions like CONNECT_SOCKET or SEND_MESSAGE are a great way to trigger things elsewhere in the application. But the reducers that respond to actions are supposed to be pure functions that only manage the state. How can we send an action and have that trigger a manipulation of the socket then?
The answer is middleware. Remember before when we created the store? Well, actions are part of the API for the store, so it makes sense that something that needs to respond to an action would probably need to be involved.
What we’ll have to do is create a ‘middleware function’ which will instantiate the socket and its various listeners, then return a function which will be called on every action that is dispatched. That function is wrapped by the closure that created the socket instance, and is around for the life of the app. It looks like this:
const socketMiddleware = store => {
// The socket's connection state changed
const onConnectionChange = isConnected => {
store.dispatch(connectionChanged(isConnected));
store.dispatch(statusChanged(isConnected ? 'Connected' : 'Disconnected'));
};
// There has been a socket error
const onSocketError = (status) => store.dispatch(statusChanged(status, true));
// The client has received a message
const onIncomingMessage = message => store.dispatch(messageReceived(message));
// The server has updated us with a list of all users currently on the system
const onUpdateClient = message => {
const messageState = store.getState().messageState;
// Remove this user from the list
const otherUsers = message.list.filter(user => user !== messageState.user);
// Has our recipient disconnected?
const recipientLost = messageState.recipient !== UI.NO_RECIPIENT && !(message.list.find(user => user === messageState.recipient));
// Has our previously disconnected recipient reconnected?
const recipientFound = !!messageState.lostRecipient && !!message.list.find(user => user === messageState.lostRecipient);
const dispatchUpdate = () => {
store.dispatch(clientUpdateReceived(otherUsers, recipientLost));
};
if (recipientLost && !messageState.recipientLost) { // recipient just now disconnected
store.dispatch(statusChanged(`${messageState.recipient} ${UI.RECIPIENT_LOST}`, true));
dispatchUpdate();
} else if (recipientFound) { // previously lost recipient just reconnected
store.dispatch(statusChanged(`${messageState.lostRecipient} ${UI.RECIPIENT_FOUND}`));
dispatchUpdate();
store.dispatch(recipientChanged(messageState.lostRecipient));
} else {
dispatchUpdate();
}
};
const socket = new Socket(
onConnectionChange,
onSocketError,
onIncomingMessage,
onUpdateClient
);
// Return the handler that will be called for each action dispatched
return next => action => {
const messageState = store.getState().messageState;
const socketState = store.getState().socketState;
switch (action.type){
case CONNECT_SOCKET:
socket.connect(messageState.user, socketState.port);
break;
case DISCONNECT_SOCKET:
socket.disconnect();
break;
case SEND_MESSAGE:
socket.sendIm({
'from': messageState.user,
'to': messageState.recipient,
'text': action.message,
'forwarded': false
});
store.dispatch(messageSent());
break;
default:
break;
}
return next(action)
};
};
This middleware function is able to instantiate the socket and respond to the actions that are related to the socket (connecting, disconnecting, sending messages), as well as dispatching actions that arise from the socket itself (connection status change, socket error, message received, update client with list of connected users).
We apply that middleware to the store when we create it like so:
// Root reducer
const rootReducer = combineReducers({
socketState: socketReducer,
messageState: messageReducer,
statusState: statusReducer
});
// Store
const store = createStore(
rootReducer,
applyMiddleware(socketMiddleware)
);
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:
React and ReactDOM only (Version 1.0.0)
React, ReactDOM, Reflux, react-reflux (Latest Version)
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.
Published at DZone with permission of Cliff Hall, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
How to Submit a Post to DZone
-
Avoiding Pitfalls With Java Optional: Common Mistakes and How To Fix Them [Video]
-
Is Podman a Drop-in Replacement for Docker?
-
Comparing Cloud Hosting vs. Self Hosting
Comments