Chapter 6. React State Management

Data is what makes our React components come to life. The user interface for recipes that we built in the last chapter is useless without the array of recipes. It’s the recipes and the ingredients along with clear instructions that would make such an app worth while. Our user interfaces are tools that creators will use to generate content. In order to build the best tools possible for our content creators, we will need to know how to effectively manipulate and change data.

In the last chapter, we constructed a component tree. A hierarchy of components that data was able to flow through as properties. Properties are half of the picture. State is the other half. The state of a React application is driven by data that has the ability to change. Introducing state to the recipe application could make it possible for chefs create new recipes, modify existing recipes, and remove old ones.

State and properties have a relationship with one another. When we work with React applications, we gracefully compose components that are tied together based upon this relationship. When the state of a component tree changes, so do the properties. The new data flows through the tree causing specific leaves and branches to re-render to reflect the new content.

In this chapter, we are going to bring applications to life by introducing state. We will learn to create stateful function components. We will learn how state can be sent down a component tree and user interactions back up the component tree. We will learn techniques for collecting form data from our users. And, we will take a look at the various ways in which we can separate concerns within our application by introducing stateful context providers.

Building a Star Rating Component

We would all be eating terrible food and watching terrible movies without the five star rating system. If we plan on letting users drive the content on our website, we will need a way to know if that content is any good or not. That makes the StarRating component one of the most important React components that you will ever build:

3 selected stars
Figure 6-1. Star Rating Component

The StarRating component will allow users to rate content based on a specific number of stars. Content that is no good gets one star. Highly-recommended content gets 5 stars. Users can set the rating for specific content by clicking on a specific star. First, we’ll need a star and we can get one from react-icons:

npm i react-icons

react-icons is a npm library that contains hundreds of svg icons that are distributed as React components. By installing it, we just installed several popular icon libraries that contain hundreds of common SVG icons. You can browse all of the icons at https://react-icons.netlify.com. We are going to use the star icon from the font awesome collection.

import React from "react";
import { FaStar } from "react-icons/fa";

export default function StarRating() {
  return [
    <FaStar color="red" />,
    <FaStar color="red" />,
    <FaStar color="red" />,
    <FaStar color="grey" />,
    <FaStar color="grey" />
  ];
}

Here we have created a StarRating component that renders five SVG stars that we have imported from react-icons. The first three stars are filled in with red, and the last two are grey. We render the stars first because seeing them gives us a roadmap for what we’ll have to build. A selected star should be filled in with red, and a star that is not selected should be greyed out. Let’s create a component that automatically files the stars based upon the selected property:

const Star = ({ selected = false }) => (
  <FaStar color={selected ? "red" : "grey"} />
);

The Star component renders an individual star and uses the selected property to fill it with the appropriate color. If the selected property is not passed to this component, we will assume that the star should not be selected and by default will be filled in with grey.

The five star rating system is pretty popular, but a ten star rating system is far more detailed. We should allow developers to select the total number of stars that they wish to use when they add this component to their app. This can be accomplished by adding a totalStars property to the StarRating component:

const createArray = length => [...Array(length)];

export default function StarRating({ totalStars = 5 }) {
  return createArray(totalStars).map((n, i) => <Star key={i} />);
}

Here we added the createArray function from Chapter 2. All we have to do is supply the length of the array that we want to create and we get a new array at that length. We use this function with the totalStars property to create an array of a specific length. Once we have an array, we can map over it and render Star components. By default, totalStars is equal to 5 which means this component will render 5 grey stars:

5 star
Figure 6-2. Five stars are displayed

The useState Hook

It’s time to make the StarRating component clickable which will allow our users to change the rating. Since the rating is a value that will change, we will store and change that value using React state. We incorporate state into a function component using a React feature called hooks. A hook contains reusable code logic that is separate from the component tree. They allow us to hook up functionality to our components. React ships with several built in hooks that we can use out of the box. In this case we want to add state to our React component, so the first hook that we will work with is React’s useState hook. This hook is already available in the react package, we simply need to import it:

import React, { useState } from "react";
import { FaStar } from "react-icons/fa";

The stars that the user has selected represents the rating. We will create a state variable called selectedStars which will hold the user’s rating. We will create this variable by adding the useState hook directly to the StarRating component:

export default function StarRating({ totalStars = 5 }) {
  const [selectedStars] = useState(3);
  return (
    <>
      {createArray(totalStars).map((n, i) => (
        <Star key={i} selected={selectedStars > i} />
      ))}
      <p>
        {selectedStars} of {totalStars} stars
      </p>
    </>
  );
}

We just hooked this component up with state. The useState hook is a function that we can invoke to return an array. The first value of that array is the state variable that we want to use. In this case that variable is selectedStars or the number of stars that the StarRating will color red. useState returns an array. We can take advantage of array destructuring which allows us to name our state variable whatever we like. The value that we send to the useState function is the default value for the state variable. In this case, selectedStars will initially be set to 3:

5 star
Figure 6-3. Three of Five stars are selected

