skip to Main Content

I have prepared a simple test project at Github to demonstrate my problem:

Animated screenshot

There is a MUI TextField, a thumbs up or down icon and a text string with word description.

The custom App.jsx is shown below:

const DICT = {
  AA: "Rough, cindery lava",
  AB: "An abdominal muscle",
  ABS: "Abdominal muscles",
  ABSQUATULATE: "",
  ABSQUATULATING: "",
  ABY: "Pay the penalty",
};

export default function App() {
  const [word, setWord] = useState("");
  const [description, setDescription] = useState("");
  const [found, setFound] = useState(false);

  const handleChange = (ev) => {
    ev.preventDefault();
    const key = ev.target.value.trim().toUpperCase();
    setWord(key);

    if (key.length < 2) {
      setDescription("");
      return;
    }

    setFound(DICT.hasOwnProperty(key));
    if (!found) {
      setDescription("The word is not found in the dictionary");
      return;
    }

    setDescription(DICT[key] || "");
  };

  return (
    <Box component="form" noValidate autoComplete="on">
      <TextField
        id="wordInput"
        label="Enter a word"
        value={word}
        onChange={handleChange}
      />

      <Box>
        {found ? <ThumbUp color="primary" /> : <ThumbDown color="error" />}
      </Box>

      <Box>{description}</Box>
    </Box>
  );
}

As you can see in the animated screenshot above, when a user keeps typing or deleting "A" into the TextField, then the word description found in the DICT object is displayed inconsistently and it takes few tries until "Rough, cindery lava" is displayed for "AA".

My problem is probably wide spread among ReactJs newbies like myself. And I think I should use the prevState form for a hook, but which one of the 3 and how? I am confused.

3

Answers


  1. When you run setKey() the value isn’t updated until the next render, so you can’t access that value (consistently) within the function.

    useEffect is designed to respond to changes in state:

    useEffect(() => {
       if (key.length < 2) {
          setDescription("");
          return;
        }
    
        setDescription(DICT[key] || "");
    }, [key]);     // this will run whenever key changes
    

    and another to respond to those changes:

    useEffect(() => {
      // this will run whenever description changes
      // note: you could just use DICT.hasOwnProperty in the previous useEffect
      // but you can't use "description" in the previous useEffect
      // because it wasn't set during the useEffect
    
      setFound(!!description);
    }, [description]);
    
    Login or Signup to reply.
  2. The problem is this code:

    setFound(DICT.hasOwnProperty(key));
    if (!found) {
      setDescription("The word is not found in the dictionary");
      return;
    }
    

    remember: the useState hook is not synchronous. So, you cannot validate the value just after setting it.

    you need to use the useEffect hook in order to listen to the state change:

    useEffect(() => {
      
        const newDescription = found ? DICT[word] || "" : "The word is not found in the dictionary"
        setDescription(newDescription)
        
    
    }, [found])
    

    but I think it is not necessary to use the found State, just the description state. Like this:

    const handleChange = (ev) => {
        ev.preventDefault();
        const key = ev.target.value.trim().toUpperCase();
        setWord(key);
    
        if (key.length < 2) {
          setDescription("");
          return;
        }
    
        setDescription(DICT[key] || The word is not found in the dictionary");
    };
    
    Login or Signup to reply.
  3. The state inside handleChange is the current state when called. All state updates would affect the next invocation of handleChange. This means that when you do this:

    setFound(DICT.hasOwnProperty(key));
    if (!found) {
      setDescription("The word is not found in the dictionary");
      return;
    }
    

    The found state you’re using is one step behind. The value would only update after the render caused by setting the state.

    The rookie mistake you’re making is having a state for computed values. Computed values are derived out of the current state, and don’t need a state of their own. You can compute the value of found and description on each render, and store them in a const instead. This would reduce the need for redundant useEffect calls that update other states, that cause multiple unnecessary re-renders, and make it harder to trace the flow.

    If the computation is resource heavy, and the component might render because of unrelated changes, you can use useMemo or memoize the component to avoid unnecessary calculations. In your case, the cost of computing both found and description is negligible.

    Example (see comments in code):

    // derive the value of description from the combination of word and found
    function getDescription(word, found) {
      if(word.length < 2) return "";
    
      return found
        ? DICT[word]
        : "The word is not found in the dictionary";
    }
    
    export default function App() {
      const [word, setWord] = useState("");
      const [description, setDescription] = useState("");
      const [found, setFound] = useState(false);
      
      const found = DICT.hasOwnProperty(word); // compute found
      const description = getDescription(word, found); // compute description
    
      const handleChange = (ev) => {
        ev.preventDefault();
        const key = ev.target.value.trim().toUpperCase();
        setWord(key);
      };
      
      // the rest of the component's code
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search