React: Lifting state into higher order components

R
andrewgbliss
7 months ago

In this article we are going to cover high order components

. . .

What is a re-render count?

Every time you make a change to your React state it will re-render your component, thus showing the change. This is very useful in making a responsive interactive web applications in React. What you want to do, when coding, is think about how many times your component is going to re-render. If you code your component the wrong way it could end up slowing the whole application down because it is running too many re-renders.

Splitting out components

Consider the following component:

import React, { useState } from "react";
import { makeStyles } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
import Box from "@material-ui/core/Box";
import { useTodos } from "../store/Store";

const useStyles = makeStyles((theme) => ({
  textField: {
    width: 400,
    [theme.breakpoints.down("xs")]: {
      width: 200,
    },
  },
}));

const TodoInput = (props) => {
  const classes = useStyles();
  const [newTodo, setNewTodo] = useState("");
  return (
    <>
      <Grid container>
        <Grid item>
          <TextField
            className={classes.textField}
            label="What do you want to do?"
            variant="outlined"
            size="small"
            value={newTodo}
            onChange={(e) => setNewTodo(e.target.value)}
          />
        </Grid>
        <Grid item>
          <Box pl={1}>
            <Button
              variant="contained"
              color="primary"
            >
              Add Todo
            </Button>
          </Box>
        </Grid>
      </Grid>
      <Grid container>
        <Grid item>
          <ul>
            {todos.map((todo) => (<li>{todo}</li>)}
          </ul>
        </Grid>
      </Grid>
    </>
  );
};

export default TodoInput;

This component contains the input field when a user wants to type in a new todo, and it shows an unordered list of todos. If you run this and check the React component profiler you can see every time you type it also re-renders the unordered list. This is known as unwanted re-renders and can slow your application down.

To fix this we will split out these components.

Higher Order Components

To split out these components we will make what is called a higher order component that will contain the split out child components. Consider the following component:

import React from "react";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";

const Todos = () => {
  return (
    <Box p={2}>
      <Grid container direction="column">
        <Grid item>
          <TodoInput />
        </Grid>
        <Grid item>
          <TodoList />
        </Grid>
      </Grid>
    </Box>
  );
};

export default Todos;

You can see that this component has the two child components TodoInput and TodoList. This is considered a higher order component that contains child components. This main Todos component contains all the needed child components in itself to be a successful Todos component. By doing things this way we can split out TodoInput and TodoList so that they render independently of each other and don't cause any unwanted re-renders.

Passing Down Props

So that's all good about splitting out the components, but now how does the TodoList get the todos data to show the list of todos. We can store the data in the higher order component and pass that data down through props (short for properties).

import React from "react";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";

const Todos = () => {
  const todos = ['feed the dog', 'go shopping', 'hang gliding'];
  return (
    <Box p={2}>
      <Grid container direction="column">
        <Grid item>
          <TodoInput />
        </Grid>
        <Grid item>
          <TodoList todos={todos} />
        </Grid>
      </Grid>
    </Box>
  );
};

export default Todos;

So in this example we store a hard coded array and then pass that array through a prop called todos like this

<TodoList todos={todos} />

This will give the TodoList component access to the array that we can then use to build out the todo list.

So let's work now on the TodoList to use props.

import React from "react";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import Checkbox from "@material-ui/core/Checkbox";

const TodoList = (props) => {
  const { todos } = props;
  return (
    <>
      <List>
        {todos.map((todo) => {
          return (
            <ListItem>
              <ListItemText primary={todo} />
              <ListItemSecondaryAction>
                <Checkbox
                  checked={false}
                />
              </ListItemSecondaryAction>
            </ListItem>
          );
        })}
      </List>
    </>
  );
};

export default TodoList;

Now we have updated the TodoList component to takes in props and use the todos array by showing a list.

Using state in the higher order component

Let's now update the higher order component to use state so we can actually add a new todo from the input component.

import React, { useState } from "react";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";

const Todos = () => {
  const [todos, setTodos] = useState(['feed the dog', 'go shopping', 'hang gliding']);
  return (
    <Box p={2}>
      <Grid container direction="column">
        <Grid item>
          <TodoInput todos={todos} setTodos={setTodos} />
        </Grid>
        <Grid item>
          <TodoList todos={todos} />
        </Grid>
      </Grid>
    </Box>
  );
};

export default Todos;

The update now is using state for the todos array. We brought in the useState hooks from react. we changed the todos array to this:

const [todos, setTodos] = useState(['feed the dog', 'go shopping', 'hang gliding']);

And then we passed the todos arrays and the setTodos function to the TodosInput so we can add more todos to the array.

import React, { useState } from "react";
import { makeStyles } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
import Box from "@material-ui/core/Box";
import { useTodos } from "../store/Store";

const useStyles = makeStyles((theme) => ({
  textField: {
    width: 400,
    [theme.breakpoints.down("xs")]: {
      width: 200,
    },
  },
}));

const TodoInput = (props) => {
  const { todos, setTodos} = props
  const classes = useStyles();
  const [newTodo, setNewTodo] = useState("");
  const onClick = () => {
    setTodos([...todos, newTodo]);
    setNewTodo("");
  };
  return (
    <>
      <Grid container>
        <Grid item>
          <TextField
            className={classes.textField}
            label="What do you want to do?"
            variant="outlined"
            size="small"
            value={newTodo}
            onChange={(e) => setNewTodo(e.target.value)}
          />
        </Grid>
        <Grid item>
            <Button
              variant="contained"
              color="primary"
              onClick={onClick}
            >
              Add Todo
            </Button>
        </Grid>
      </Grid>
    </>
  );
};

export default TodoInput;

In this update we bring in the todos and setTodos state from the higher order component and when we click on the button it adds to the state. Then we can open up the profiler again and see that it doesn't re-render the list when we type in the TodoInput component.

Conclusion

So that's how to lift state to a higher order component so that you can split out components and share state between them. Doing this helps, not only simplify your components, but cuts down on the re-render count.

Having a fast web application will make your users happy.


R
andrewgbliss
7 months ago