Building a random quote machine with React Hooks, Redux and Redux Thunk


This is part 11 (the last part!) 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 #10, I’m using global state management (with Redux) to help me update the different components with the current application state using React to build the UI.

In this flavor, I continue using Redux to help me update the different components 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)) (like in flavor #5). This time, however, I’m using React to build the UI (not manually building the UI with HTML, like previously in flavor #5).

These are the different flavors that are part of this series:

  1. HTML + CSS + Vanilla JS + quotes array
  2. HTML + CSS + Vanilla JS + JSON with quotes (members-only post)
  3. HTML + CSS + Vanilla JS + quotes REST API
  4. HTML + Vanilla JS + SAAS + quotes array (nah, because let’s be opinionated about SASS)
  5. HTML + Vanilla JS + Bootstrap + quotes array (nah, because let’s be opinionated about Bootstrap)
  6. HTML + CSS + JQuery + JSON with quotes (nah, because let’s be opinionated about JQuery)
  7. HTML + CSS + Redux + quotes array
  8. HTML + CSS + Redux + JQuery + JSON with quotes (nah, because let’s be opinionated about JQuery)
  9. HTML + CSS + Redux + Redux Thunk + JSON with quotes
  10. HTML + CSS + React + quotes array
  11. HTML + CSS + React + JSON with quotes (members-only post)
  12. HTML + CSS + React Hooks + quotes array
  13. HTML + CSS + React Hooks + JSON with quotes (members-only post)
  14. HTML + CSS + React + React Redux + quotes array (nah, because let’s be bullish about writing any new components with React Hooks)
  15. HTML + CSS + React + React Redux + Redux Thunk + JSON with quotes (nah, because let’s be bullish about writing any new components with React Hooks)
  16. HTML + CSS + React Hooks + React Redux + quotes array
  17. HTML + CSS + React Hooks + React Redux + Redux Thunk + JSON with quotes (this flavor)
  18. HTML + CSS + React Hooks + React Redux Hooks + quotes array (nah, because let’s be opinionated about React Redux Hooks)
  19. HTML + CSS + React Hooks + React Redux Hooks + Redux Thunk + JSON with quote (nah, because let’s be opinionated about React Redux Hooks)

If you are new here, you may wonder why I am overcomplicating a really simple application that can be shipped in 3 lines of functional Vanilla JavaScript. And the reason is: Shipping a simple app in a simple environment with fewer moving parts = great way to practice a new way to solve a problem. This is the spirit behind the Making a random quote machine in a few different flavors series. Enjoy!


If you’re curious about what comes up next in Morse Wall and would like to make a writer very happy today, you should subscribe to Morse Wall.


Flavor #11: HTML + CSS + React Hooks + React Redux + Redux Thunk + JSON with quotes

Much like in flavor #10, I’m creating a Redux store that manages the state of the entire app. The React components can then subscribe to the pieces of data in the store that are relevant to them. Actions are dispatched directly from React components. These dispatched actions then ultimately trigger store updates (for more on state, check “What is state?” in flavor #4).

Much of the Redux and Redux Thunk code in this flavor remains the same as in flavor #5, so it may be helpful to give that writing a quick read. Similarly, reading flavor #10 may be helpful, given most of the React components used in this flavor have been detailed there. Also, much of the React Redux code here remains the same as in flavor #10.

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

Calling asynchronous endpoints in Redux

I am using the Redux Thunk middleware between the actions being dispatched and the actions reaching the reducer (for more on actions and reducers, check flavor #4).

Redux Thunk helps me handle the challenge in that, by default, actions in Redux are dispatched synchronously. For more on Redux Thunk, check flavor #5 - Calling asynchronous endpoints in Redux as well as flavor #5 - Adding the Redux Thunk library and flavor #5 - Creating the Redux store when using the Redux Thunk middleware .

The design of doing one batch endpoint call versus multiple endpoint calls is detailed in flavor #5 - Designing for one batch endpoint call versus multiple endpoint calls. The same thinking applies to this flavor.

Working through the user stories


From the user stories, I want to be welcomed by a quote when I first load the app. In other words, I want to be able to read the quote from the Redux store on first “content” page load. For the first user story, I’ll be calling an asynchronous function (handleAsync) through the useEffect Hook in my React component.

By using useEffect, I’m effectively telling React that the component needs to do this “effect” after the browser has painted. React will call the function passed to useEffect later, after performing the DOM updates. For more on useEffect, check flavor #9 - Controlling asynchronous code in React with Hooks.

Then, I need to ship user story #2: I want to be able to click a UI element (a button) to get a new quote. To achieve this, I’ll be dispatching another action from my React component with the help of chosenRandomQuoteToState (defined in my React component further down).

Redux and initial state - Show me the code already!

Defining actions, action creators and the reducer in full:

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

// 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()`
const requestingApiData = () => {
  return {
    type: REQUESTING_API_DATA,
  };
};

const receivedApiData = (apiData) => {
  return {
    type: RECEIVED_API_DATA,
    payloadQuotes: apiData,
  };
};

//defining an action type
const NEW_QUOTE = "NEW_QUOTE";

//defining an action creator, which creates the action to select a new quote.  The action creator is a function that returns an action (object that contains information about an action-event that has occurred). It creates the action to add a new quote.
const newQuoteActionCreator = (chosenQuoteInput) => {
  return {
    type: NEW_QUOTE,
    payloadQuote: chosenQuoteInput,
  };
};

// defining initial state
const initialState = {
  status: "",
  quotesData: [],
};

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

Disclaimer: Use of switch...case in reducers

switch...case statements are great to show how the internals work but aren't typically used in production. You can use any conditional logic you want to write a reducer.

Pretty much all code in the snippet above follows the Redux logic implemented in part 5 of this series - A new action creator. So, check that writing out for a deeper dive down.

With the above in place, I can define the asynchronous function that allows me to create an asynchronous action (hit an asynchronous endpoint). For this, 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.

//script.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/[email protected]/db.json"
      );
      const responseObject = await responseJSON.json();
      const quotesArray = responseObject.quotes;
      // dispatch received data action here
      store.dispatch(receivedApiData(quotesArray));
      let initialQuote =
        quotesArray[Math.floor(Math.random() * quotesArray.length)];
      store.dispatch(newQuoteActionCreator(initialQuote));
    };
    makeRequest();
  };
};