In order to collect a different rating from the user, we will need to allow them to click on any of our stars. This means that we will need to make the stars clickable by adding an onClick handler to the FaStar component:

const Star = ({ selected = false, onSelect = f => f }) => (
  <FaStar color={selected ? "red" : "grey"} onClick={onSelect} />
);

Here we modified the star to contain an onSelect property. Check it out: this property is a function. When a user clicks on the FaStar component, we will invoke this function which can notify its parent that a star has been clicked. The default value for this function is f => f. This is simply a fake function that does nothing. It just returns whatever argument was sent to it. However, if we do not set a default function, and the onSelect property is not defined, an error will occur when we click the FaStar component because the value for onSelect must be a function. Even though f => f does nothing, it is a function, which means it can be invoked without causing errors. If an onSelect property is not defined, no problem. React will simply invoke the fake function and nothing will happen.

Now that our Star component is clickable, we will use it to change the state of the StarRating:

export default function StarRating({ totalStars = 5 }) {
  const [selectedStars, setSelectedStars] = useState(0);
  return (
    <>
      {createArray(totalStars).map((n, i) => (
        <Star
          key={i}
          selected={selectedStars > i}
          onSelect={() => setSelectedStars(i + 1)}
        />
      ))}
      <p>
        {selectedStars} of {totalStars} stars
      </p>
    </>
  );
}

In order to change the state of the StarRating component, we’ll need a function that can modify the value of selectedStars. The second item in the array that is returned by the useState hook is a function that can be used to change the state value. Again, by destructuring this array, we can name that function whatever we like. In this case, we are calling it setSelectedStars, because that is what it does, it sets the value of selectedStars.

The most important thing to remember about hooks is that they can cause the component that they are hooked into to re-render. Every time we invoke setSelectedStars function to change the value of selectedStars the StarRating function component will be re-invoked by the hook, it will render again. This time with a new value for selectedStars. This is why hooks are such a killer feature. When data within the hook changes, they have the power to re-render the component that they are hooked into with new data.

The StarRating component will be re-rendered every time a user clicks a Star. When the user clicks the Star, the onSelect property of that star is invoked. When the onSelect property is invoked, we’ll invoke the setSelectedStars function and send it the number of the star that was just selected. We can use the i variable from the map function to help us calculate that number. When the map function renders the first Star, the value for i is 0. This means that we need to add 1 to this value to get the correct number of stars. When setSelectedStars is invoked, the StarRating component is invoked with a the value for selectedStars.

dev tools 2 star
Figure 6-4. Hooks in React developer tools

The React developer tools will show you which hooks are incorporated with specific components. When we render the StarRating component in the browser we can view debugging information about that component by selecting it in the developer tools. In the colum on the right, we can see that the StarRating component incorporates a state hook that has a value of 2. As we interact with the app we can watch the state value change and the component tree re-render with the corresponding number of stars selected.

React State the “Old Way”

In previous versions of React, before v16.8.0, the only way to add state to a component was to use a class component. This required not only a lot of syntax, but it also made it more difficult to reuse functionality across components. Hooks were designed to solve problems presented with class components by providing a solution to incorporate functionality into function components.

The following is the code is a class component. This was the original StarRating Component that was printed in the first edition of this book.

import React, { Component } from "react";

export default class StarRating extends Component {
  constructor(props) {
    super(props);
    this.state = {
      starsSelected: 0
    };
    this.change = this.change.bind(this);
  }

  change(starsSelected) {
    this.setState({ starsSelected });
  }

  render() {
    const { totalStars } = this.props;
    const { starsSelected } = this.state;
    return (
      <div>
        {[...Array(totalStars)].map((n, i) => (
          <Star
            key={i}
            selected={i < starsSelected}
            onClick={() => this.change(i + 1)}
          />
        ))}
        <p>
          {starsSelected} of {totalStars} stars
        </p>
      </div>
    );
  }
}

This class component does the same thing as our function component with noticeably more code. Additionally, it introduces more confusion thorough the use of the this keyword and function binding.

As of today, this code still works. We are no longer covering class components in this book because they are no longer needed. Function components and hooks are the future of React, and we are not looking back. There could come a day where class components are officially deprecated, and this code will no longer be supported.

State in Component Trees

It is not a great idea to use state in every single component. Having state data distributed throughout too many of your components will make it harder to track down bugs and make changes within your application. This occurs because it is hard to keep track of where the state values live within your component tree. It is easier to understand your application’s state, or state for a specific feature, if we were to manage it from one location. There are several approaches to this methodology and the first one we will analyze is storing state at the root of your component tree and passing it down to child components via props.

Let’s build a small application that can be used to save a list of colors. We’ll call the app the Color Organizer, and it will allow users to associate a list of colors with a custom title and rating. To get started a sample dataset may look like the following:

[
  {
    "id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
    "title": "ocean at dusk",
    "color": "#00c4e2",
    "rating": 5
  },
  {
    "id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
    "title": "lawn",
    "color": "#26ac56",
    "rating": 3
  },
  {
    "id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
    "title": "bright red",
    "color": "#ff0000",
    "rating": 0
  }
]

