Home > Back-end >  Issues with states not updating correctly in React/Django
Issues with states not updating correctly in React/Django

Time:02-20

Building a site where users upload images/video, in the component that handles that, I have the images load into the page after being uploaded, so that the user can make sure they want to post them, and have the option to remove them if they like. I originally had this as a Class-based view, and everything worked as expected, but now, after changing to a functional component, after uploading images, React doesn't seem to notice the changes to imageLinks (even though the console shows that imageLinks is getting added to), until I update something else like the post title, then they all load in as expected. Once loaded in, if I click the delete button, React instantly updates and the photos/videos no longer shows up, as expected.

Any ideas as to why this is behaving so oddly? The fact that the deleteMedia function works just fine is what really weirds me out.

I set up my variables like this

export default function NewPost(props) {
  const [postCategories, setPostCategories] = useState([]);
  const [postTitle, setPostTitle] = useState();
  const [postDescription, setPostDescription] = useState();
  const [postHashtags, setPostHashtags] = useState();
  const [imageLinks, setImageLinks] = useState([]);
...}

In my component, I have this to handle uploading and deleting files.

const uploadMedia = async (file) => {
    var csrftoken = getCookie("csrftoken");
    var media_id = makeMediaID();
    let formData = new FormData();
    formData.append("content_id", contentID);
    formData.append("creator_id", USER_ADDRESS);
    formData.append("content_media", file);
    formData.append("media_id", media_id);
    const response = await fetch("/api/newpost/media/", {
      method: "POST",
      headers: {
        "X-CSRFToken": csrftoken,
      },
      body: formData,
    });
    console.log(response);
    const r_json = await response.json();
    if (r_json.success) {
      let tempLinks = imageLinks;
      tempLinks.push({
        img: r_json.image_url,
        id: r_json.m_id,
        type: r_json.m_type,
      });
      setImageLinks(tempLinks);
      console.log(imageLinks);
    } else {
      console.log(r_json.message);
    }
  };
  
  const deleteMedia = async (media) => {
    var csrftoken = getCookie("csrftoken");
    let formData = new FormData();
    formData.append("media_id", media);
    formData.append("creator_id", USER_ADDRESS);
    const response = await fetch("/api/newpost/media/delete/", {
      method: "DELETE",
      headers: {
        "X-CSRFToken": csrftoken,
      },
      body: formData,
    });
    console.log(response);
    const r_json = await response.json();
    if (r_json.success) {
      let tempLinks = imageLinks.filter((item) => item.id !== media);
      setImageLinks(tempLinks);
    } else {
      console.log("Media deletion error");
    }
  };

And in my render, I have this, which worked just fine when it was a class-based component.

{imageLinks.map((item) => (
                  <Grid item xs={9} align="center" key={item.img}>
                    <Card style={{ maxWidth: 550, margin: 15 }}>
                      <div
                        style={{
                          display: "flex",
                          alignItem: "center",
                          justifyContent: "center",
                        }}
                      >
                        <CardMedia
                          style={{
                            width: "100%",
                            maxHeight: "550px",
                          }}
                          component={item.type}
                          image={item.img}
                          controls
                          title={String(item.img)}
                        />
                      </div>
                      <div align="center">
                        <CardActions>
                          <Button
                            endIcon={<DeleteForeverIcon />}
                            label="DELETE"
                            color="secondary"
                            variant="contained"
                            onClick={() => deleteMedia(item.id)}
                          >
                            REMOVE
                          </Button>
                        </CardActions>
                      </div>
                    </Card>
                  </Grid>
                ))}

CodePudding user response:

Issue

The issue is a state mutation.

const [imageLinks, setImageLinks] = useState([]);

tempLinks is a reference to the imageLinks state. You are pushing directly into this tempLinks array and saving the same array reference back into state. The code is also incorrectly attempting to log the state immediately after enqueueing the state update. This doesn't work as React state updates are asynchronously processed.

if (r_json.success) {
  let tempLinks = imageLinks; // <-- reference to state
  tempLinks.push({            // <-- state mutation!
    img: r_json.image_url,
    id: r_json.m_id,
    type: r_json.m_type,
  });
  setImageLinks(tempLinks);   // <-- same reference back into state
  console.log(imageLinks);    // <-- only logs current state
}

The imageLinks state array reference never changes so React doesn't see this as state actually being updated.

Solution

Create and return a new array reference for React.

if (r_json.success) {
  setImageLinks(imageLinks => [ // <-- new array reference
    ...imageLinks,              // <-- shallow copy previous state
    {                           // <-- append new element object
      img: r_json.image_url,
      id: r_json.m_id,
      type: r_json.m_type,
    }
  ]);
}

Use a separate useEffect hook to console log the state updates.

useEffect(() => {
  console.log(imageLinks);
}, [imageLinks]);
  • Related