Nearly all the code in the above snippet is covered in flavor #5 - Calling asynchronous endpoints in Redux. The difference here is that I’ve decided to follow a slightly different approach and dispatch a new random quote to the Redux store as soon as I’ve fetched the quotes from JSON file (previously, in flavor #5, I’ve used a Redux selector that would take state as an argument and return a randomly selected quote object).

As handleAsync is dispatched (more on dispatching handleAsync, with the help of useEffect, further down), an update of the state in the Redux store is triggered with the help of the reducer. The Redux store will then keep the full JSON contents stored in the state as well as a first randomly selected quote.

State holding quotes status currentQuote

No JavaScript modules here (again)

For the reasons I’ve detailed in flavor #6, I’m not dividing this flavor’s code into modules. This can be noted when comparing this flavor’s code with flavor #5’s source code, which is modularized.

A visual look into the application’s execution flow

In summary, I’m changing the state in the Redux store with the contents of a full JSON file and random quotes that get selected as the application runs. Redux updates the store with help from action creators, the Redux Thunk middleware, a reducer and the React Redux API.

On high-level, data flows in a similar way to part 5 of this series - A visual look into the application’s execution flow : Different React components, such as the Get New Quote button, trigger state changes in the Redux store (with a new random quote) and a React component next updates the UI with the new state.

React Redux Redux Thunk Execution Flow

React Redux - specifying state and actions to React

By now I have the Redux code in place but still need to provide React (React code further down) access to the Redux store and the actions it needs to dispatch updates. I’m getting this done with the React Redux package. For more on React Redux, including the code for <Provider /> and connect(), check flavor #10 - React Redux API.

Disclaimer: React-Redux Hooks API and connect()

You may have noticed that flavors using React-Redux hooks have been crossed out from the list of flavors covered in this 11-part series. This is mainly due to me (at time of writing), being opinionated about React-Redux hooks since code becomes harder to test given the tighter coupling between the React component and Redux.

(Update 2021: The recommendation is now that using the React-Redux hooks API should be the default approach in your React components).

The existing connect API, however, still works and will continue to be supported.

(Update 2021 II: The intention with this series is to get very incremental progress from one stack to another. Making it all as much bare bones as possible in order to get concepts adequately explored and explained. For this reason, I've covered the Redux core library (unopinionated - let's you decide how to handle everything) in the flavors covered in this series. That said, Redux Toolkit is now intended to be the standard way to write Redux logic and the recommendation is for it to be used).


mapStateToProps

//script.js
//mapping state to props. Allows me to specify exactly what pieces of the state should the React component have access to. Taking state as argument, it returns an object which maps that state to specific property names. These properties will become accessible to the React component via props
const mapStateToProps = (state) => {
  return {
    currentQuote: state.data,
    stateQuotes: state.quotesData,
    stateStatus: state.status,
  };
};

