Jpark9061
Jpark9061

Reputation: 1080

React how to fix stale state in child component

Creating multiple child components, which each can setState of its parent, makes these child components have different versions of history of the state (aka stale state)

To reproduce the error:

  1. create 2 child components by clicking "add new social media"
  2. submit both child components to set parent state
  3. submit the firstly created component again, and now the second component input disappears from the resulting state

Another error:

  1. create 2 child components by clicking "add new social media"
  2. submit the secondly created child component
  3. submit the firstly created child component, and now the second component input disappears from the resulting state

All in all, I want the resulting state to have ALL the data of each components. How can I fix this?

https://codesandbox.io/s/blissful-fog-oz10p

const Admin = () => {
  const [links, setLinks] = useState({});
  const [newLink, setNewLink] = useState([]);

  const updateLinks = (socialMedia, url) => {
    setLinks({
      ...links,
      [socialMedia]: url
    });
  };

  const linkData = {
    links,
    updateLinks
  };

  const applyChanges = () => {
    console.log(links);
  };

  return (
    <>
      {newLink ? newLink.map(child => child) : null}
      <div className="container-sm">
        <Button
          type="submit"
          fullWidth
          variant="contained"
          color="primary"
          onClick={() => {
            setNewLink([
              ...newLink,
              <AddNewLink key={Math.random()} linkData={linkData} />
            ]);
          }}
        >
          Add new social media
        </Button>
        <Button
          type="submit"
          fullWidth
          variant="contained"
          color="primary"
          style={{ marginTop: "50px" }}
          onClick={() => applyChanges()}
        >
          Apply Changes
        </Button>
        <h3>{JSON.stringify(links, null, 4)}</h3>
      </div>
    </>
  );
};

export default Admin;

const AddNewLink = props => {
  const [socialMedia, setSocialMedia] = useState("");
  const [url, setUrl] = useState("");
  const { updateLinks } = props.linkData;

  const handleSubmit = () => {
    updateLinks(socialMedia, url);
  };

  return (
    <>
      <FormControl
        style={{ marginTop: "30px", marginLeft: "35px", width: "90%" }}
      >
        <InputLabel>Select Social Media</InputLabel>
        <Select
          value={socialMedia}
          onChange={e => {
            setSocialMedia(e.target.value);
          }}
        >
          <MenuItem value={"facebook"}>Facebook</MenuItem>
          <MenuItem value={"instagram"}>Instagram</MenuItem>
          <MenuItem value={"tiktok"}>TikTok</MenuItem>
        </Select>
      </FormControl>
      <form
        noValidate
        autoComplete="off"
        style={{ marginBottom: "30px", marginLeft: "35px" }}
      >
        <TextField
          id="standard-basic"
          label="Enter link"
          style={{ width: "95%" }}
          onChange={e => {
            setUrl(e.target.value);
          }}
        />
      </form>
      <div className="container-sm">
        <Button
          type="submit"
          fullWidth
          variant="contained"
          color="primary"
          style={{ marginBottom: "30px" }}
          onClick={() => handleSubmit()}
        >
          Submit
        </Button>
      </div>
    </>
  );
};

export default AddNewLink;

Upvotes: 3

Views: 1044

Answers (1)

Yousaf
Yousaf

Reputation: 29282

The problem you are facing is caused by updateLinks() function closing over the links state.

When you create two AddNewLink components, each of them is passed updateLinks() function. Since initially, links is an empty object, updateLinks() function passed to both instances of AddNewLink, has a closure over the links variable and links at that time refers to an empty object {}. So when you submit the form, as far as updateLinks() function is concerned, links is an empty object. So when merging links with the data passed from AddNewLink component, only the data from the submitted form is saved in the state because links is an empty object.

Solution:

You could use useRef hook to access the latest value of links inside updateLinks() function.

const Admin = () => {
   ...
   
   const linkRef = useRef();

   const updateLinks = (socialMedia, url) => {
      linkRef.current = { ...linkRef.current, [socialMedia]: url };
      setLinks(linkRef.current);
   };
   
   ...
};

Also i don't think you need more than one instance of AddNewLink component. You could add more than one social media link in the state with only a single instance of AddNewLink component.

Demo:

Edit sharp-tdd-hym00

Upvotes: 3

Related Questions