Search code examples
reactjsreact-reduxredux-toolkitrtk-queryreact-forms

Data not being passed down as a prop in React


So for some context, I am building a small website using the mern stack (React, Redux toolkit, Mongo, Express) for my father and his dealership. I am running into a problem on the front end with regards to my edit user form. currently I have not configured jwt tokens or any protected routes, that will be coming in the future. however I have created the ability to create user account detail form which is sent to the backend and is processed. This ties into my problem which is the edit user form.

When at the /users in the url on my frontend you are presented with a page with a list of all the users and their specific details. I also have an edit button which should, once clicked, put you to the edit user form page so that you can edit that users details. This is where I have run into my problem. When I click on edit on someone with a dealership role (there are three roles, dealership, admin and customer) you can edit their details no problem, it works the logic is correct and does everything as expected. when you try to do the same with anyone with either the admin or customer role it does not work it throws an error which you will see in the images below. Im guessing this error is due to the data not being passed down correctly, hence reading undefined. some of the below images are blurred to hide sensitive information of users.

users list page when you click on edit for someone with the dealership role it works just fine as the below image shows. User with dealership role on edit user form Now when you click on edit on someone with any other role you get the below error. Error on Non dealership roles

Here is the code for the EditUser.js

import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectUserById } from "./usersApiSlice";
import EditUserForm from "./EditUserForm";

const EditUser = () => {
  const { id } = useParams();

  const user = useSelector((state) => selectUserById(state, id));

  const content = user ? <EditUserForm user={user} /> : <p>Loading...</p>;

  return content;
};

export default EditUser;

Here is the code for EditUserForm.js

import { useState, useEffect } from "react";
import { useUpdateUserMutation, useDeleteUserMutation } from "./usersApiSlice";
import { useNavigate } from "react-router-dom";
import { ROLES } from "../../config/roles";

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const PWD_REGEX = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W).{8,}$/;