The color-data.json file contains an array of three colors. Each color has an id, title, color, and rating. First, we will create a UI consisting of React components that will be used to display this data in a browser. Then we will allow the users to add new colors as well as rate and remove colors from the list.

Sending State Down a Component Tree

In this iteration, we will store state in the root of the Color Organizer, the App component, and pass the colors down to child components to handle the rendering. The App component will be the only component within our application that holds state. We will add the list of colors to the App with the useState hook.

import React, { useState } from "react";
import colorData from "./color-data.json";
import ColorList from "./ColorList.js";

export default function App() {
  const [colors] = useState(colorData);
  return <ColorList colors={colors} />;
}

The App component sits at the root of our tree. Adding useState to this component hooks it up with state management for colors. In this example, the colorData is the array of sample colors from above. The App component uses the colorData as the initial state for colors. From there, the colors are passed down to a component called the ColorList:

import React from "react";
import Color from "./Color";

export default function ColorList({ colors = [] }) {
  return (
    <div>
      {colors.length === 0 ? (
        <p>No Colors Listed. (Add a Color)</p>
      ) : (
        colors.map(color => <Color key={color.id} {...color} />)
      )}
    </div>
  );
}

The ColorList receives the colors from the App component as props. If the list is empty, this component will display a message to our users. When we have a color array we can map over it and pass the details about each color further down the tree to the Color component:

export default function Color({ title, color, rating }) {
  return (
    <section>
      <h1>{title}</h1>
      <div style={{ height: 50, backgroundColor: color }} />
      <StarRating selectedStars={rating} />
    </section>
  );
}

The Color component expects three properties: title, color, and rating. These values are found in each color object and were passed to this component using the spread operator <Color {...color} />. This takes each field from the color object and passes it to the Color component as a property with the same name as the object key. The Color component displays these values. The title is rendered inside of a heading one element. The color value is displayed as the backgroundColor for a div element. The rating is passed further down the tree to the StarRating component which will display the rating visually as selected stars:

export default function StarRating({ totalStars = 5, selectedStars = 0 }) {
  return (
    <>
      {createArray(totalStars).map((n, i) => (
        <Star
          key={i}
          selected={selectedStars > i}
          onSelect={() => onRate(i + 1)}
        />
      ))}
      <p>
        {selectedStars} of {totalStars} stars
      </p>
    </>
  );
}

This StarRating component has been modified. We’ve turned it into a Pure Component. A Pure Component is a function component that does not contain state and will render the same user interface given the same props. We made this component a pure component because the state for color ratings are stored in the colors array at the root of the component tree. Remember the goal of this iteration is to store state in a single location and not have it distributed through many different components within the tree.

Note

It is possible for the StarRating component to hold it’s own state and receive state from a parent component via props. This is typically necessary when distributing components for wider use by the community. We demonstrate this technique in the next chapter when we cover the useEffect hook.

At this point we have finished passing state down the component tree from the App component all the way to each Star component that is filled red to visually represent the rating for each color. If we render the app based on the color-data.json that was listed above we should see our colors in the browser:

color organizer
Figure 6-5. Color Organizer rendered in the browser

Sending Interactions back up a Component Tree

So far, we’ve rendered a representation of the colors array as UI by composing React components and passing data down the tree from parent component to child component via props. What happens if we want to remove a color from the list or change the rating of a color in our list? The colors are stored in state at the root of our tree. We’ll need to collect interactions from child components and send them back up the tree to the root component where we can change the state.

For instance, let’s say we wanted to add a delete button next to each color’s title that would allow users to remove colors from state. We would add that button to the Color component:

import { FaTrash } from "react-icons/fa";

export default function Color({ id, title, color, rating, onRemove = f => f }) {
  return (
    <section>
      <h1>{title}</h1>
      <button onClick={() => removeColor(id)}>
        <FaTrash color="red" />
      </button>
      <div style={{ height: 50, backgroundColor: color }} />
      <StarRating selectedStars={rating} />
    </section>
  );
}

Here we have modified the color by adding a button that will allow users to remove colors. First, we imported a trash can icon from react-icons. Next we wrapped the FaTrash icon in a button. Adding an onClick handler to this button allows us to invoke the onRemove function property which has been added to our list of properties along with the id. When a user clicks the remove button, we will invoke removeColor and pass it the id of the color that we want to remove. That is why the id value has also been gathered from the Color component’s properties.

This solution is great because we keep the Color component pure. It doesn’t have state, and can easily be reused in a different part of the app or another application all together. The Color component is not concerned with what happens when a user clicks the remove button. All it cares about is notifying the parent that this event has occurred and passing the information about which color the user wishes to remove. It is now the parent’s responsibility to handle this event:

export default function ColorList({ colors = [], onRemoveColor = f => f }) {
  return (
    <div className="color-list">
      {colors.length === 0 ? (
        <p>No Colors Listed. (Add a Color)</p>
      ) : (
        colors.map(color => (
          <Color key={color.id} {...color} onRemove={onRemoveColor} />
        ))
      )}
    </div>
  );
}

