DevOps - Setting Up A React App with React-Redux
After you have instantiated a new app following the Docker, Django, React, AWS guidelines, you should have a 'frontend' folder structure that looks like this:
/Test_project
---/frontend
--- ---/node_modules
--- ---/public
--- ---/src
--- ---package.json
--- ---package-lock.json
--- ---.gitignore
--- ---Dockerfile
--- ---README.md
---/backend
---docker-compose.ymlMost of the relevant React files will have been auto-generated into the /src directory. Inside /src you should see "index.js" and "App.js" among others. If you run "$sudo docker-compose up" to start the containers, you should see the React "starter" page with the spinning logo at the frontend port specified in the docker-compose.yml file.
React Organization Note: It's okay to reorganize files within the /src folder! Depending on the layout and size of your app, you may even need to move or rename App.js at some point. Any time you reorganize your file structure within /src, however, you will want to go through and make sure that all of the "import" lines at the top of files that call each other have also been updated.
The important files to adjust within the /public folder are "index.html" and "favicon.ico", mainly to customize the information that appears on the browser tab. You can also eventually delete the two react logo files that are included; they are only used for the initial "spinning logo" page that you get when you haven't started working on your new app yet.
At this stage, it is time to determine how the front end of your app will manage its global state. Global state refers to any information that needs to be available across multiple parts of the React app. Examples include whether or not the user has logged in, whether the user has selected to view an app in dark mode, and information that may have been returned to the front end from the back end via an API hit.
For a React app that is big enough to require a state management tool, a common choice is React-Redux.
Global State Management With React-Redux
React-Redux creates a global state, called the "store", which it makes available to React Components by dispatching selected elements of the state into a component's props. It also allows components to manage the state through functions that use an included method called "dispatch()" to instruct a reducer to perform a specified action.
Important React-Redux Lingo:
store - the global state
action - an object that communicates the 'type' of action to be performed on the global state, and the optional 'payload' of new data
reducer - a switch case that takes in an initial version of the state and the action and returns the modified state
dispatch - a method in the Redux library that is used to send the action instruction to the reducer
Setting Up React-Redux
Install the main Redux library, the React-Redux library, and thunk, a middleware provider, by running the following commands:
$sudo docker-compose run --rm frontend npm install --save redux react-redux redux-thunkYou can also install the Chrome extension "Redux DevTools" from the Chrome Web Store. We will be adding code to our "createStore()" function that allows the global state to be easily viewed from the browser using this extension.
Creating the Store
Create a file called "store.js" in your /src folder, and copy this code into it:
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';
//initialize the global store as an empty object
let persistedState = {
authentication : { token: null }
};
//check the user's local storage for a token
try {
const storedToken = localStorage.getItem('token');
if (storedToken) {
persistedState.authentication = {
token: JSON.parse(storedToken)
}
}
} catch (error) {
console.log("error accessing local storage: ", error);
}
const middleware = [thunk];
//create the store
const store = createStore(
rootReducer,
persistedState,
compose(
applyMiddleware(...middleware),
(window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__()) || compose));
//any changes to the token should be saved to the user's local storage
store.subscribe(() => {
try {
const serializedToken = JSON.stringify(store.getState().authentication.token);
localStorage.setItem('token', serializedToken);
} catch (error) {
console.log("error saving token to local storage: ", error);
}
})
export default store;This code will create the global state that checks the user's local storage for a token from a previous login. Using local storage for the authentication token will prevent the user from having to log back in every time the app refreshes.
For this store.js file to work, you will also need to provide the createStore() function with a reducer. If there will ultimately be multiple different types of data in your global state, you may want to split up your reducers, actions, and state in order to keep them organized. In this case, you will also want to create a root reducer.
Creating Your First Reducer & the Root Reducer
Here are a test reducer and rootReducer file you can use to get started. Make sure that the file paths and filenames in your import lines at the top of these pages and your store.js page are accurate to the folder structure and naming conventions you are using for your project.
A sample reducer file that can handle returning a token string to the global state:
//create an initial version of the state with default values
const initialState = {
token: null
}
// Use the initialState as a default value
export default function sampleReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case "login":
//use the spread operator to return the rest of the state
//then modify the part you want to change
return {
...state,
token: action.payload //the token will be received in the "action" object
}
case "logout":
return {
...state,
token: null
}
default:
// If this reducer doesn't recognize the action type, or hasn't
// been given instructions for this specific action, return the existing state unchanged
return state;
}
}A sample root reducer file:
import { combineReducers } from 'redux';
import authReducer from './testReducers/authReducer';
import sampleReducer from './testReducers/sample1';
export default combineReducers({
authentication: authReducer,
sample: sampleReducer
})As you create additional reducers to manage different types of data in the global state, you will need to add them to the root reducer file the same way that "sampleReducer" is imported and combined into an object.
Providing the Global State to the React App
Finally, you will need to make sure your React project has access to the global state. This is done by manipulating the index.js file, which is the React file that serves your project (App.js) to the html page.
Import the "Provider" from Redux and your "store" from store.js into the index.js file, then wrap the <App /> component with the Provider component and pass in the store as a parameter. Your index.js file code should look like this:
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './store'
import App from './App';
ReactDOM.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();Once this is done, reboot your containers to make sure the new javascript libraries have been fully installed and check the front end port in your browser. The React logo should be spinning again with no errors in the console (right-click "Inspect" or CTRL+Shift+I). If you installed the Redux DevTools browser extension, you should see a green bullseye at the upper right side of your browser. Clicking this should open a window that reveals your global state:

