'React: hook-using component won't recognize current state of parent

So, I'm working on a writing page, which implements multilingual support using hooks:

const [ lang, setLang ] = useState('en');
const [ postState, setPostState ] = useState({
  'en': {
    title: '',
    ...
  }
  ...
});

and each input component handles it like this:

<input
  type='text'
  onInput={
    e => setPostState({
      ...postState,
      [lang]: {
        ...postState[lang],
        title: e.target.value
      }) }
  value={ postState[lang].title }
  ... />

And it works very well, until I get to a custom component that uses useState in its own scope:

<Gallery
  onSelect={ (e, src) =>
  {
    setPostState({
      ...postState,
      [lang]: {
        ...postState[lang],
        header: src
      });
   } } />

in this case, Gallery is a custom component that uses its own hook, and passes onSelect to children it generates using that hook:

const [ files, setFiles ] = useState([]);
...
useEffect(async () =>
{
  const result = await axios.get('/api/upload');
  setFiles(result.data.files);
}, []);
return (<>
  ...
  { files.map(name => (<Media src={ name } onClick={ onSelect }/>)) }
  ...
</>

Note that Media also uses useState (in order to set the element it uses based on the media mime type, and passes to said element onClick to which it provides both the event and src), yet unless I take the useState out of Gallery, the functions always runs with the value lang initially had (en in this case), not whatever it currently has.

Edit: someone suggested the media component, and further debugging does seem to isolate the problem to it:

export default ({src='', onClick }) =>
{
  const [ elem, setElem ] = useState(<div/>);
  useEffect(async () =>
  {
    const blob = await axios.get(
      `/static/uploads/${ src }`,
      { responseType: 'blob' });
    const type = blob.data.type.split('/')[0];
    switch (type)
    {
      case 'image':
        setElem(<img onClick={ e => onClick(e, src) } src={ URL.createObjectURL(blob.data) }/>);
        break;
      ...
    }
  }, []);
  return elem;
};


Solution 1:[1]

Thanks for sharing the "Media" component, now I'm able to replicate your issue perfectly. As you said adding onclick as useEffect dependency will work, yes that works for me as well. The problem here seems to be with the implementation of "Media" component, Two major problem I see here :

  • One is directly related to your issue is using dynamic props (eg: onClick) without adding as dependency [Directly related to your issue]

  • Another is making the useEffect callback async [Not Recommended at all]

I'm sharing the improved "Media" component which worked for me, you can try as well.

JSX

import { useEffect, useState } from "react";
import axios from "axios";
const Media = ({ src, onSelect }) => {
  const [type, setType] = useState("");
  const fetchFileType = async (srcFile) => {
    const blob = await axios.get(`/static/uploads/${srcFile}`, {
      responseType: "blob",
    });
    const type = blob.data.type.split("/")[0];
    setType(type);
  };
  useEffect(() => {
    fetchFileType(src);
  }, [src]);
  switch (type) {
    case "image":
      return <img alt="option" onClick={(e) => onSelect(e, src)} src={src} />;
    default:
      return <></>;
  }
};
export default Media;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1