The Color component’s parent is the ColorList. This component also doesn’t have access to state. Instead of removing the color, it simply passes the event up to it’s parent. It accomplishes this by adding an onRemoveColor function property. If a Color component invokes the onRemove property, the ColorList will in turn invoke it’s onRemoveColor property and send the responsibility for removing the color up to it’s parent. The color’s id is still being passed to the onRemoveColor function.

The parent of the ColorList is the App. This component is the component that has been hooked up with state. This is where we can capture the color id and remove the color in state:

export default function App() {
  const [colors, setColors] = useState(colorData);
  return (
    <ColorList
      colors={colors}
      onRemoveColor={id => {
        const newColors = colors.filter(color => color.id !== id);
        setColors(newColors);
      }}
    />
  );
}

First, we’ve added a variable for setColors. Remember the second argument in the array returned by useState is a function that we can use to modify the state. When the ColorList raises an onRemoveColor event we capture the id of the color to remove from the arguments and use it to filter the list of colors to exclude the color that the user wants to remove. Next, we change the state. We use the setColors function to change change the array of colors to the newly filtered array.

Changing the state of the colors array causes the App component to be re-rendered with the new list of colors. Those new colors are passed to the ColorList component which is also re-rendered. It will render Color components for the remaining colors and our UI will reflect the changes that we have made, it will render one less color.

If we want to rate the colors that are stored in the App components state we’ll have to repeat the process with an onRate event. First we’ll have to collect the new rating from the individual star that was clicked and pass that value to the parent of the StarRating:

export default function StarRating({
  totalStars = 5,
  selectedStars = 0,
  onRate = f => f
}) {
  return (
    <>
      {createArray(totalStars).map((n, i) => (
        <Star
          key={i}
          selected={selectedStars > i}
          onSelect={() => onRate(i + 1)}
        />
      ))}
      ...
    </>
  );
}

Then we would have to grab the rating from the onRate handler that we added to the StarRating component. We then pass the new rating along with the id of the color to be rated up to the Color component’s parent via another onRate function property:

export default function Color({
  id,
  title,
  color,
  rating,
  onRemove = f => f,
  onRate = f => f
}) {
  return (
    <section>
      <h1>{title}</h1>
      <button onClick={() => removeColor(id)}>
        <FaTrash color="red" />
      </button>
      <div style={{ height: 50, backgroundColor: color }} />
      <StarRating
        selectedStars={rating}
        onRate={rating => onRate(id, rating)}
      />
    </section>
  );
}

In the ColorList component, we would have to capture onRate event from individual color components and pass them up to it’s parent via the onRateColor function property:

export default function ColorList({
  colors = [],
  onRemoveColor = f => f,
  onRateColor = f => f
}) {
  return (
    <div className="color-list">
      {colors.length === 0 ? (
        <p>No Colors Listed. (Add a Color)</p>
      ) : (
        colors.map(color => (
          <Color
            key={color.id}
            {...color}
            onRemove={onRemoveColor}
            onRate={onRateColor}
          />
        ))
      )}
    </div>
  );
}

Finally, after passing the event up through all of these components we arrive at the App, where state is stored and the new rating can be saved:

export default function App() {
  const [colors, setColors] = useState(colorData);
  return (
    <ColorList
      colors={colors}
      onRateColor={(id, rating) => {
        const newColors = colors.map(color =>
          color.id === id ? { ...color, rating } : color
        );
        setColors(newColors);
      }}
      onRemoveColor={id => {
        const newColors = colors.filter(color => color.id !== id);
        setColors(newColors);
      }}
    />
  );
}

The App component will change color ratings when the ColorList invokes the onRateColor property with the id of the color to rate and the new rating. We will use those values to construct an array of new colors by mapping over the existing colors and changing the rating for the color that matches the id property. Once we send the newColors to the setColors function the state value for colors will change and the App component will be invoked with a new value for the colors array.

Building Forms

For a lot of us, being a web developer means that you are collecting large amounts of information from users with forms. If this sounds like your job, then you will be building a lot of form components with React. All of the HTML form elements that are available to the DOM are also available as React elements, which means that you may already know how to render a form with JSX:

<form>
  <input type="text" placeholder="color title..." required />
  <input type="color" required />
  <button>ADD</button>
</form>

This form element has three child elements: two input elements, and a button. The first input element is a text input that will be used to collect the title value for new colors. The second input element is a HTML color input that will allow the users to pick a color from a color wheel. We’ll be using basic HTML form validation so we’ve marked both inputs as required. The ADD button will be used to add a new color.

Using Refs

When it is time to build a form component in React there are several patterns available to you. One of these patterns involves accessing the DOM node directly using a React feature called refs. In React, a ref is an mutable object that stores values for the lifetime of a component. There are several use cases that involve using refs, in this section we will look at how we can access a DOM node directly with a ref.

React provides us with a useRef hook that we can ue to create a ref. We will use this hook when building the AddColorForm component:

import React, { useRef } from "react";

