skip to Main Content

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


  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, user?.id]);

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

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

  const editUser = (user) => {
    setUserData(user);
  };

Child component

function UpdateUser({ userData, editditUser }) {


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

    editUser({ ...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;

    editUser(_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");
  };
}

2

Answers


  1. 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?.();
      };
    }
    
    Login or Signup to reply.
  2. When you ask something like this, you should include a Minimal reproducible example, leaving behind all the variables that are not necessary to the problem (e.g. navigate function, or dispatch function)

    Since you haven’t included the complete components, i have to make a supposition here: when you call your UpdateUser component you are passing userData and the editUser as props. In this case, there a 2 errors. First, you need to include userData in your useEffect dependencies array, otherwise it will not update when userData changes. Then, in your UpdateUser component you never call the editditUser function passed as prop. Here there’s a minimal code to do what you want

    Parent component:

    "use client"
    
    import {useEffect, useState} from "react";
    import {UpdateUser} from "./child";
    
    export type User = {
        name?: string,
        id?: string
    }
    
    export const Parent = () => {
        const [user, setUser] = useState<User>({})
    
        useEffect(() => {
            const fetchAllUsers = () => {
                // your logic here
            }
            fetchAllUsers()
        }, [user]);
    
        return (
            <div>
                <p>Name: <span className="font-bold">{user?.name}</span></p>
                <p>ID: <span className="font-bold">{user?.id}</span></p>
                <UpdateUser user={user} setUser={setUser} />
            </div>
        )
    }
    
    

    Child component:

    import {User} from "./parent";
    import {Dispatch, SetStateAction, useRef} from "react";
    
    export const UpdateUser = ({ user, setUser }: {user: User, setUser: Dispatch<SetStateAction<User>>}) => {
        const ref = useRef<HTMLInputElement>(null)
    
        const updateName = () => {
            setUser({...user, name: ref.current?.value})
        }
    
        return (
            <>
                <input ref={ref} type="text" placeholder="new name" />
                <button onClick={updateName}>Update name</button>
            </>
        )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search