ReactJS - Updating the state causes lag

Viewed 414

Description

I have a small app that uses the useCallback() hook to update the state, but every time I do, it causes lag on the page. And I mean actual lag, not just the "waiting for the async setState()" delay.

My theory is that updating the state re-render too many components, because if I reduce the state to have much fewer values, the lag disappears.

Essentially, I fear that my way of updating the state isn't isolating the values I want.

Code

I loaded the repo into a CodeSandbox: https://codesandbox.io/s/long-forest-y9cdj?file=/src/App.js

Here's the gist:

  1. The buttons on the page have an onClick, passed down from App.js

  2. In App.js, we generate the handleClick callback which calls the reducer

  3. In reducer.js, the "UPDATE_COUNT" case updates the state to set the correct value.

To reproduce the problem

Mess around and click some buttons at a somewhat high tempo, and you should experience some lag. You know, the type of lag that causes the button animations to freeze, and your cursor to stay in the "pointer finger" model.

Question

Can the state update be done in a way that reduces this lag?

1 Answers

The only thing I found that was odd was the fact that you were declaring your useStyles hook inside your components, so new styling was being created each render cycle. Move these outside the components.

Example:

App.js

const useStyles = makeStyles((theme) => ({
    root: {
        flexGrow: 1
    },
    paper: {
        padding: theme.spacing(0),
        textAlign: "center",
        color: theme.palette.text.primary,
        backgroundColor: theme.palette.background
    }
}));

function App() {
  const classes = useStyles();

  const { state, dispatch } = React.useContext(MainContext);

  const handleClick = useCallback(
    (areaName, monsterName, newCount) => {
      newCount = newCount > 10 ? 10 : newCount;
      newCount = newCount < 0 ? 0 : newCount;

      dispatch({
        type: "UPDATE_COUNT",
        area: areaName,
        monster: monsterName,
        count: newCount
      });
    },
    [dispatch]
  );

  const areaObjects = Object.entries(state).map(([area, monster]) => {
    const monsters = Object.entries(monster).map(([monsterName, count]) => (
      <Grid item xs={12} key={monsterName}>
        <Monster
          area={area}
          name={monsterName}
          count={count}
          handleClick={handleClick}
        />
      </Grid>
    ));

    return (
      <Grid item xs={12} sm={6} md={4} lg={3} xl={2} key={area}>
        <Paper className={classes.paper}>
          <Grid item xs={12}>
            <h3>{area}</h3>
          </Grid>
          {monsters}
        </Paper>
      </Grid>
    );
  });

  return (
    <div className={classes.root} id={"app-container"} key={"app-container"}>
      <Grid container spacing={0}>
        {areaObjects}
      </Grid>
    </div>
  );
}

Monster.js

const useStyles = makeStyles((theme) => ({
  root: {
    flexGrow: 1
  },
  addButtonContainer: {
    backgroundColor: "lightblue"
  },
  subtractButtonContainer: {
    backgroundColor: "lightyellow"
  },
  maxButtonContainer: {
    backgroundColor: "lightgreen"
  },
  monsterName: {
    textAlign: "center"
  },
  row: {
    backgroundColor: "lightgray"
  },
  rowSuccess: {
    backgroundColor: "lightgreen"
  }
}));

export default function Monster({ area, name, count, handleClick }) {
  const handleIncrement = (areaName, monsterName) => {
    handleClick(areaName, monsterName, count + 1);
  };

  const handleDecrement = (areaName, monsterName) => {
    handleClick(areaName, monsterName, count - 1);
  };

  const handleMaxout = (areaName, monsterName) => {
    handleClick(areaName, monsterName, 10);
  };

  const classes = useStyles();
  const rowClass = count === 10 ? classes.rowSuccess : classes.row;

  return (
    <Grid container item className={rowClass}>
      <Grid item xs={7} className={classes.monsterName}>
        {name}
      </Grid>
      <Grid item xs={1} className={classes.maxButtonContainer}>
        <IconButton
          color={"primary"}
          variant={"contained"}
          size={"small"}
          onClick={() => handleMaxout(area, name)}
        >
          <ArrowUpward fontSize={"small"} />
        </IconButton>
      </Grid>
      <Grid item xs={1} className={classes.addButtonContainer}>
        <IconButton
          color={"primary"}
          variant={"contained"}
          size={"small"}
          onClick={() => handleIncrement(area, name)}
        >
          <Add fontSize={"small"} />
        </IconButton>
      </Grid>
      <Grid item xs={2}>
        {count}
      </Grid>
      <Grid item xs={1} className={classes.subtractButtonContainer}>
        <IconButton
          color={"secondary"}
          variant={"contained"}
          size={"small"}
          onClick={() => handleDecrement(area, name)}
        >
          <Remove fontSize={"small"} />
        </IconButton>
      </Grid>
    </Grid>
  );
}

I also memoized the state being provided by the context, though I don't entirely think this part was necessary.

index.js

import React, { useMemo } from "react";

import initialState from "./initialState";
import reducer from "./reducer";

const MainContext = React.createContext();

function MainContextProvider(props) {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  const value = useMemo(() => ({ state, dispatch }), [state]);

  return (
    <MainContext.Provider value={value}>{props.children}</MainContext.Provider>
  );
}

const MainContextConsumer = MainContext.Consumer;

export { MainContext, MainContextProvider, MainContextConsumer };

Edit reactjs-updating-the-state-causes-lag

Related