Random Quote – Part 5 – Redux Thunk: Making a random quote machine in a few different flavors

17 min read

Here comes part 5 of the Making a random quote machine in a few different flavors series.

You can read more about this project’s background on the project’s page. Or start from the beginning with part 1 of this series.

In flavor #4, the different UI elements are updated using state management with Redux.

In this flavor, I continue updating the different UI elements with the current application state, but the app is calling an asynchronous endpoint as the data (quotes) is requested from an external resource (a JSON stored in a public content delivery network (CDN)).

In Part 6 (to be posted next week), I will cover a sixth flavor and will be using a User Interface JavaScript library (React) to build the app’s UI. If you’re curious about the next flavors and would like to make a writer very happy today, you should join Morse Wall’s newsletter.

Flavor #5: HTML + CSS + Redux + JSON with quotes

Like previously in flavor #2, I’m storing the quotes in a separate JSON file. This time I’m storing it in jsDelivr, a public CDN.

There is a limited number of quotes from the JSON in the code snippet below (so I can illustrate the point), but many more quotes in the JSON in production.

{
  "quotes": [
    {
      "quoteText": "\"Many of you appear concerned that we are wasting valuable lesson time, but I assure you we will go back to school the moment you start listening to science and give us a future.\"",
      "quoteAuthor": "@GretaThunberg"
    },
    {
      "quoteText": "\"I was fortunate to be born in a time and place where everyone told us to dream big. I could become whatever I wanted to. I could live wherever I wanted to. People like me had everything we needed and more. Things our grandparents could not even dream of. We had everything we could ever wish for and yet now we may have nothing. Now we probably don’t even have a future any more.\"",
      "quoteAuthor": "@GretaThunberg"
    },
    {
      "quoteText": "\"That future was sold so that a small number of people could make unimaginable amounts of money. It was stolen from us every time you said that the sky was the limit, and that you only live once. You lied to us. You gave us false hope. You told us that the future was something to look forward to.\"",
      "quoteAuthor": "@GretaThunberg"
    }
  ]
}

Calling asynchronous endpoints in Redux

Since I’m hitting asynchronous endpoints I need to adjust my code to make sure things will happen in order. I need to make sure the code will only try to render the quote when I have the data available, i.e. after I’ve finished fetching the data from the JSON file. This is of course necessary even when dealing with Vanilla JavaScript code like in flavor #2. The additional challenge in this flavor (with Redux) is that, by default, actions in Redux are dispatched synchronously. To help with this, I am using the Redux Thunk middleware. Redux Thunk is a middleman that sits in between an action being dispatched and the action reaching the reducer (for more on actions and reducers, check flavor #4).

The concept of a thunk comes from computer programming and more widely refers to using a function to delay the evaluation/running of an operation.

To create an asynchronous action, I return a function in an action creator that takes dispatch as an argument. Within this function, I can dispatch actions and perform asynchronous requests.

//handleAsync.js
import { requestingApiData } from "../../redux/actions/actions.js";
import { receivedApiData } from "../../redux/actions/actions.js";
import { store } from "../index.js";

// defining a special action creator that returns a function. The returned function takes dispatch as an argument. Within this function, I can dispatch actions and perform asynchronous requests. It's common to dispatch an action before initiating any asynchronous behavior so that the application state knows that some data is being requested (this state could display a loading icon, for instance). Then, once the application receives the data, another action is dispatched, an action that carries the data as a payload along with information that the action is completed.
const handleAsync = () => {
  return function(dispatch) {
    // dispatch request action here
    store.dispatch(requestingApiData());
    const makeRequest = async () => {
      const responseJSON = await fetch(
        "https://cdn.jsdelivr.net/gh/morsewall/jsondb@master/db.json"
      );
      const responseObject = await responseJSON.json();
      const quotes = responseObject.quotes;
      // dispatch received data action here
      store.dispatch(receivedApiData(quotes));
    };
    makeRequest();
  };
};

export default handleAsync;

