Search code examples
reactjstypescriptnext.jsreact-query

Unable to access the props of fetched data with useQuery


"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { IUser } from "@/interfaces/database.interfaces";
import { UserType } from "@/interfaces/userpage.interfaces";
import { useQuery } from 'react-query';
import { Types } from "mongoose";
import RoleSelector from "@/components/User Page/RoleSelector";

async function getUsers(): Promise<UserType[] | undefined> {
  const res = await fetch('/api/users', {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  });
  const data = await res.json();
  return data;
}

async function getUserDetails(id:Types.ObjectId):Promise<IUser | undefined>{
  const res = await fetch(`/api/users/${id}`,{
    method: 'GET',
    headers:{
      'Content-Type':'application/json'
    }
  });
  if (!res.ok) {
    return undefined;
  }
  const data = await res.json();
  return data;
}

const User2 = () => {
  // get User 
  const roles = ['User', 'Maintainer', 'Admin'];
  const { data:Users, status } = useQuery('users', getUsers);
  const [openUserId, setOpenUserId] = useState<Types.ObjectId | null>(null);

  const handleToggle = (userId: Types.ObjectId) => {
    if (openUserId !== userId) {
      setOpenUserId(userId);
    } else if (openUserId === userId) {
      setOpenUserId(null);
    }
  };



  if (status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'error') {
    return <div>Error fetching data</div>;
  }

  
  // let's not use hooks for now

  // Pagination

  // Search somehow?

  // Filter somehow?

  // Define Roles here

  // Make sure: Less definitions in the loops

  return (
    <div className="background background-light900_dark300 h-auto">
      { /* Restricted Access component */}
      <h1 className="h1-bold text-dark100_light900">Users</h1>
      {/* <UserSearch /> */}
      <section className="mt-7 border border-gray-200 p-4 shadow-md shadow-gray-300 dark:border-dark-400 dark:shadow dark:shadow-gray-500">
        {
          Users?.map((user) => {
            return (
              <div key={`${user._id}`} className="border-b-2 border-gray-300">
                <div  className="flex space-x-40 p-4 max-mmd:items-start" onClick={()=> handleToggle(user._id)}> 
                 <div className="flex flex-1 max-mmd:flex-col">
                  <p className="text-dark500_light700 font-medium">
                    {user.display_name}
                  </p>
                  <p className="user-info text-dark500_light700 text-right font-medium max-mmd:text-left">
                    {user.email}
                  </p>
                </div>
                <motion.span
                  className={`${openUserId === user._id ? "rotate-180" : "rotate-0"} max-xs:hidden`}
                  initial={{ rotate: 0 }}
                  animate={{ rotate: openUserId === user._id ? 180 : 0 }}
                  transition={{ duration: 0.2 }}
                >
                  <svg
                    className="size-5 text-gray-400"
                    viewBox="0 0 20 20"
                    fill="currentColor"
                    aria-hidden="true"
                  >
                    <path
                      fillRule="evenodd"
                      d="M5.293 9.293a1 1 0 011.414 0L10 12.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
                      clipRule="evenodd"
                    />
                  </svg>
                </motion.span>
                </div>
                <AnimatePresence>
                  {
                    
                    openUserId === user._id &&
                    // fetch some data first and than render the component
                    (
                      <UserDetails userId ={user._id} roles={roles}/>
                    )
                  }
                </AnimatePresence>
              </div>
            );
          })
        }
      </section>
      {/* <UserPagination /> */}
    </div>
  );
};

const UserDetails = ({userId,roles}:{userId:Types.ObjectId,roles:string[]}) => {
  const { data, status } = useQuery(['DetailsUser', userId], () => getUserDetails(userId), {
    enabled: !!userId,
    staleTime: Infinity, // Cache the data indefinitely
  });

  if (status === 'loading') {
    return <div>Loading...</div>;
  }
  if(status === 'error'){
    return <div>Error fetching data</div>;
  }
  console.log(data);
  console.log(data?.role)
  
  return(
   <motion.div
   initial="collapsed"
   animate="open"
   exit="collapsed"
   variants={{
     open: { opacity: 1, height: "auto" },
     collapsed: { opacity: 0, height: 0 },
   }}
   transition={{ duration: 0.4, ease: "easeInOut" }}
   className="relative overflow-hidden">
<div className="relative grid grid-cols-1 justify-items-start px-4 pb-7 md:items-baseline mmd:grid-cols-2 mmd:grid-rows-1">
        <div className="absolute left-3 top-6">
           <RoleSelector userId={data?._id} userRole={data?.role} roles={roles} />
        </div>
        <button
          className="absolute bottom-5 right-10 rounded bg-red-600 px-3 py-1 font-bold text-white hover:bg-red-700 md:px-4 md:py-2 xl:px-7 xl:py-2"
        >
          Delete
        </button>
        <div className="">
          <div className="mt-20">
            <h4 className="text-dark100_light900">Card number</h4>
            <p className="font-extralight text-gray-400 dark:text-gray-600">
              {data?.card_number}
            </p>
          </div>
          <div className="mb-5 mt-6">
            <h4 className="text-dark100_light900">Card ID</h4>
            <p className="font-extralight text-gray-400 dark:text-gray-600">
              {data?.card_id}
            </p>
          </div>
        </div>
      </div>
   </motion.div>
  )

}
export default User2;

The above code is rendering a users page of a NextJS application. The issue lies within the UserDetails commponent function, which is fetching a user from the database, based on their user id. After fetching it is unable to access the props of the fetched data.

The data that I am trying to fetch in the UserDetails component is based on the following interface:

  export interface IUser {
    _id:Types.ObjectId,
    azure_id: string;
    email: string;
    first_name: string;
    last_name: string;
    display_name: string;
    card_id: string;
    card_number: string;
    role: string;
    permissions?: Types.ObjectId[];
  }

When running console log for debugging:

console.log(data);
console.log(data?.role)

I get the following response:

Response of the console log


Solution

  • I see a log where data is defined, and then a log correctly showing data.role as undefined. data has user and message properties. I assume you meant to log data.user.role instead. It looks like all the properties you are trying to access are on data.user and not on the root data reference. Based on the getUserDetails return type it appears you should "unpack"/return data.user from getUserDetails.

    async function getUserDetails(id: Types.ObjectId): Promise<IUser | undefined>{
      try {
        const res = await fetch(`/api/users/${id}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json'
          }
        });
        const { user } = await res.json();
        return user; // <-- return the res.data.user IUser object
      } catch {
        return undefined;
      }
    }