mapStateToProps is called every time the store state changes. It maps state to specific property names (in this case currentQuote for data - check the reducer above, stateQuotes for quotesData and stateStatus for status). These properties are then accessible to my React component via props (with the help of connect() - check flavor #10 - React Redux API for more on it).

mapDispatchToProps

mapDispatchToProps receives dispatch and returns an object that maps dispatch actions to specific property names (in this case selectNewQuote, which I will be calling inside my React component further down and handleAsyncFx, which I’m calling with useEffect in the React component).

These properties are then accessible to my React component via props (through connect()). Each property returns a function that calls dispatch with an action creator (in this casenewQuoteActionCreator and handleAsync - check action creators above) and any relevant action data (a randomly selected quote in the case of newQuoteActionCreator).

//script.js
//mapping dispatch to props. Specifying what actions should the React component have access to. Allows me to specify which action creators I need to be able to dispatch. It is used to provide specific action creators to the React components so they can dispatch actions against the Redux store. It returns an object that maps dispatch actions to property names, which become component props. As opposed to mapStateToProps (that returns a piece of state), here each property returns a function that calls dispatch with an action creator and any relevant action data. I have access to this dispatch because it's passed in to mapDispatchToProps() as a parameter when I define the function, just like I've passed state to mapStateToProps(). The object should have a property selectNewQuote set to the dispatch function, which takes a parameter for the new quote to add when it dispatches newQuoteActionCreator(). It should also have a property handleAsyncFX set to a dispatch function that dispatches handleAsync().
const mapDispatchToProps = (dispatch) => {
  return {
    selectNewQuote: function (quoteToBeNewQuote) {
      dispatch(newQuoteActionCreator(quoteToBeNewQuote));
    },
    handleAsyncFX: function () {
      dispatch(handleAsync());
    },
  };
};

The loading component

Since I’m hitting an asynchronous endpoint in this flavor, I’m adjusting my code to make sure that the application will only try to render the quote when I have the data available.

As part of this “code adjustment”, I’m creating a React component (a loading spinner) that renders before the quotes data is available to be rendered, i.e. before I’ve finished fetching the data from the JSON file.

// script.js
//defining a component for the loading spinner
const Loading = () => {
  return (
    <div className="loading-container">
      <div className="loader-dzg" />
    </div>
  );
};

React Dispatching Actions

I now have all the Redux, Redux Thunk and React Redux bits in place. The bit missing is to define the React function component that will build the UI.

The async/await function (props.handleAsyncFX) passed to useEffect will run after the render is committed to the screen. This is great since I don’t want to stop the App component from mounting. For more on useEffect, check flavor #9 - Controlling asynchronous code in React with Hooks.

I want the component to go ahead, render the Loading component and only re-render when fetch() has finished fetching the requested resource from the network (data from the JSON file).

By default, effects run after every completed render and since I’m setting the state after every data fetch, the component would update and the effect would run again. It would fetch the data again and again. To avoid this, and only fetch data when the component mounts, I provide an empty array ([]) as second argument to useEffect to avoid activating it on component updates. This tells React that the effect doesn’t depend on any values from props or state, so it never needs to re-run.

Disclaimer: React Suspense

A new way to fetch data is in the works with React Suspense and in the longer term, the React core team will recommend using Suspense for data fetching (Update 2021: Suspense for Data Fetching is available on an experimental release).

//script.js
// defining the function component. Redux state and dispatch are passed to the component as props
const App = (props) => {
  React.useEffect(() => {
    props.handleAsyncFX();
  }, []);

  const random = (array) => {
    return Math.floor(Math.random() * array.length);
  };

  const randomQuoteFunction = (array) => {
    return array[random(array)];
  };

  //defining a function to (ultimately) update the Redux state with a new quote. Passing a randomly selected quote via props. Dispatching selectNewQuote() from props and passing in the randomly selected new quote as an argument
  const chosenRandomQuoteToState = () => {
    //selecting a random quote from the array
    let chosenQuote = randomQuoteFunction(props.stateQuotes);
    props.selectNewQuote(chosenQuote);
  };

  //the component returns JSX, and as per code snippet below, JSX clearly represents HTML, composing the UI.
  //as a React component can only return one single element, I’m using <React.Fragment> to add a parent tag to my JSX elements without adding an extra node to the DOM.
  return (
    <React.Fragment>
      {(() => {
        switch (props.stateStatus) {
          case "waiting":
            return <Loading />;
          case "received":
            return <Loading />;
          case "new quote":
            return (
              <React.Fragment>
                <div className="container">
                  <div id="quote-box">
                    {/* //passing data via props to QuoteBox component */}
                    <QuoteBox
                      text={props.currentQuote.quoteText}
                      author={props.currentQuote.quoteAuthor}
                    />
                    <div className="actions">
                      <Button
                        id="new-quote"
                        title="Get New Quote"
                        onClick={chosenRandomQuoteToState}
                      />
                      <TwitterShare link={twitterLink} />
                    </div>
                  </div>
                </div>
                <footer>
                  <ul className="footer-options">
                    <li className="footer-link">
                      <a href="#" className="footer-linktext">
                        Legal
                      </a>
                    </li>
                    <li className="footer-link">
                      <a href="#" className="footer-linktext">
                        Contact Us
                      </a>
                    </li>
                  </ul>
                  <span>
                    © 2019 Developed by Pat Eskinasy. All Rights Reserved.
                  </span>
                </footer>
              </React.Fragment>
            );
        }
      })()}
    </React.Fragment>
  );
};

App reads state from the Redux store (given Redux is passing state to App via props) and the newly selected random quote will render on the screen (for more on passing props, check flavor #10 - Passing props to components).

Then, when the Get New Quote button is clicked, React calls chosenRandomQuoteToState, which calls selectNewQuote (see mapDispatchToProps above), as it has been passed as props to App. Now, provided the specific action creator (newQuoteActionCreator, coming from mapDispatchToProps), the React component can dispatch actions against the Redux store and update the state with a new quote.

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.

Illustrated below in the extension is the data object (holding the current randomly selected quote).

What happens in the Redux store


Making the machine tweet and voilà

Defining App, the function component, now in full:

//script.js
// defining the function component. Redux state and dispatch are passed to the component as props
const App = (props) => {
  React.useEffect(() => {
    props.handleAsyncFX();
  }, []);

  //making the machine tweet
  let twitterLink;

  if (props.currentQuote) {
    let quoteTextElem = props.currentQuote.quoteText;
    let quoteAuthorElem = " - " + props.currentQuote.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
      twitterLink = "https://twitter.com/intent/tweet?text=" + subString;
    } else {
      //generate url available for Twitter intent and inject url on HTML
      twitterLink = "https://twitter.com/intent/tweet?text=" + contentQuote;
    }
  }

  const random = (array) => {
    return Math.floor(Math.random() * array.length);
  };

  const randomQuoteFunction = (array) => {
    return array[random(array)];
  };

  //defining a function to (ultimately) update the Redux state with a new quote. Passing a randomly selected quote via props. Dispatching selectNewQuote() from props and passing in the randomly selected new quote as an argument
  const chosenRandomQuoteToState = () => {
    //selecting a random quote from the array
    let chosenQuote = randomQuoteFunction(props.stateQuotes);
    props.selectNewQuote(chosenQuote);
  };

  //the component returns JSX, and as per code snippet below, JSX clearly represents HTML, composing the UI.
  //as a React component can only return one single element, I’m using <React.Fragment> to add a parent tag to my JSX elements without adding an extra node to the DOM.
  return (
    <React.Fragment>
      {(() => {
        switch (props.stateStatus) {
          case "waiting":
            return <Loading />;
          case "received":
            return <Loading />;
          case "new quote":
            return (
              <React.Fragment>
                <div className="container">
                  <div id="quote-box">
                    {/* //passing data via props to QuoteBox component */}
                    <QuoteBox
                      text={props.currentQuote.quoteText}
                      author={props.currentQuote.quoteAuthor}
                    />
                    <div className="actions">
                      <Button
                        id="new-quote"
                        title="Get New Quote"
                        onClick={chosenRandomQuoteToState}
                      />
                      <TwitterShare link={twitterLink} />
                    </div>
                  </div>
                </div>
                <footer>
                  <ul className="footer-options">
                    <li className="footer-link">
                      <a href="#" className="footer-linktext">
                        Legal
                      </a>
                    </li>
                    <li className="footer-link">
                      <a href="#" className="footer-linktext">
                        Contact Us
                      </a>
                    </li>
                  </ul>
                  <span>
                    © 2019 Developed by Pat Eskinasy. All Rights Reserved.
                  </span>
                </footer>
              </React.Fragment>
            );
        }
      })()}
    </React.Fragment>
  );
};

The code to make the machine tweet (in snippet above inside App) is very much in line with what has been implemented in all previous flavors of this project. In other words, the code here follows the logic implemented in part 1 of this series.

The conditional check for props.currentQuote helps me handle the readiness of the application. This is because I can only access props.currentQuote.quoteText or props.currentQuote.quoteAuthor when props.currentQuote exists (i.e. I can only access properties of the randomly selected quote object when the object has been selected, not before that).

Done. The last random quote machine is shipped. Source code for flavor #11 in Github.

It was fun making this whole series!


Acknowledgement

The following goes without saying, but here it comes anyways: Please note that in this ever changing world of technology, I am writing this article in July 2019 (hello, summer!) and alternate solutions might have become available as you read this writing.


This was part 11 (the last part!) 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.

If you’re curious about what is next with Morse Wall, you should subscribe to Morse Wall. I think you’ll like it a lot!


So, what did you like about this post? Come say hello on Twitter!

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

Happy coding!


Where to find me:

Follow me on Twitter:

Follow Pat Eskinasy on Twitter

You can always get my latest writings directly to your email address by subscribing to Morse Wall.