In this case, the asynchronous request is fetch() (just like in flavor #2). Above is the special action creator designed to handle the asynchronous behavior. This is a special action creator since it returns a function instead of an action object (handleAsync returns a function). The returned function takes dispatch as an argument. It is within this function that I can dispatch actions and perform asynchronous requests.

It’s common to dispatch an action before initiating any asynchronous behavior so that the application knows that some data is being requested (this state could for instance display "Loading..." on the console). Then, once the application receives the data, another action is dispatched, this action carries the data as payload along with information that the action is completed.

The two dispatched action creators above (requestingApiData and receivedApiData) are defined in actions.js. They return actions containing information about action-events that have occurred. RECEIVED_API_DATA is tied to having received the data from a finalized fetch() and gives the apiData value a key named quotes.

//actions.js
import { REQUESTING_API_DATA } from "../constants/constants.js";
import { RECEIVED_API_DATA } from "../constants/constants.js";

// defining action creators related to the asynch function. Action creator is a function that returns an action (object that contains information about an action-event that has occurred). The action creator gets called by `dispatch()`
export const requestingApiData = () => {
  return {
    type: REQUESTING_API_DATA
  };
};

export const receivedApiData = apiData => {
  return {
    type: RECEIVED_API_DATA,
    quotes: apiData
  };
};

I’m defining those action types in constants.js.

//constants.js
// defining action types for the special asynch action creator-type function
export const REQUESTING_API_DATA = "REQUESTING_API_DATA";
export const RECEIVED_API_DATA = "RECEIVED_API_DATA";

When the action creators are called it is time for the reducer to make sense of the type of action and inform the Redux store how to respond to the action (i.e. telling the store how to modify the state).

//reducers.js
import { REQUESTING_API_DATA } from "../constants/constants.js";
import { RECEIVED_API_DATA } from "../constants/constants.js";

// defining default state
const defaultState = {
  status: "",
  quotes: []
};

//defining reducer functions to allow the Redux store to know how to respond to the action created
const getNextQuoteReducer = (state = defaultState, action) => {
  switch (action.type) {
    case REQUESTING_API_DATA:
      return {
        ...state,
        status: "waiting",
        quotes: []
      };
    case RECEIVED_API_DATA:
      return {
        ...state,
        status: "received",
        quotes: action.quotes
      };
    default:
      return state;
  }
};

export default getNextQuoteReducer;

Fetching the data

As per my user stories: “I want to be welcomed by a quote when I first load the app”. So far, however, I haven’t even fetched the quotes from the JSON yet. So, doing that now:

//index.js
"use strict";

import handleAsync from "./js-modules/handleAsync.js";

// The UMD build makes Redux available as a window.Redux global variable
const Redux = window.Redux;

//dispatching the asynch special action creator
store.dispatch(handleAsync());

From the above, I’m dispatching the handleAsync action creator, which will trigger an update of the state in the Redux store with the help of the reducer. When the app has finalized fetching, the Redux store will keep the full JSON contents stored in the state.

Adding the Redux Thunk library

I’m soon calling the Redux Thunk middleware, so I better add it to the project. The Redux Thunk library is available from a precompiled UMD package, so I can simply add it as script to my HTML. The snippet below shows the bottom of my HTML.

<!-- index.html -->
  </footer>
	<!-- adding the Redux library from a precompiled UMD package. window.Redux is the global variable to call Redux. -->
	<script src="https://unpkg.com/redux@4.0.4/dist/redux.js"></script>
	<!-- adding the Redux Thunk library from a precompiled UMD package. window.ReduxThunk.default is the global variable to call Redux Thunk. -->
	<script src="https://unpkg.com/redux-thunk@2.3.0/dist/redux-thunk.min.js"></script>
	<script type="module" src="src/js/index.js"></script>
</body>
</html

With the library added, the UMD build makes Redux Thunk available as a window.ReduxThunk.default global variable. I’m calling that global variable ReduxThunk, so I can simply call the library by ReduxThunk from now on.

//index.js
// The UMD build makes Redux-Thunk available as a window.ReduxThunk.default global variable
const ReduxThunk = window.ReduxThunk.default;

Creating the Redux store when using the Redux Thunk middleware

I still need to create the Redux store. The createStore() method takes as arguments:

//index.js
"use strict";

import getNextQuoteReducer from "../redux/reducers/reducers.js";
import handleAsync from "./js-modules/handleAsync.js";

// The UMD build makes Redux available as a window.Redux global variable
const Redux = window.Redux;

// The UMD build makes Redux-Thunk available as a window.ReduxThunk.default global variable
const ReduxThunk = window.ReduxThunk.default;

//to add Chrome's Redux DevTool's extension https://github.com/zalmoxisus/redux-devtools-extension that allows me to go back in the state history. When the extension is not installed, I'm using Redux’s compose.
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || Redux.compose;

//creating the Redux store, including Redux Thunk middleware. This is where the state lives.
export const store = Redux.createStore(
  getNextQuoteReducer, composeEnhancers(Redux.applyMiddleware(ReduxThunk))
);

//dispatching the asynch special action creator
store.dispatch(handleAsync());

With Redux Thunk middleware now added, action creators, receiving dispatch as an argument, can call dispatch asynchronously and take control by dispatching functions.

Designing for one batch endpoint call versus multiple endpoint calls

I now have the full JSON (of quotes) in the Redux Store. I however only need one (randomly selected) quote to render in the UI. I could design the app to do one endpoint call (fetch()) every time a new quote was needed, but that would mean using network resources every time a new quote was requested.

Asynchronous design of endpoint calls

Deciding whether to duplicate data in the Redux store

As the Redux store has the full JSON of quotes, I could use the store as a database and use Redux selectors (a selector is a function that accepts Redux state as an argument and returns data that is derived from that state) to do SELECT-type queries called by UI listeners (e.g. through click events). For this application this would mean using a Redux selector to randomly pick a quote from the store.

I’m duplicating data in the Redux store instead. In other words, I’m making the Redux store hold the currently randomly selected quote in addition to the full JSON of quotes. And the reason for that decision is: I want to allow any store listener to getState() and consistently get the current quote that has been randomly selected.

So, the Redux store stores:

Alternatively, I could store the index for the quote object in the Redux store. That would similarly allow me to getState() and arrive at the same quote object without fully duplicating the data.

Working through the user stories

Once again, back to the user stories: “I want to be welcomed by a quote when I first load the app” and “I want to be able to click a UI element (a button) to get a new quote”.

Like in flavor #4, I’m naming the UI elements to make them more readable throughout the code:

//index.js
//defining UI elements
const newQuoteButton = document.getElementById("new-quote");
const quoteTextContent = document.getElementById("text");
const quoteAuthorContent = document.getElementById("author");
export const tweetButton = document.getElementById("tweet-quote");

I’m now creating a store listener function which is called whenever an action is dispatched to the store. I then retrieve the state with the getState() method and render my UI elements with data from the state.

//index.js
//creating store listener function that is called whenever an action is dispatched to the store. The getState() method retrieves the current state held in the Redux store
store.subscribe(() => {
  const state = store.getState();
  if (state.status == "waiting") {
    console.log("Loading…");
  }
  if (state.status == "received") {
    let quoteObject = getRandomElementSelector(state);
    //inject random quote on HTML
    quoteTextContent.innerHTML = quoteObject.quoteText;
    //inject author on HTML
    quoteAuthorContent.innerHTML = "- " + quoteObject.quoteAuthor;
    //calling the JS module that generates a Twitter url for Twitter intent
    getTwitterUrl(quoteObject);
  }
});

The getRandomElementSelector above is a Redux selector. I’ve implemented it to take state as argument and return a randomly selected quote object.

//selectors.js
//defining a Redux selection, i.e. it takes Redux state as an argument and return some data from it
const getRandomElementSelector = state => {
  let array = state.quotes;
  //access random quote from array
  return array[Math.floor(Math.random() * array.length)];
};

export default getRandomElementSelector;

The getTwitterUrl called inside the store listener further above, handles truncating the quote text to the adequate tweetable number of characters, generates the url for the Tweet Quote button and sets the url on the HTML. Below code is as per flavor #4.

//getTwitterUrl.js
import { tweetButton } from "../index.js";

//defining function that generates a Twitter URL (for Twitter intent) and inject url on HTML, making it a JS function/module
const getTwitterUrl = quoteObject => {
  //truncating quote text in case full tweet gets to be over 280 characters
  let quoteTextElem = quoteObject.quoteText;
  let quoteAuthorElem = " - " + quoteObject.quoteAuthor;
  let contentQuote = quoteTextElem + quoteAuthorElem;
  if (contentQuote.length > 280) {
    let charCountAuthor = quoteAuthorElem.length;
    const extraStylingChar = "..." + '"';
    let extraCharCount = extraStylingChar.length;
    let subString =
      quoteTextElem.substring(0, 280 - extraCharCount - charCountAuthor) +
      extraStylingChar +
      quoteAuthorElem;
    //generate url available for Twitter intent and inject url on HTML
    tweetButton.href = "https://twitter.com/intent/tweet?text=" + subString;
  } else {
    //generate url available for Twitter intent and inject url on HTML
    tweetButton.href = "https://twitter.com/intent/tweet?text=" + contentQuote;
  }
};

export default getTwitterUrl;

With the code so far I’m shipping the “I want to be welcomed by a quote when I first load the app” user story, but “I want to be able to click a UI element (a button) to get a new quote” has not been implemented yet.

I’m starting with the UI listener function tied to the newQuoteButton. This function will be dispatching actions via the newQuoteActionCreator action creator to the Redux store when a click event happens. I’m implementing this new action creator to trigger the storing of the randomly chosen quote object in the Redux store as mentioned above.

//index.js
//creating UI listeners. Dispatching actions (via the action creators that return a "type" to the reducer) to the redux store. When a new state is set in the Redux store, the store listeners will be retrieving the current state held in the Redux store
newQuoteButton.addEventListener("click", () => {
  store.dispatch(newQuoteActionCreator());
});

So, when newQuoteActionCreator is called, it returns an action (which has an informative type). Inside the returned action/object, I’m naming payload the key that will be associated with the object that will store the selected random quote. The selected random quote, like previously, is chosen by getRandomElementSelector, the selector function.

This is actions.js in full, including newQuoteActionCreator:

//actions.js
import { NEW_QUOTE } from "../constants/constants.js";
import { REQUESTING_API_DATA } from "../constants/constants.js";
import { RECEIVED_API_DATA } from "../constants/constants.js";
import getRandomElementSelector from "../selectors/selectors.js";
import { store } from "../../js/index.js";

// defining action creators related to the asynch function. Action creator is a function that returns an action (object that contains information about an action-event that has occurred). The action creator gets called by `dispatch()`
export const requestingApiData = () => {
  return {
    type: REQUESTING_API_DATA
  };
};

export const receivedApiData = apiData => {
  return {
    type: RECEIVED_API_DATA,
    quotes: apiData
  };
};

//defining action creator related to the "Get New Quote" button. a function that returns an action (object that contains information about an action-event that has occurred). The action creator gets called by `dispatch()`
export const newQuoteActionCreator = () => {
  const state = store.getState();
  let quoteObject = getRandomElementSelector(state);
  return {
    type: NEW_QUOTE,
    payload: quoteObject
  };
};

The reducer then makes sense of the type and informs the Redux store how to respond to the action. In other words, the reducer tells the Redux store how to modify the state.

reducers.js now in full, including the NEW_QUOTE case:

//reducers.js
import { NEW_QUOTE } from "../constants/constants.js";
import { REQUESTING_API_DATA } from "../constants/constants.js";
import { RECEIVED_API_DATA } from "../constants/constants.js";

// defining default state
const defaultState = {
  status: "",
  quotes: []
};

//defining reducer functions to allow the Redux store to know how to respond to the action created
const getNextQuoteReducer = (state = defaultState, action) => {
  switch (action.type) {
    case REQUESTING_API_DATA:
      return {
        ...state,
        status: "waiting",
        quotes: []
      };
    case RECEIVED_API_DATA:
      return {
        ...state,
        status: "received",
        quotes: action.quotes
      };
    case NEW_QUOTE:
      return {
        ...state,
        status: "new quote",
        data: action.payload
      };
    default:
      return state;
  }
};

export default getNextQuoteReducer;

The new state object has a new property: data and this property has value equal to the object associated with payload (see newQuoteActionCreator).

I now need to adjust the store listener to render a new quote whenever a new randomly selected quote is added to the store. Here I’m adding an if statement to handle any changes to store when state.status is equal to new quote.

//creating store listener function that is called whenever an action is dispatched to the store. The getState() method retrieves the current state held in the Redux store
store.subscribe(() => {
  const state = store.getState();
  if (state.status == "waiting") {
    console.log("Loading…");
  }
  if (state.status == "received") {
    console.log(state);

    console.log(state.quotes);
    let quoteObject = getRandomElementSelector(state);
    //inject random quote on HTML
    quoteTextContent.innerHTML = quoteObject.quoteText;
    //inject author on HTML
    quoteAuthorContent.innerHTML = "- " + quoteObject.quoteAuthor;
    //calling the JS module that generates a Twitter url for Twitter intent
    getTwitterUrl(quoteObject);
  }
  if (state.status == "new quote") {
    //inject random quote on HTML
    quoteTextContent.innerHTML = state.data.quoteText;
    //inject author on HTML
    quoteAuthorContent.innerHTML = "- " + state.data.quoteAuthor;
    //calling the JS module that generates a Twitter url for Twitter intent
    getTwitterUrl(state.data);
  }
});

So now the random quote machine ships all the required user stories!

Action! What happens in the Redux store

As I use Redux DevTools Extension to time travel in the application’s state, I can check the state of the Redux store at every point in time. So, below is the state prior to finalizing the fetch() (i.e. status: "waiting" from reducers.js), the state when the store has the full JSON (i.e. status: "received") and the state when it additionally stores a new object called data that holds the current randomly selected quote (i.e. after the “Get New Quote” button is clicked).

What happens in the Redux store gif

What happens in the store

Redux + Redux Thunk application structure

Like I did in flavor #4, as I’ve been jumping across a few JS modules in the write-up above, it can be helpful to zoom out and give a high-level view of the application’s file structure. Since this is a very simple app, I’m distributing actions, constants, selectors and reducers in different folders as opposed to following a more modular approach (bringing them together under a single folder related to every specific feature. Not following this approach since there aren’t many “features” in this application):

.
├── index.html
└── src
    ├── js
    │   ├── index.js
    │   └── js-modules
    │       ├── getTwitterUrl.js
    │       └── handleAsync.js
    ├── redux
    │   ├── actions
    │   │   └── actions.js
    │   ├── constants
    │   │   └── constants.js
    │   ├── reducers
    │   │   └── reducers.js
    │   └── selectors
    │       └── selectors.js
    └── stylesheets
        └── style.css

A visual look into the application’s execution flow

Despite this flavor using a middleware and working with asynchronicity, the code execution flow is not that much different to flavor #4. I’m changing the state in the Redux store with random quotes that get selected as the application runs (additionally, in this flavor, I’m also storing the contents of a full JSON file in the Redux store).

Yes, Redux updates the store with help from an action creator and a reducer (and in this flavor a third-party extension point (Redux Thunk middleware) is between dispatching an action and handing the action off to the reducer), but on high-level, this is how data flows in the application: Different UI elements, such as the Get New Quote button, trigger state changes in the Redux store (with a new random quote) and a store listener function next updates the UI with the new state.

And in a diagram:

Redux code execution with Redux Thunk

You can check the project live. Source code for flavor #5 in Github.

Live project Source code

In Part 6 (to be posted next week), I will cover a sixth flavor and will be using a User Interface JavaScript library (React) to build the app’s UI. If you’re curious about the next flavors, you should join Morse Wall’s newsletter. I think you’ll like it a lot!


So, what did you like about this post? Leave a comment or come say hello on Twitter!

Also, if you have built a random quote machine after reading this post, share it below as a comment. Really excited to see what you create!

Happy coding!

Enjoyed this content?

Help keep it coming by sending a donation or buying me coffees.
You can also join Morse Wall's newsletter, or subscribe to various site feeds to get notified of new posts or follow Morse Wall on social media.

Leave a comment

by Pat
by Pat