const EditUserForm = ({ user }) => {
  const [updateUser, { isLoading, isSuccess, isError, error }] =
    useUpdateUserMutation();

  const [
    deleteUser,
    { isSuccess: isDelSuccess, isError: isDelError, error: delerror },
  ] = useDeleteUserMutation();

  const navigate = useNavigate();

  const [email, setEmail] = useState(user.email);
  const [validEmail, setValidEmail] = useState(false);
  const [password, setPassword] = useState("");
  const [validPassword, setValidPassword] = useState(false);
  const [firstname, setFirstname] = useState(user.firstname);
  const [lastname, setLastname] = useState(user.lastname);
  const [telephone, setTelephone] = useState(user.telephone);
  const [roles, setRoles] = useState(user.roles);
  const [profilePicture, setProfilePicture] = useState(user.profilePicture);
  const [companyName, setCompanyName] = useState(user.companyName);

  useEffect(() => {
    if (isSuccess || isDelSuccess) {
      setEmail("");
      setPassword("");
      setRoles([]);
      setFirstname("");
      setLastname("");
      setTelephone("");
      setProfilePicture("");
      setCompanyName("");
      navigate("/dashboard/settings");
    }
  }, [isSuccess, isDelSuccess, navigate]);

  useEffect(() => {
    setValidEmail(EMAIL_REGEX.test(email));
  }, [email]);

  useEffect(() => {
    setValidPassword(PWD_REGEX.test(password));
  }, [password]);

  const onEmailChanged = (e) => setEmail(e.target.value);
  const onPasswordChanged = (e) => setPassword(e.target.value);

  const onRolesChanged = (e) => {
    const values = Array.from(
      e.target.selectedOptions, //HTMLCollection
      (option) => option.value
    );
    setRoles(values);
  };

  const onFirstnameChanged = (e) => setFirstname(e.target.value);
  const onLastnameChanged = (e) => setLastname(e.target.value);
  const onTelephoneChanged = (e) => setTelephone(e.target.value);
  const onProfilePictureChanged = (e) => setProfilePicture(e.target.value);
  const onCompanyNameChanged = (e) => setCompanyName(e.target.value);
    }));
  };

  const onSaveUserClicked = async (e) => {
    if (password) {
      await updateUser({
        id: user.id,
        email,
        password,
        roles,
        firstname,
        lastname,
        telephone,
        profilePicture,
      });
    } else {
      await updateUser({
        id: user.id,
        email,
        roles,
        firstname,
        lastname,
        telephone,
        profilePicture,
        companyName,
      });
    }
  };

  const onDeleteUserClicked = async () => {
    await deleteUser({ id: user.id });
  };

  const options = Object.values(ROLES).map((role) => {
    return (
      <option key={role} value={role}>
        {" "}
        {role}
      </option>
    );
  });

  let canSave;

  if (password) {
    canSave =
      roles.length > 0 &&
      password &&
      validEmail &&
      validPassword &&
      firstname.trim().length > 0 &&
      lastname.trim().length > 0 &&
      (telephone.trim().length === 0 || /^[0-9]+$/.test(telephone)) &&
      !isLoading;
  } else {
    canSave =
      roles.length > 0 &&
      validEmail &&
      firstname.trim().length > 0 &&
      lastname.trim().length > 0 &&
      (telephone.trim().length === 0 || /^[0-9]+$/.test(telephone)) &&
      !isLoading;
  }

  const errClass = isError || isDelError ? "errmsg" : "offscreen";
  const validEmailClass = validEmail ? "" : "form__input--incomplete";
  const validPwdClass = validPassword ? "" : "form__input--incomplete";
  const validRolesClass = roles.length > 0 ? "" : "form__input--incomplete";
  const validFirstNameClass =
    firstname.trim().length > 0 ? "" : "form__input--incomplete";
  const validLastNameClass =
    lastname.trim().length > 0 ? "" : "form__input--incomplete";
  const validTelephoneClass =
    telephone.trim().length === 0 || /^[0-9]+$/.test(telephone)
      ? ""
      : "form__input--incomplete";
  const validCompanyNameClass =
    !Object.values(ROLES).includes("Dealership") ||
    companyName.trim().length > 0
      ? ""
      : "form__input--incomplete";
  const validProfilePictureClass =
    profilePicture.trim().length > 0 ? "" : "form__input--incomplete";

  const errContent = (error?.data?.message || delerror?.data?.message) ?? "";

  const content = (
    <>
      <p className={errClass}>{errContent}</p>

      <form className="editUserForm" onSubmit={onSaveUserClicked}>
        <h2 className="signUp">EditUserForm</h2>

        <div className="inputFields">
          <label className="form__label--visually-hidden" htmlFor="email">
            Email: <span className="nowrap">[Must include @]</span>
          </label>
          <input
            className={`form__input ${validEmailClass}`}
            id="email"
            name="email"
            type="text"
            autoComplete="off"
            value={email}
            onChange={onEmailChanged}
            onFocus={(e) => {
              if (e.target.value === "Email") {
                e.target.value = "";
              }
            }}
            onBlur={(e) => {
              if (e.target.value === "") {
                e.target.value = "Email";
              }
            }}
            placeholder="Email"
          />

         <label className="form__label--visually-hidden" htmlFor="roles">
            ASSIGNED ROLES:
          </label>
          <select
            id="roles"
            name="roles"
            className={`form__select ${validRolesClass}`}
            multiple={true}
            size="3"
            value={roles}
            onChange={onRolesChanged}
          >
            {options}
          </select>

        
        <div className="form__action-buttons">
          <button
            className="icon-button"
            title="Save"
            disabled={!canSave}
            onClick={onSaveUserClicked}
          >
            Save
          </button>
          <button
            className="icon-button"
            title="delete"
            onClick={onDeleteUserClicked}
          >
            delete
          </button>
        </div>
      </form>
    </>
  );

  return content;
};