export default function AddColorForm({ onNewColor = f => f }) {
  const txtTitle = useRef();
  const hexColor = useRef();

  const submit = e => { ... }

  return (...)
}

First, when creating this component we will also create two refs using the useRef hook. The txtTitle ref will be used to reference the text input that we’ve added to the form to collect the color title. The hexColor ref will be used to access hexadecimal color values from the HTML color input. We can set the values for these refs directly in JSX using the ref property:

  return (
    <form onSubmit={submit}>
      <input ref={txtTitle} type="text" placeholder="color title..." required />
      <input ref={hexColor} type="color" required />
      <button>ADD</button>
    </form>
  );
}

Here we set the value for the txtTitle and hexColor refs by adding the ref attribute to these input elements in JSX. This creates a current field on our ref object that references the DOM element directly. This provides us access to the DOM element, which means we can capture it’s value. When the user submits this form by clicking the “ADD” button we will invoke the submit function:

const submit = e => {
  e.preventDefault();
  const title = txtTitle.current.value;
  const color = hexColor.current.value;
  onNewColor(title, color);
  txtTitle.current.value = "";
  txtColor.current.value = "";
};

When we submit HTML forms, by default they send a POST request to the current url with the values of the form elements stored in the body. We don’t want to do that. This is why the first line of code in the submit function is e.preventDefault(), which prevents the browser from trying to submit the form with a POST request.

Next, we capture the current values for each of our form elements using their refs. These values are then passed up to this component’s parent via the onNewColor function property. Both the title and the hexadecimal value for the new color are passed as function arguments. Finally, we reset the value attribute for both inputs to clear the data and prepare the form to collect another color.

Did you notice the subtle paradigm shift has occurred by using refs? We are mutating the value attribute of DOM nodes directly by setting them equal to "" empty strings. This is imperative code. The AddColorForm is now what we call an uncontrolled component because it uses the DOM to save the form values. Sometimes using uncontrolled component can get you out of problems. For instance, you may want to share access to a form and it’s values with code outside of React. However, a controlled component is a better approach.

Controlled Components

In controlled component the from values are managed by React and not the DOM. They do not require us to use refs. They do not require us to write imperative code. Adding features like robust form validation are much easier when working with a controlled components. Let’s modify the AddColorForm by giving it control over the form’s state:

import React, { useState } from "react";

export default function AddColorForm({ onNewColor = f => f }) {
  const [title, setTitle] = useState("");
  const [color, setColor] = useState("#000000");

  const submit = e => { ... };

  return ( ... );
}

First, instead of using refs we are going to save the values for the title and color using React state. Instead of useRef we will state create variables for title and color. Additionally, we will also define the functions that can be used to change state: setTitle and setColor.

Now that the component controls the values for title and color, we can display them inside of the form input elements by setting the value attribute. Once we set the value attribute of an input element, we will no longer be able to change with the form. The only way to change the value at this point would be to change the state variable every time the user types a new character in the input element. That is exactly what we’ll do:

<form onSubmit={submit}>
  <input
    value={title}
    onChange={event => setTitle(event.target.value)}
    type="text"
    placeholder="color title..."
    required
  />
  <input
    value={color}
    onChange={event => setColor(event.target.value)}
    type="color"
    required
  />
  <button>ADD</button>
</form>
}

This controlled component now sets the value of both input elements using the title and color from state. Whenever these elements raise an onChange event we can access the new value using the event argument. The event.target is a reference to the DOM element so we can obtain the current value of that element with event.target.value. When the title changes, we’ll invoke setTitle to change the title value in state. Changing that value will cause this component to re-render and we can now display the new value for title inside the input element. Changing the color works exactly the same way.

When it is time to submit the form we can simply pass the state values for title and color to the onNewColor function property as arguments when we invoke it. The setTitle and setColor functions can be used to reset the values after the new color has been passed to the parent component.

const submit = e => {
  e.preventDefault();
  onNewColor(title, color);
  setTitle("");
  setColor("");
};

It’s called a controlled component, because React controls the state of the form. It’s worth pointing out that controlled form components are re-rendered, a lot. Think about it, every new character typed in the title field causes the AddColorForm to re-render. Using the color wheel in the color picker causes this component to re-render way more than the title field because the color value repeatably changes as you drag the mouse around the color wheel. This is ok, React is designed to handle this type of work load. Hopefully knowing that controlled components are re-rendered frequently will prevent us from adding some long and expensive process to this component. At the very least this knowledge will come in handy when you are trying to optimize your React components.

Creating Custom Hooks

Whey you have a large form with a lot of input elements you may be tempted to copy and paste these two lines of code:

value={title}
onChange={event => setTitle(event.target.value)}

It might seem like you are working faster to simply copy and paste these properties into every form element while tweaking the variables names along the way. However, whenever you copy and paste code you should hear a tiny little alarm sound in your head. Copying and pasting code suggests that there is something redundant enough to abstract away in a function.

We can package the details necessary to create controlled form components into a custom hook. We could create our own useInput hook where we can abstract away the redundancy involved with creating controlled form inputs:

import { useState } from "react";

