Redux and React :: Full-stack web development "Hello World" tutorials for entrepreneurs: Part 5 of 10
This post is part 5 of a 10-part series within a series that is designed to teach full-stack web development for entrepreneurs. Here we’ll begin diving into Redux and React.
Redux and React
The above tutorial setup a lot of “magic” between our Ruby back-end and the JavaScript receiving and sending data through ActionCable. In reactive web applications, there is always an architectural question of how to manage the “state” of front-end code (e.g. what tab is selected, what menu is showing, what data is in an element, etc…). In the StimulusReflex world, this state is managed by parameters in HTML tags (e.g. those “data-x” parameters).
However, predating StimulusReflex was React, followed closely by Redux for state management. React was birthed out of the developer team at Facebook as a way to create fast and “reactive” web experiences with highly dynamic data. At a high-level, the way Redux/React works is by passing parameters from a data store into a collection of React components that present data conditionally based on what those parameters are. If a React component has some kind of interactive feature (e.g. a link, button, form, input field, etc…), then that input can be passed back to Redux to update the data store, and if desired, sent off to a back-end server for safe keeping.
Fundamentally, Redux works via a “one way” data stream of “actions” that are sent through “reducers” to update “application state” in order to send/receive data from a server do things like update interfaces in a browser.
Importantly, Redux works by creating a new application state object every time an action is fired. In other words, the state object is immutable; by creating new state objects, this allows Redux to do interesting things like keep an accurate history of state to facilitate features such as undos and navigation retracing.
Another way to show the above flow is this gif:
When we write Redux applications, we use a dispatch() function to send event actions into a set of reducers that are “listening” for actions applicable for their part of the store. Reducers that have a “job to do” to update state when an action comes their way end up returning a new state object, which is then handed by the UI as defined by the “presentational” React components you build. In other words, Redux is the “container” of the data, and React “presents” the data in UIs and sends events back to Redux, which then fires off actions to reducers.
It’s worth taking a moment to at least skim through the Redux Essentials overview on their main website. Importantly, Redux understands it is a fair amount of JavaScript to write (especially compared to what we did with StimulusReflex!), so it knows it’s not applicable for all applications. Redux is best for large, complex code bases worked on by many developers. As a student, it is important for you to at least have a functional understanding of the basic concepts of Redux and React before we dive deeper into it in Part 2 of this series when we build a React Native mobile app.
For now, let’s get started with some basic setup within our Rails app. Typically, a Redux/React app would be its own separate code base and deployed onto a static website hosting provider (such as Google’s Firebase). However, for simplicity in our learning environment here we can integrate Redux and React inside our Rails app to have an isolated “app within an app”.
Ok, let’s get started first by making sure you have downloaded and installed the React DevTools Extension for Chrome and Redux DevTools Extension for Chrome. These are important for understanding (and debugging!) how our Redux/React application is working - or not working - under the hood.
Now let’s install the Ruby and JavaScript packages from our terminal:
- bundle add react-rails
- yarn add react redux react-redux @reduxjs/toolkit redux-immutable immutable normalizr axios prop-types redux-thunk react-dom react_ujs
- yarn add @babel/preset-react --dev
As you can immediately see, there are quite a lot of JavaScript packages required for building and running a Redux/React app. We’ll discuss what each one does as we go.
The “@babel/preset-react” package for our dev environment (i.e. the “--dev” flag), for example, makes sure that when our JavaScript compiles it will correctly interpret our React JSX syntax.
In fact, let’s go ahead and open up our babel.config.js file (which is at our quotesapp root directory) and we’ll create a new line 20 containing:
['@babel/preset-react', {}],
So the diff will look like this:
The way we’re going to approach the setup is to start from the Rails view and work our way backwards, so on Line 45 of our app>views>welcome>index.html file (i.e. at the bottom) go ahead and put in:
<%= react_component 'ReactQuotesApp' %>
The diff will look like this:
What we’re doing here is leveraging the react_component method from the react-rails gem to render our React app within our Rails app. Our little React app will be defined as “ReactQuotesApp”, so we’ll need to open up our app>javascript>packs>application.js file and add the following starting at line 11:
const ReactRailsUJS = require('react_ujs');
window.document.onload = function(e){
ReactRailsUJS.mountComponents();
}
global.ReactQuotesApp = require('../react_quotes_app').default;
...which will look like this diff:
What we’re doing here is to tell react-rails to mount our components after the page is loaded, and we’re defining our ReactQuotesApp variable from a react app we’ll need to build.
So, let’s go ahead and create a new folder called react_quotes_app inside our app>javascript folder, and in it create an index.js file containing simply:
import App from './app';
export default App;
And now let’s create an app.js file within our new directory and put the following in it. This is where the magic starts to happen:
import React from 'react';
import { Provider } from 'react-redux'
import { store } from './store';
import Quotes from './quotes_container';
const app = () => (
<Provider store={store}>
<Quotes />
</Provider>
);
export default app;
First we make sure that React is available, and that we can provide it with data from our store. We’ll set up our Redux container file that will serve as the data interface (i.e. the “state container”) for our entire little React app here.
Finally, we use standard React JSX syntax to define a function variable called app that wraps our Quotes component with our Provider, and we make sure the default export variable from this file is our app.
Now, from this file we first need to set up our store. Go ahead and create a file called store.js in our ReactQuotesApp folder here and in it put the following:
import thunk from 'redux-thunk'
import { configureStore } from '@reduxjs/toolkit'
import RootReducer from './reducer'
export const store = configureStore({
reducer: RootReducer,
middleware: [thunk],
})
export default store
There is a LOT going on in this little file. The concept of a Redux “store” can be confusing, so let’s talk through it.
First, we introduce and define the concept of a thunk for Redux. Here is a great explanation of what a “thunk” is. In short, a thunk is a function that is wrapped by another function for the purpose of doing work later (e.g. asynchronously). We’ll use one to fetch quotes from our server and save them in our store.
Our imported thunk here from redux is actually intended to be a “middleware”; i.e. software that is run “in the middle” of a stack to do certain things. For our purposes, we’re going to use it to fetch data from a simple API server we’ll set up within Rails:
In addition, we use configureStore from Redux Toolkit, which handles some basic development setup for us (such as activating Redux DevTools). Technically, configureStore() would automatically import redux-thunk for us, but I manually included it here for educational purposes so you can see how to add other middleware if you’d like, such redux-logger).
Finally, setting up the store entails feeding it the root reducer, which we’ll set up and talk about next.
Create a file called reducer.js in your ReactQuotesApp folder and in it put the following:
import { combineReducers } from 'redux-immutable'
import QuotesReducer, * as QuotesSelect from './quotes_reducer'
const RootReducer = combineReducers({
quotes: QuotesReducer,
})
export default RootReducer;
export const getQuote = (state, id) => QuotesSelect.getQuote(state.quotes, id);
export const getQuotesIds = (state) => QuotesSelect.getQuotesIds(state.quotes);
export const getQuotesLoaded = (state) => QuotesSelect.getQuotesLoaded(state.quotes);
export const getQuotesLoading = (state) => QuotesSelect.getQuotesLoading(state.quotes);
What we’re doing here is setting up our RootReducer to be able to add more reducers in the “tree” later if we want, but for now we are just adding a reducer for the “quotes” part of our store.
In addition, we added four lines at the bottom of this file that define selectors, i.e. functions that we’ll use in our Redux containers to select (get) data from our store.
Now since we rely on importing our quotes reducer, let’s go ahead and create a file in the same directory called quotes_reducer.js and put the following in it:
import { Map, List } from 'immutable';
import actions from './actions';
import { combineReducers } from 'redux-immutable'
const DEFAULT_MAP = new Map();
const DEFAULT_LIST = new List();
function byId(state = DEFAULT_MAP, action) {
if (action.response && action.response.entities && action.response.entities.quotes) {
return state.merge((new Map(action.response.entities.quotes)).map(props => new Map(props)));
}
return state;
}
function allIds(state = DEFAULT_LIST, action) {
switch (action.type) {
case actions.FETCH_QUOTES_SUCCESS:
return new List(action.response.result);
default:
return state;
}
}
function loading(state = false, action) {
switch (action.type) {
case actions.FETCH_QUOTES_REQUEST:
return true;
case actions.FETCH_QUOTES_SUCCESS:
case actions.FETCH_QUOTES_FAILURE:
return false;
default:
return state;
}
}
function loaded(state = false, action) {
switch (action.type) {
case actions.FETCH_QUOTES_SUCCESS:
return true;
default:
return state;
}
}
const DEFAULT_STATE = new Map();
export const QuotesReducer = combineReducers({
byId,
allIds,
loading,
loaded,
}, () => DEFAULT_STATE);
export const getQuote = (state, id) =>
state.getIn(['byId', id.toString()]);
export const getQuotesIds = (state) =>
state.get('allIds');
export const getQuotesLoading = (state) =>
state.get('loading') ;
export const getQuotesLoaded = (state) =>
state.get('loaded') ;
export default QuotesReducer;
There is again a LOT going on in here, but fundamentally we are importing a couple important data structures from immutable.js that play nice with Redux (i.e. a Map and a List, which are similar to Objects and Arrays, respectively, but aren’t intended to be modified).
We are also setting up our four reducer functions that are combined to make the QuotesReducer. These reducer functions are intended to be constantly listening for actions, and if an applicable action is fired, it will update its part of the state. Note that the part of the state that these reducers return is a completely new thing (Map, List, boolean, etc...). This is an important part of Redux.
Note for advanced developers: I am intentionally not introducing the concept of Redux Slices in this tutorial since it abstracts away a lot of the fundamentals that are helpful for students to learn. Students, we’ll revisit the concept of Slices in Part 2 of this series, but for now we’ll manually create immutable reducers and action functions so you better understand how Redux works under the hood.
With our reducer tree setup, now let’s prep our actions and API setup. Let’s first create a file called schema.js in our RailsQuotesApp folder and put the following two lines it:
import { schema } from 'normalizr';
export const quote = new schema.Entity('quotes');
What this is doing is helping to format our quotes data that we’ll receive from our Rails server.
Now let’s create our actions.js file in the same folder and have it contain:
import * as api from './api';
const ACTIONS = {
FETCH_QUOTES_REQUEST: 'RQA::FetchQuotesRequest',
FETCH_QUOTES_SUCCESS: 'RQA::FetchQuotesSuccess',
FETCH_QUOTES_FAILURE: 'RQA::FetchQuotesFailure',
}
const genericAPI = (action, request, success, failure, errorMessage, meta = {}) => (dispatch) => {
dispatch({ type: request, ...meta });
return action().then(
response => dispatch({ type: success, response, ...meta }),
error => dispatch({ type: failure, message: error.message || errorMessage, ...meta }),
);
};
export const fetchQuotes = () =>
genericAPI(
api.fetchQuotes,
ACTIONS.FETCH_QUOTES_REQUEST,
ACTIONS.FETCH_QUOTES_SUCCESS,
ACTIONS.FETCH_QUOTES_FAILURE,
'Failed to load quotes',
);
export default ACTIONS;
With every action function, Redux wants a simple string passed to it to define the action type, so we set up those strings in our ACTIONS object.
Next we are creating a genericAPI function that handles the firing of our API calls and returning of data through the states of requesting, succeeding, or failing the API call.
Finally, we use the genericAPI function to create our function to fetch quotes from our server.
And, the astute student will notice that genericAPI is a thunk, which we talked about above.
Now let’s create a new file called api.js in that same folder and in it put the following:
import axios from 'axios';
import { normalize } from 'normalizr';
import * as schema from './schema';
axios.interceptors.request.use((config) => {
const newConf = config;
const csrfMetaTag = document.head.querySelector('meta[name="csrf-token"]');
if (csrfMetaTag) {
newConf.headers['X-CSRF-Token'] = csrfMetaTag.content;
}
newConf.headers['Content-Type'] = 'application/json';
newConf.headers.Accept = 'application/json';
newConf.responseType = 'json';
return config;
});
const normalizeQuotes = response => normalize(response.data, [schema.quote]);
export const fetchQuotes = () =>
axios.get('/api/v1/quotes').then(normalizeQuotes);
We are using Axios as our API client for HTTP requests, which makes life easier for us to work with data being sent and retrieved. Our interceptors function here is important to structure all of our API calls from our web browser such that our Rails server will recognize the requests as authentic (i.e. it will help prevent us from getting hacked).
At the bottom of our API file here we define a function to “normalize” our data when we expect quotes from our server, and we ultimately define our fetchQuotes() function to perform our API call.
Now that we’ve set up our basic tooling for Redux, let’s make sure to now define our container and view for our QuotesApp. Let’s go ahead and create a file called quotes_container.js in our same folder and in it put the following:
import { connect } from 'react-redux'
import QuotesView from './quotes_view'
import { getQuotesIds, getQuotesLoaded, getQuotesLoading } from './reducer'
import { fetchQuotes } from './actions'
const mapStateToProps = (state) => {
return {
quotesIds: getQuotesIds(state),
quotesLoaded: getQuotesLoaded(state),
quotesLoading: getQuotesLoading(state),
};
};
const mapDispatchToProps = {
fetchQuotes
};
const QuotesContainer = connect(mapStateToProps, mapDispatchToProps)(QuotesView)
export default QuotesContainer
This contact file is where all the guts of Redux comes together. Our connect function is what’s used to wrap our QuotesView with data and functions. The mapStateToProps function is used for passing data from our store into our view, and the mapDispatchToProps function is what’s used to pass actions into our view.
Now let’s go ahead and create a file called quotes_view.js and have it contain:
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { List } from 'immutable'
import Quote from './quote_container'
const propTypes = {
quotesIds: PropTypes.instanceOf(List).isRequired,
quotesLoaded: PropTypes.bool,
quotesLoading: PropTypes.bool,
fetchQuotes: PropTypes.func.isRequired,
}
const defaultProps = {
quotesLoaded: false,
quotesLoading: false,
};
const QuotesView = ({
quotesIds,
quotesLoaded,
quotesLoading,
fetchQuotes,
}) => {
useEffect(() => {
if (!quotesLoaded){
fetchQuotes();
}
}, [quotesLoaded, fetchQuotes])
return (
<div>
<h2>React Quotes App</h2>
{quotesLoading ?
<p>Loading...</p>
:
<p>Here is your list of quotes:</p>
}
<ul>
{quotesIds && quotesIds.map(id => <Quote key={`quote-${id}`} id={id} />)}
</ul>
</div>
)
}
QuotesView.propTypes = propTypes;
QuotesView.defaultProps = defaultProps;
export default QuotesView;
This is the meat of a React component. In the Redux world we call this a “presentational component” because it is designed to simply present the data however we have Redux set up its state parameters.
We define propTypes and defaultProps to let React know what kind of data to expect (and what data should be set by default in a component). This way we’ll get useful error messages in our JavaScript console if we code up something incorrectly.
As we set up our QuotesView component, we need to pass it those four parameters as indicated, that way we can use it as desired in our view code.
The useEffect() function is triggered when the parameters at the end of that function change. So, when the component first loads, those parameters get populated and the useEffect fires off our fetchQuotes action. Once the quotes are loaded, it won’t fire the request again.
Finally, within our return we have a conditional render to check if the quotes are loading or not, then we iterate through our quoteIds to load up our Quote components, which we’ll have be our <li> elements.
So, let’s go ahead and create our quote_container.js file in that same folder and in it put:
import { connect } from 'react-redux'
import QuoteView from './quote_view'
import { getQuote } from './reducer'
const mapStateToProps = (state, { id }) => {
return {
quote: getQuote(state, id),
};
};
const mapDispatchToProps = {};
const QuoteContainer = connect(mapStateToProps, mapDispatchToProps)(QuoteView)
export default QuoteContainer
You’ll notice we simply pull out the { id } parameter since we are passing it in the props to the component.
Now let’s go ahead and create our quote_view.js file in that same folder and have it contain:
import React from 'react'
import PropTypes from 'prop-types'
import { Map } from 'immutable'
const propTypes = {
quote: PropTypes.instanceOf(Map),
}
const defaultProps = {};
const QuoteView = ({
quote,
}) => {
return (
<li>"{quote.get('content')}" - {quote.get('author_name')}</li>
)
}
QuoteView.propTypes = propTypes;
QuoteView.defaultProps = defaultProps;
export default QuoteView;
This is our <li> element that we’ll use to populate quotes from our database.
Now, on the Rails side, let’s go ahead and create a folder called app>controllers>api and app>controllers>api>v1 to follow conventions when building an API. Inside that v1 folder let’s create a file called quotes_controller.rb and have it contain:
class Api::V1::QuotesController < ApplicationController
def index
respond_to do |format|
format.json { render json: Quote.order(created_at: :desc).all }
end
end
end
Which will look like this:
What we’re telling our controller to do is simply output the JSON (JavaScript Object Notation) of our quotes, which our normalizer setup on the React side handles gracefully.
Let’s go ahead and open up our app>config>routes.rb file and add starting on line 5:
namespace :api do
namespace :v1 do
get '/quotes', to: 'quotes#index', as: 'quotes'
end
end
...which will make the diff look like this:
At this point you should have 21 file changes that in VSC looks like this:
Assuming you’re in sync here (go back and read the above carefully if you missed anything), then go ahead and restart your rails server and you should see your React app at the bottom of your localhost:3000 page. For me it looks like this:
Open your developer console in chrome (option+command+j) and click to your new Redux tab that your chrome extension gives you. In there you should see the store:
And you should see your running list of actions and the data sent/received from them:
Go ahead and browse around here to get a sense for how Redux works and how you’ll be able to use Redux devTools to help you develop. We’ll be using it a lot along the way.
We’ve made a LOT of changes, so let’s go ahead and push our changes to GitHub. Be sure to visually inspect all your changes in VSC (or “git status” and “git diff” in your terminal) - which should look like this commit diff - and then:
- git add .
- git commit -a -m "render hello world quotes with Redux and React"
- git push
Good - it’s generally best practice to make smaller commits along the way, but for our learning purposes as we set everything up, we just worked straight through a lot of material.
Comments
Interested in participating in the comments on this article? Sign up/in to get started:
Sign in with Ethereum
By signing up/in with Satchel you are agreeing to our terms & conditions and privacy policy.