Preventing of incorrect state with React hooks

Viewed 161

I have component

import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";

const data = {
  "1": ["1a", "1b"],
  "2": ["2a"]
};

const slugs = {
  "1a": { text: "Lorem" },
  "1b": { text: "ipsum" },
  "2a": { text: "..." }
};

const ExamplePage: React.FC = () => {
  const { id } = useParams<{ id: string }>();
  const [index, setIndex] = useState(0);

  useEffect(() => {
    setIndex(0);
  }, [id]);

  console.log("state", {
    id,
    index
  });

  const slug = data[id][index];
  const text = slugs[slug].text;

  function onPrev(): void {
    if (index <= 0) {
      return;
    }

    setIndex((index) => index - 1);
  }

  function onNext(): void {
    if (index >= data[id].length - 1) {
      return;
    }

    setIndex((index) => index + 1);
  }

  return (
    <div>
      <button onClick={onPrev}>Prev</button>
      <span>{text}</span>
      <button onClick={onNext}>Next</button>
    </div>
  );
};

export default ExamplePage;

And Route for this:

<Route path="/:id" component={ExamplePage} />

Live version: https://codesandbox.io/s/react-hooks-ewl4d

There is a bug with this code when:

  1. User is on /1 url
  2. User clicks button "Next"
  3. User clicks link to /2 url

In this case id will be "2", index will be 1, but there isn't data["2"][1].

As you can see useEffect don't help in this case because useEffect don't stop current function call.

My question is: How I can ensure that this state will be always correct?

I know that I can write const text = slugs[slug]?.text; and this solve my bug, but still, in one moment, component have incorrect state. I wondering if is a way to prevent this incorrect state.

In React class component this problem can be solved by getDerivedStateFromProps - You can see this live on https://codesandbox.io/s/react-hooks-solve-in-react-component-xo43g

1 Answers

The useEffect will run async so you are trying to set the slug and text before the index has updated.

You can put the slug and text into state and then use another useEffect to update them when the index or id changes:

const { id } = useParams();
const [index, setIndex] = useState(0);
const [slug, setSlug] = useState();
const [text, setText] = useState();

useEffect(() => {
  setIndex(0);
}, [id]);

useEffect(() => {
  const newSlug = data[id][index];
  if (!newSlug) return; // If id changes but index has not updated yet

  setSlug(newSlug);
  setText(slugs[newSlug].text);
}, [id, index]);
Related