The screenshot shows a "sample" reducer which was imported into the root reducer but not provided in this tutorial. If you are following this tutorial to get a new app started, you may want to remove the import line and the "sample" key/value pair from the root reducer (or create your own sample reducer) before checking the app in your browser window.
Using App.js to Create a Sample Action
So far we have set up a global state which checks the user's local storage for a token on load (but returns null if there is none), and a reducer which can provide the global state with a token code. Now it's time to add an action that will tell the reducer to modify the token.
Step 1: Connect your React component to the global state.
Import Redux's "connect" method into your component and wrap the component's export line with connect(). The format will be something like this:
import React from 'react';
import { connect } from 'react-redux';
import './App.css';
function App() {
return (
<div className="App">
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);In this example, "mapStateToProps" and "mapDispatchToProps" are placeholders for code that you will be writing next.
Step 2: Define what information from the global state will be available to your component.
Write a function (conventionally called "mapStateToProps") that takes the global state as an argument and returns whatever variables this component will need. If you are using a root reducer, remember that your global state object will be an object of objects, and the name your gave each reducer will be the name of each interior object. In the case of our token example, mapStateToProps looks like this:
const mapStateToProps = (state) => {
return {
token: state.authentication.token
}
}Step 3: Define which actions your component will need to dispatch to a reducer.
The next argument that can be passed to connect() is an object containing the functions that return an action which this component will be able to dispatch to the reducer to modify the global state. If you have a lot of actions, you should define them in a dedicated "actions.js" file (commonly kept in the same place as the reducer.js file that contains the reducers that correspond to it). Below is an example of an App.js file structure that import the functions to your component and passes them directly to connect in an object.
import React from 'react';
import { connect } from 'react-redux';
import { testLogin, testLogout } from './redux/authActions.js'
import './App.css';
function App() {
return (
<div className="App">
</div>
);
}
const mapStateToProps = (state) => {
return {
token: state.authentication.token
}
}
export default connect(mapStateToProps, { testLogin, testLogout })(App);The test functions that were defined in "authActions.js" in this case look like this:
export const testLogin = ( email, password ) => dispatch => {
if (email && password) {
dispatch({
type: "login",
payload: "test-token"
})
}
}
export const testLogout = () => dispatch => {
dispatch({
type: "logout"
})
}For your app, the log in function will probably be an ajax POST that sends the username and password to your back end and receives a token in response if the login information is valid. In this setup, your function that returns the action object will look more like this:
import axios from 'axios';
export const logIn = (loginForm) => dispatch => {
axios
.post(`${globalDomain}/api/auth/`, loginForm, { crossdomain: true })
.then(res => {
//dispatches the LOG_IN action to the reducer
dispatch({
type: LOG_IN,
payload: {
token: res.data.token,
}
})
})
.catch(err => {
console.log("log in fail: ", err)
dispatch({
type: LOG_IN_ERROR,
payload: err.message
})
})
}For a complete set of instructions on how to manage your front end and back end data flow with ajax, visit this tutorial.
Finally, you will want your App.js component be able to submit a test username and password to the testLogin function, which will result in the action that saves the test token being dispatched to the reducer. Here is an extremely basic setup for this kind of testing. Note that the "App" function had to be converted into a class component in order to hold the value of the two input fields in its own local state before submitting them to the testLogin function.
class App extends Component {
state = {
email: "",
password: "",
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value,
})
}
loginButtonClicked = event => {
event.preventDefault();
this.props.testLogin(this.state.email, this.state.password);
}
logoutButtonClicked = () => {
this.props.testLogout();
}
render() {
if (this.props.token) {
return (
<div className="App">
"You logged in!"
<button onClick={this.logoutButtonClicked}>Log Out</button>
</div>
)
} else {
return (
<div className="App">
<form onSubmit={this.loginButtonClicked}>
<label>
Email:
<input type="text" id="email" value={this.state.email} onChange={this.handleChange} />
</label>
<label>
Password:
<input type="password" id="password" value={this.state.password} onChange={this.handleChange} />
</label>
<input type="submit" value="Log In" />
</form>
</div>
)
}
}
}
const mapStateToProps = (state) => {
return {
token: state.authentication.token
}
}
export default connect(mapStateToProps, { testLogin, testLogout })(App);These examples should give you all the basics you need in order to start creating a React front end with Redux for state management! All of these libraries also have extensive docs available online.