Search code examples
reactjsstatererender

Parent component not updating data when state is set from child component


I have a form that takes user data as prop from parent component to update, updates them then sets the state of parent, but parent component doesn't re-render when userData state changes so i have to refresh the page to see the updates

Parent component

  let emptyUser = {
    id: "",
    firstName: "",
    lastName: "",
    position: "",
    gender: "",
    birthDate: "",
    status: "",
    isAdmin: "",
    image: null,
    url: "",
  };

  const [userData, setUserData] = useState(emptyUser);

  useEffect(() => {
    const getUsers = async () => {
      const res = await fetch("http://localhost:8000/api/users", {
        credentials: "include",
      });

      const data = await res.json();
      if (data.status === 401) {
        dispatch({ type: "LOGOUT" });
        navigate("/login");
      }
      setUsers(data.users);
      // setUsers(data.users.filter((u) => u.id != 80));
    };
    getUsers();
  }, [dispatch, navigate, userData]);



  <UpdateUser userData={userData} setUserData={setUserData}/>

Child component

function UpdateUser({ userData, setUserDate }) {


  const onSelectFile = (e) => {
    if (!e.target.files || e.target.files.length === 0) {
      setUserDate({ ...userData, image: undefined });
      return;
    }

    setUserDate({ ...userData, image: e.target.files[0] });
    setDisableBtn(false);
  };

  const onInputChange = (e, variable) => {
    const val = (e.target && e.target.value) || "";
    let _user = { ...userData };
    _user[`${variable}`] = val;

    setUserDate(_user);
    setDisableBtn(false);
  };

  const editUser = async (e) => {
    e.preventDefault();
    let errors = {};

    if (
      userData.firstName.length > 15 ||
      userData.lastName.length > 15 ||
      userData.firstName.length < 3 ||
      userData.lastName.length < 3
    ) {
      errors.errName =
        "First name and/or must be between 3 and 15 characters in length.";
    }
    if (!userData.gender) {
      errors.errGender = "Gender must not be empty.";
    }
    if (!userData.status) {
      errors.errStatus = "Status must not be empty.";
    }

    setErrors(errors);

    if (Object.keys(errors).length > 0) {
      return;
    }
    setDisableBtn(true);
    let url;
    if (preview) {
      const imageRef = storageRef(storage, `images/${Date.now()}`);
      const snapshot = await uploadBytes(imageRef, userData.image);
      url = await getDownloadURL(snapshot.ref);
    }

    const userToUpdate = {
      firstName: userData.firstName,
      lastName: userData.lastName,
      email: userData.email,
      position: userData.position,
      gender: userData.gender,
      birthDate: userData.birthDate,
      image: url || userData.image,
      status: userData.status,
      isAdmin: userData.isAdmin === "Admin" ? 1 : 0,
    };
    const res = await fetch(
      `http://localhost:8000/api/edit-user/${userData.id}`,
      {
        credentials: "include",
        method: "PUT",
        body: JSON.stringify(userToUpdate),
        headers: { "Content-Type": "application/json" },
      }
    );
    const data = await res.json();
    if (data.status === 401) {
      dispatch({ type: "LOGOUT" });
      navigate("/login");
    }
    if (data.status === 400) {
      setDisableBtn(false);
      setErrUser(data.message);
      return false;
    }
    console.log("success");
  };
}

Solution

  • IIUC, the Parent Component fetches a list of users (from Backend), and holds a userData state to edit one of these users when needed. This userData state is shared with the Child Component, which displays the edit form and handles the request to Backend to save the updated user data when the form is submitted.

    But the list in the Parent does not reflect that change for that user, because the displayed data (users) is not the state (user) that has been modified.

    In that case, you can display the updated user in several ways, depending on how your Parent JSX is structured and how it calls the Child.

    Given the code you share, one of the solutions could be to re-fetch the entire list of users after the update request succeeds. The added advantage is that you are sure to retrieve what the Backend actually stored (it may sanitize / reject some updates):

    // PARENT
    // Build the fetch function separately, so that it can be called imperatively
    // once the update request succeeds
    const getUsers = useCallback(async () => {
      const res = await fetch("http://localhost:8000/api/users", {
        credentials: "include",
      });
    
      const data = await res.json();
      if (data.status === 401) {
        dispatch({ type: "LOGOUT" });
        navigate("/login");
      }
      setUsers(data.users);
    }, [dispatch, navigate]);
    
    // Fetch users list initially 
    useEffect(() => {
      getUsers();
    }, []);
    
    return <UpdateUser
      onUpdateSuccess={getUsers} // Pass the fetch function as callback
      // Other props
    />;
    
    // CHILD (UpdateUser)
    function UpdateUser({
      onUpdateSuccess,
      // Other props
    }) {
      const editUser = async (e) => {
        // Prepare data, send request to Backend...
        console.log("success");
        // Fire the callback
        onUpdateSuccess?.();
      };
    }