export const useInput = initialValue => {
  const [value, setValue] = useState(initialValue);
  return [
    { value, onChange: e => setValue(e.target.value) },
    () => setValue(initialValue)
  ];
};

This is a custom hook. It doesn’t take a lot of code. Inside of this hook we are still using the useState hook to create a state value. Next we return an array. The first value of the array is the object that contains the same properties that we were tempted to copy and paste: the value from state along with an onChange function property that changes that value in state. The second value in the array is a function that can be reused to reset the value back to its initial value. We can use our hook inside of the AddColorForm:

import React from "react";
import { useInput } from "./hooks";

export default function AddColorForm({ onNewColor = f => f }) {
  const [titleProps, resetTitle] = useInput("");
  const [colorProps, resetColor] = useInput("");

  const submit = event => { ... }

  return ( ... )
}

The useState hook is encapsulated within our useInput hook. We can obtain the properties for both the title and the color by destructuring them from the first value of the returned array. The second value of this array contains a function that we can use to reset the value property back to its initial value, an empty string. The titleProps and colorProps are ready to be spread into their corresponding input elements:

return (
  <form onSubmit={submit}>
    <input
      {...titleProps}
      type="text"
      placeholder="color title..."
      required
    />
    <input {...colorProps} type="color" required />
    <button>ADD</button>
  </form>
);
}

Spreading these properties from our custom hook is much more fun than pasting them. Now both the title and the color inputs are receiving properties for their value and onChange events. We’ve used our hook to create controlled form inputs without worrying about the underlying implementation details. The only other change that we need to make is when this form is submitted:

const submit = event => {
  event.preventDefault();
  onNewColor(titleProps.value, colorProps.value);
  resetTitle();
  resetColor();
};

Within the submit function, we need to be sure to grab the value for both the title and the color from their properties. Finally, we can use the custom reset functions that were returned from the useInput hook.

Hooks are designed to be used inside of React components. We can compose hooks within other hooks because eventually the customized hook will be used inside of a component. Changing the state within this hook still causes the AddColorForm to re-render with new values for titleProps or colorProps.

Adding Colors to State

Both the controlled form component and the uncontrolled from component pass the values for title and color to the parent component via the onNewColor function. The parent doesn’t care whether we used a controlled component or uncontrolled component, it only wants the values for the new color.

Let’s add the AddColorForm, whichever one you choose, to the the App component. When the onNewColor property is invoked we will save the new color in state:

import React, { useState } from "react";
import colorData from "./color-data.json";
import ColorList from "./ColorList.js";
import AddColorForm from "./AddColorForm";
import { v4 } from "uuid";

export default function App() {
  const [colors, setColors] = useState(colorData);
  return (
    <>
      <AddColorForm
        onNewColor={(title, color) => {
          const newColors = [
            ...colors,
            {
              id: v4(),
              rating: 0,
              title,
              color
            }
          ];
          setColors(newColors);
        }}
      />
      <ColorList .../>
    </>
  );
}

When a new color is added the onNewColor property is invoked. The title and hexadecimal value for the new color are passed to this function as arguments. We use these arguments to create a new array of colors. First, we spread the current colors from state into the new array. Then we add an entirely new color object using the title and color values. Additionally, we set the rating of the new color to 0, because it has not yet been rated. We also use the v4 function found in the uuid package to generate a new unique id for the color. Once we have a array of colors that contains our new color, we save it to state by invoking setColors. This causes the App component to re-render with a new array of colors. That new array will be used to update the UI. We will see the new color at bottom of the list.

With this change, we complete the first iteration on the Color Organizer. Users can now add new colors to the list, remove colors from the list, and rate any existing color on that list.

React Context

Storing state in one location at the root of your tree was an important pattern that helped us all be more successful with early versions of React. Learning to pass state both down and up a component tree via properties is a necessary right of passage for any React developer. It’s something that we all should know how to do. However, as React evolved and our component trees got larger, following this principle slowly became more unrealistic. It is hard for many developers to maintain state in a single location at the root of a component tree for a complex application. Passing state down and up the tree through dozens of components is tedious and bug ridden.

Passing State from root to leaves

The UI elements that most of us work on are complex. The root of the tree is often very far from the leafs. This puts data that the application depends on many layers away from the components that use the data. Every component must receive props that they only pass to their children. This will not only bloat your code and make your UI harder to scale.

Passing state data through every component as props until it reaches the component that needs to use it is like taking the train from San Francisco to DC. On the train you will pass through ever state but you won’t get off until you reach your destination.

train sf dc
Figure 6-6. Train from San Francisco to DC

It is obviously more efficient to fly from San Francisco to DC. This way, you do not have to pass through every state, you simply fly over them.

fly sf dc
Figure 6-7. Flight from San Francisco to DC

In React, context is like jet setting for your data. You can place data in React context by creating a context provider. A context provider is a React component that you can wrap around your entire component tree, or specific sections of your component tree. The context provider is the departing airport where your data boards the plane. It’s also the airline hub. All flights depart from that airport to different destinations. Each destination is a context consumer The context consumer is the React component that retrieves the data from context. This is the destination airport where your data lands, deplanes, and goes to work.