export default EditUserForm;

I have removed part of the form as it takes up a lot of space, most of the fields are like the email field above. Here is my roles.js file too.

export const ROLES = {
  Customer: "Customer",
  Dealership: "Dealership",
  Admin: "Admin",
};

If there are any other files you would like to see please let me know such as the usersApiSlice.js, store.js etc. if you would like to see file tree please do let me know. I would like to mention I have tested my backend with postman with all crud operations. I also do not have errors on my error middleware loggers. It seems like the backend is not the issue but rather the front end. I appreciate if anyone has taken the time to read this.

here is my store.js

import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";
import { setupListeners } from "@reduxjs/toolkit/dist/query";

export const store = configureStore({
  reducer: {
    [apiSlice.reducerPath]: apiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
  devTools: true,
});

setupListeners(store.dispatch);

Here is my usersApiSlice.js

import { createSelector, createEntityAdapter } from     "@reduxjs/toolkit";
import { apiSlice } from "../../app/api/apiSlice";

const usersAdapter = createEntityAdapter({});

const initialState = usersAdapter.getInitialState();

export const usersApiSlice = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => "/users",
      validateStatus: (response, result) => {
        return response.status === 200 && !result.isError;
      },
      transformResponse: (responseData) => {
        const loadedUsers = responseData.map((user) => {
          user.id = user._id;
          return user;
        });
        return usersAdapter.setAll(initialState, loadedUsers);
      },
      providesTags: (result, error, arg) => {
        if (result?.ids) {
          return [
            { type: "User", id: "LIST" },
            ...result.ids.map((id) => ({ type: "User", id })),
          ];
        } else return [{ type: "User", id: "LIST" }];
      },
    }),
    addNewUser: builder.mutation({
      query: (initialUserData) => ({
        url: "/users",
        method: "POST",
        body: {
          ...initialUserData,
        },
      }),
      invalidatesTags: [{ type: "User", id: "LIST" }],
    }),
    updateUser: builder.mutation({
      query: (initialUserData) => ({
        url: "/users",
        method: "PATCH",
        body: {
          ...initialUserData,
        },
      }),
      invalidatesTags: (result, error, arg) => [{ type: "User", id: arg.id }],
    }),
    deleteUser: builder.mutation({
      query: ({ id }) => ({
        url: `/users`,
        method: "DELETE",
        body: { id },
      }),
      invalidatesTags: (result, error, arg) => [{ type: "User", id: arg.id }],
    }),
  }),
});

export const {
  useGetUsersQuery,
  useAddNewUserMutation,
  useUpdateUserMutation,
  useDeleteUserMutation,
} = usersApiSlice;

// returns the query result object
export const selectUsersResult =   usersApiSlice.endpoints.getUsers.select();

// creates memoized selector
const selectUsersData = createSelector(
  selectUsersResult,
  (usersResult) => usersResult.data 
// normalized state object with ids & entities
);

//getSelectors creates these selectors and we rename them
 with aliases using destructuring
export const {
  selectAll: selectAllUsers,
  selectById: selectUserById,
  selectIds: selectUserIds,
  // Pass in a selector that returns the users slice of state
} = usersAdapter.getSelectors(
  (state) => selectUsersData(state) ?? initialState
);

I tried things such as testing my backend, googling the error and my problem and things of that sort of nature. I even tried using ai to help but any solutions provided did not work.


Solution

  • I was receiving undefined because the form was a one size fits all for all users, when users with different roles have extra bits of data fields. this meant that the form was reading undefined on certain parts such as companyName because the customer doesn't have that. this explains why the form was only working on the dealership role and not any other role because data that was not there was trying to be accessed hence reading undefined. Consider having separate forms for different roles and pass the the user down to these forms conditionally

      const content = user ? (
    user.role === "dealership" ? (
      <EditDealerForm user={user} />
    ) : (
      <EditUserForm user={user} />
    )
    

    ) : (

    Loading...

    );