context
Figure 6-8. Context Providers and Consumers

Using context still allows us store state data in a single location, but it does not require us to pass that data through a bunch of components that do not need it.

Placing Colors in Context

In order to use context in React, we must first place some data in a context provider and add that provider to our component tree. React comes with a function called createContext that we can use to create a new context object. This object contains two components: a context Provider and a Consumer.

Let’s place the default colors found in the color-data.json file in to context. We’ll add context to the index.js file, the entry point of our application:

import React, { createContext } from "react";
import colors from "./color-data";
import { render } from "react-dom";
import App from "./App";

export const ColorContext = createContext();

render(
  <ColorContext.Provider value={{ colors }}>
    <App />
  </ColorContext.Provider>,
  document.getElementById("root")
);

Using createContext we created a new instance of React context that we named ColorContext. The color context contains two components: the ColorContext.Provider and the ColorContext.Consumer. We need to use the provider to place the colors in state. We add data to context by setting the value property of the Provider. In this scenario, we added an object containing the colors to context. Since we wrapped the entire App component with the provider the array of colors will made available to any context consumers found in our entire component tree. It is important to notice that we’ve also exported the ColorContext from this location. This is necessary because we will need to access the ColorContext.Consumer when we want to obtain the colors from context.

Note

A context Provider does not always have to wrap an entire application. It is not only ok to wrap specific sections component with a context Provider, it can make your application more efficient. The Provider will only provide context values to it’s children.

Note

It is ok to use multiple context providers. In fact, you may be using context providers in your React app already without even knowing about it. Many npm packages designed to work with React use context behind the scenes.

Now that we are providing the colors value in context, the App component no longer needs to hold state and pass it down to it’s children as props. We have made the App component a “fly over” component. The Provider is the App’s parent, it is providing the colors in context. The ColorList is the App component’s child, it can obtain the colors directly on its own. So the app doesn’t need to touch the colors at all, which is great because the App component itself has noting to do with colors. That responsibility has been delegated further down the tree.

We can remove a lot of lines of code from the App. It only needs to render the AddColorForm and the ColorList. It no longer has to worry about the data:

import React from "react";
import ColorList from "./ColorList.js";
import AddColorForm from "./AddColorForm";

export default function App() {
  return (
    <>
      <AddColorForm />
      <ColorList />
    </>
  );
}

Retrieving Colors with useContext

The addition of hooks make working with context a joy. The useContext hook is used to obtain values from context. It obtains those values that we need from the context Consumer. The ColorList component no longer needs to obtain the array of colors from its properties. It can access them directly via the useContext hook:

import React, { useContext } from "react";
import { ColorContext } from "./";
import Color from "./Color";

export default function ColorList() {
  const { colors } = useContext(ColorContext);
  return (
    <div className="color-list">
      {colors.length === 0 ? (
        <p>No Colors Listed. (Add a Color)</p>
      ) : (
        colors.map(color => <Color key={color.id} {...color} />)
      )}
    </div>
  );
}

We’ve modified the ColorList component. We’ve removed the colors=[] property because the colors are being retrieved from context. The useContext hooks requires the context instance to obtain values from it. The ColorContext instance is being imported from the index.js file where we create the context and add the provider to our component tree. The ColorList can now construct a user interface based on the data that has been provided in context.

Using Context Consumer

The Consumer is accessed within the useContext hook, which means that we no longer have to work directly with the consumer component. Before hooks, we would have to obtain the colors from context using a pattern called render props within the context consumer. Render props are passed as arguments to a child function. The following example is how you would user the consumer to obtain the colors from context:

export default function ColorList() {
  return (
    <ColorContext.Consumer>
      {context => {
        return (
          <div className="color-list">
            {context.colors.length === 0 ? (
              <p>No Colors Listed. (Add a Color)</p>
            ) : (
              context.colors.map(color => <Color key={color.id} {...color} />)
            )}
          </div>
        )
      }}
    </ColorContext.Consumer>
  )
}

Stateful Context Providers

The context provider can place an object into context, but it can’t mutate the values in context on its own. It needs some help from a parent component. The trick is to create a stateful component that renders a context provider. When the state of the stateful component changes it will re-render the context provider with new context data. Any of the context providers children will also be re-rendered with the new context data.

The stateful component that renders the context provider is your custom provider. That is the component that will be used to when it is time to wrap your App with the provider. In a brand new file, let’s create a ColorProvider:

import React, { createContext, useState } from "react";
import colorData from "./color-data.json";

const ColorContext = createContext();

export const ColorProvider = ({ children }) => {
  const [colors, setColors] = useState(colorData);
  return (
    <ColorContext.Provider value={{ colors, setColors }}>
      {children}
    </ColorContext.Provider>
  );
};

The ColorProvider is a component that renders the ColorContext.Provider. Within this component, we have created a state variable for colors using the useState hook. The initial data for colors is still being populated from color-data.json. Next, the ColorProvider adds the colors from state to context using the value property of the ColorContext.Provider. Any children rendered within the ColorProvider will be wrapped by the ColorContext.Provider and will have access to the colors array from context.

You may have noticed that the setColors function is also being added to context. This gives context consumers the ability to change the value for colors. Whenever setColors is invoked, the colors array will change. This will cause the ColorProvider to re-render and your UI will update itself to display the new colors array.

Adding the setColors to context may not be the best idea. It invites other developers and yourself to make mistakes later on down the road when using it. There are only three options when it comes to chancing the value of the colors array. Users can add colors, remove colors, or rate colors. It’s a better idea to add functions for each of these operations to context. This way you do not expose the setColors function to consumers. You only expose functions for the changes that they are allowed to make.

export const ColorProvider = ({ children }) => {
  const [colors, setColors] = useState(colorData);

  const addColor = (title, color) =>
    setColors([
      ...colors,
      {
        id: v4(),
        rating: 0,
        title,
        color
      }
    ]);

  const rateColor = (id, rating) =>
    setColors(
      colors.map(color => (color.id === id ? { ...color, rating } : color))
    );

  const removeColor = id => setColors(colors.filter(color => color.id !== id));

  return (
    <ColorContext.Provider value={{ colors, addColor, removeColor, rateColor }}>
      {children}
    </ColorContext.Provider>
  );
};

That looks better. We added functions to context for all of the operations that can be made on the colors array. Now any component within our tree can consume these operations and make changes to colors using simple functions that we can document.

Custom Hooks with Context

There is one more killer change that we can make. The introduction of hooks has made it so that we do not have to expose Context to consumer components at all. Let’s face it, context can be confusing for team members who are not reading this book. We can make everything much easer for them by wrapping context in a custom hook. Instead of exposing the ColorContext instance, we can create a hook called useColors that returns the colors from context.

import React, { createContext, useState, useContext } from "react";
import colorData from "./color-data.json";
import { v4 } from "uuid";

const ColorContext = createContext();
export const useColors = () => useContext(ColorContext);

This one simple change makes a huge impact on architecture. We have wrapped all of the functionality necessary to render and work with stateful colors in a single javascript module. Context is contained to this module, yet exposed through a hook. This works because we return context using the useContext hook which has access to the ColorContext locally in this file. It is now appropriate to rename this module color-hooks.js and distribute this functionality for wider use by the community.

Consuming colors using the ColorProvider and the useColors hook is a joyous event. This is why we program. Let’s take this hook out for a spin in the current Color Organizer app. First, we need to wrap our App component with the custom ColorProvider. We can do this in the index.js file:

import React from "react";
import { ColorProvider } from "./color-hooks.js";
import { render } from "react-dom";
import App from "./App";

render(
  <ColorProvider>
    <App />
  </ColorProvider>,
  document.getElementById("root")
);

Now any component that is a child of the App can obtain the colors from the useColors hook. The ColorList components needs to access the colors array to render the colors on the screen:

import React from "react";
import Color from "./Color";
import { useColors } from "./color-hooks";

export default function ColorList() {
  const { colors } = useColors();
  return ( ... );
}

We have removed any references to context from this component. Everything it needs is now being provided from out hook. The Color component could use our hook to obtain the functions for rating and removing colors directly:

import React from "react";
import StarRating from "./StarRating";
import { useColors } from "./color-hooks";

export default function Color({ id, title, color, rating }) {
  const { rateColor, removeColor } = useColors();
  return (
    <section>
      <h1>{title}</h1>
      <button onClick={() => removeColor(id)}>X</button>
      <div style={{ height: 50, backgroundColor: color }} />
      <StarRating
        selectedStars={rating}
        onRate={rating => rateColor(id, rating)}
      />
    </section>
  );
}

Now the Color component no longer needs to pass events to the parent via function props. It has access to the rateColor and removeColor functions in context. They are easily obtained through the useColors hook. This is a lot of fun, but we are not finished yet. The AddColorForm can also benefit from the useColors hook:

import React from "react";
import { useInput } from "./hooks";
import { useColors } from "./color-hooks";

export default function AddColorForm() {
  const [titleProps, resetTitle] = useInput("");
  const [colorProps, resetColor] = useInput("#000000");
  const { addColor } = useColors();

  const submit = e => {
    e.preventDefault();
    addColor(titleProps.value, colorProps.value);
    resetTitle();
    resetColor();
  };

  return ( ... );
}

The AddColorForm component can add colors directly with the addColor function. When colors are added, rated, or removed, the state of the colors value in context will change. When this change happens, the children of the ColorProvider are re-rendered with new context data. All of this is happening through a simple hook.

Hooks provide software developers with the stimulation that they need to stay motivated and enjoy front end programming. This is primarily because they are an awesome tool for separating concerns. Now React components only need to concern themselves with rendering other react components and keeping the user interface up to data. React hooks can concern themselves with the logic that is required to make the app work. Both UI and hooks can be developed separately, tested separately, even deployed separately. This is all very good news for React.

We have only scratched the surface of what can be accomplished with hooks. In the next chapter, we’ll dive a little deeper.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset