Passing a function as a property to two components returns undefined in the target component
P粉818125805
P粉818125805 2024-02-26 10:15:16
0
1
428

import React, { useContext, useState } from 'react';
import Main from '../components/Main';
import Row from '../components/Row';
import requests from '../requests';
import AppContext from '../lib/AuthContext';

const Home = () => {

  const [likedItems, setLikedItems] = useState([]);

  const contextValue = useContext(AppContext);
  const token = window.localStorage.getItem('trailerflix-jwt');

  const handleNewLikes = item => {
    setLikedItems(prevLikedItems => [...prevLikedItems, item]);
  };

  if (token && contextValue.user?.user) {
    return (
      <>
        <Main handleNewLikes={handleNewLikes} />
        <Row rowId='0' title={`${(token && contextValue.user?.user) ? contextValue?.user?.user.username : null}'s List`} fetchURL='/auth/get-likes'
        likedItems={likedItems} handleNewLikes={handleNewLikes} />
        <Row rowId='1' title='Top 10 Movies in the U.S. Today' fetchURL={requests.popular} />
        <Row rowId='2' title='Coming Soon' fetchURL={requests.upcoming} />
        <Row rowId='3' title='Trending Now' fetchURL={requests.trending} />
        <Row rowId='4' title='Now Playing in Theaters' fetchURL={requests.nowPlaying} />
        <Row rowId='5' title='Animation' fetchURL={requests.animation} />
        <Row rowId='6' title='Horror' fetchURL={requests.horror} />
        <Row rowId='7' title='Comedy' fetchURL={requests.comedy} />
      </>
    );
  } else {
    return (
      <>
        <Main />
        <Row rowId='1' title='Top 10 Movies in the U.S. Today' fetchURL={requests.popular} />
        <Row rowId='2' title='Coming Soon' fetchURL={requests.upcoming} />
        <Row rowId='3' title='Trending Now' fetchURL={requests.trending} />
        <Row rowId='4' title='Now Playing in Theaters' fetchURL={requests.nowPlaying} />
        <Row rowId='5' title='Animation' fetchURL={requests.animation} />
        <Row rowId='6' title='Horror' fetchURL={requests.horror} />
        <Row rowId='7' title='Comedy' fetchURL={requests.comedy} />
      </>
    );
  }

};

export default Home;

So this is the parent component and from here I pass the handleNewLikes function as prop to Row.

import axios from 'axios';
import React, { useEffect, useState, useRef } from 'react';
import Media from './Media';
import { MdChevronLeft, MdChevronRight } from 'react-icons/md';

const Row = ({ title, fetchURL, rowId, videos, likedItems, handleNewLikes }) => {

  const [media, setMedia] = useState([]);

  useEffect(() => {
    const token = window.localStorage.getItem('trailerflix-jwt');
    if (token && fetchURL === '/auth/get-likes') {
      axios.get(fetchURL, {
        headers: {
          'Content-Type': 'application/json',
          'X-Access-Token': `${token}`
        }
      })
        .then(res => {
          const flattenedArray = res.data.map(item => item.favoritedItem);
          const newestLikesFirst = flattenedArray.reverse();
          setMedia(newestLikesFirst);
        })
        .catch(error => {
          console.error('Axios GET request failed:', error);
        });
    } else if (fetchURL !== '/auth/get-likes') {
      axios.get(fetchURL)
        .then(response => {
          setMedia(response.data.results);
        })
        .catch(error => {
          console.error('Axios GET request failed:', error);
        });
    }
  }, [fetchURL, likedItems]);

  const validMedia = [];

  for (let i = 0; i < media.length; i++) {
    if (media[i].backdrop_path !== null) {
      validMedia.push(media[i]);
    } else {
      media.splice(i, 1);
    }
  }

  const rowRef = useRef(null);
  const [isMoved, setIsMoved] = useState(false);

  const handleSlider = direction => {
    setIsMoved(true);

    if (rowRef.current) {
      const { scrollLeft, clientWidth } = rowRef.current;

      const scrollTo =
        direction === 'left' ? scrollLeft - clientWidth : scrollLeft + clientWidth;

      rowRef.current.scrollTo({ left: scrollTo, behavior: 'smooth' });
    }

  };

  return (

    <>
      <h2 className='text-white font-bold md:text-2xl p-4 mt-8 mb-3 ml-4'>{title}</h2>
      <div className='relative flex items-center group mb-10 ml-4'>

        <MdChevronLeft className={`text-white bg-transparent left-0 absolute hover:opacity-100 cursor-pointer z-10 hidden invisible lg:visible md:visible group-hover:block ${!isMoved && ' lg:invisible md:invisible'}`} size={60}
          onClick={() => handleSlider('left')}
        />
        <div id={'slider' + rowId} className='w-full h-full overflow-x-scroll whitespace-nowrap scroll-smooth relative scrollbar-hide overflow-y-hidden' ref={rowRef}>
          {validMedia.map((item, id) => {
            return <Media key={id} item={item} rowId={rowId} handleNewLikes={handleNewLikes} likedItems={likedItems}/>;
          })}
        </div>
        <MdChevronRight className='text-white bg-transparent right-0 absolute hover:opacity-100 cursor-pointer z-10 hidden invisible lg:visible md:visible group-hover:block' size={60}
          onClick={() => handleSlider('right')}
        />
      </div>
    </>
  );
};

export default Row;

This is the row component where I pass the handleNewLikes function to the media.

import React, { useState, useContext, useEffect } from 'react';
import axios from 'axios';
import { FaHeart, FaRegHeart } from 'react-icons/fa';
import Player from './Player';
import AppContext from '../lib/AuthContext';

const Media = ({ item, rowId, handleNewLikes, likedItems }) => {
  const contextValue = useContext(AppContext);

  const [watchClicked, setWatchClicked] = useState(false);
  const [isLiked, setIsLiked] = useState(false);
  const [key, setKey] = useState('');
  const [playTrailer, setPlayTrailer] = useState(false);
  const [noTrailer, setNoTrailer] = useState(false);

  useEffect(() => {
    handleFavoritesList();
  }, []);

  console.log(handleNewLikes);

  const handleTrailerClick = () => {
    axios
      .get(`https://api.themoviedb.org/3/movie/${item.id}?api_key=${process.env.MOVIEDB_API_KEY}&append_to_response=videos`)
      .then(response => {
        const trailer = response.data.videos.results.find(vid => vid.name === 'Official Trailer');
        if (response.data.videos.results.length === 0) {
          setKey('');
          setPlayTrailer(false);
          setNoTrailer(true);
          document.body.style.overflowY = 'hidden';
        } else if (trailer) {
          setKey(trailer);
          setPlayTrailer(true);
        } else if (!trailer) {
          setKey(response.data.videos.results[0]);
          setPlayTrailer(true);
        }
      });
    setWatchClicked(true);
    document.body.style.overflowY = 'hidden';
  };

  const handleLikes = () => {
    const token = window.localStorage.getItem('trailerflix-jwt');
    if (token && contextValue?.user?.user) {
      axios.post('/auth/likes', item, {
        headers: {
          'Content-Type': 'application/json',
          'X-Access-Token': token
        }
      })
        .then(response => {
          console.log(handleNewLikes);
          handleNewLikes(item);
          setIsLiked(true);
        })
        .catch(error => {
          console.error('Fetch failed during POST', error);
        });
    } else {
      window.alert('You need to be signed in to save a movie!');
    }
  };

  const handleFavoritesList = () => {
    const token = window.localStorage.getItem('trailerflix-jwt');
    if (token && contextValue.user?.user) {
      axios.get('/auth/get-likes', {
        headers: {
          'Content-Type': 'application/json',
          'X-Access-Token': token
        }
      })
        .then(response => {
          const result = response.data;
          const isItemLiked = result.some(obj => obj.favoritedItem.id === item.id);
          setIsLiked(isItemLiked);
        })
        .catch(error => {
          console.error('Fetch failed during GET', error);
        });
    } else {
      setIsLiked(false);
    }
  };

  const truncateString = (str, num) => {
    if (str?.length > num) {
      return str.slice(0, num) + '...';
    } else {
      return str;
    }
  };

  const { title, original_title, media_type, name } = item;

  return (
    <>
      {watchClicked && (
        <Player trailer={key} playTrailer={playTrailer} noTrailer={noTrailer} onClose={() => setWatchClicked(false)} />
      )}
      <div className="w-[200px] sm:w-[300px] lg:w-[400px] inline-block cursor-pointer relative transition duration-200 ease-out p-2 lg:mr-1 sm:mr-2 md:hover:scale-105">
        <img
          className="w-full h-auto block rounded-sm object-cover md:rounded"
          src={`https://image.tmdb.org/t/p/w500/${rowId === '1' || rowId === '4' ? item?.poster_path : item?.backdrop_path}`}
          alt={title || original_title || name || media_type || 'Title Unavailable'}
        />
        <div className="absolute top-0 left-0 w-full h-full hover:bg-black/80 opacity-0 hover:opacity-100 ease-in duration-300 text-white">
          <div className="white-space-normal text-xs md:text-sm lg:text-base font-bold flex justify-center items-center text-center h-full">
            <div className="flex-wrap">
              <p className="mb-2">
                {truncateString(title || original_title || name || media_type || 'Title Unavailable', 35)}
              </p>
              <div>
                <a
                  className="border bg-gray-300 text-black border-gray-300 py-1 px-1 text-xs lg:text-base hover:bg-red-600 hover:border-red-600 hover:text-gray-300 ease-in duration-250"
                  onClick={handleTrailerClick}
                >
                  Watch
                </a>
              </div>
            </div>
          </div>
          <p onClick={() => handleLikes()}>
            {isLiked && contextValue.user?.user
              ? (
                <FaHeart className="absolute top-4 left-4 text-red-600" />
                )
              : (
                <FaRegHeart className="absolute top-4 left-4 hover:text-red-600 ease-in duration-100" />
                )}
          </p>
        </div>
      </div>
    </>
  );
};

export default Media;

In media, the function is sometimes defined in the console and sometimes undefined. Also, when I click on the heart icon and the handleLikes function is triggered, the handleNewLikes function within it will always appear as undefined.

This is what I see in the console

Thank you for your help, I've been stuck for a few days lmao

I know I could probably use some kind of state management library, but that would require quite a bit of refactoring, and I feel like this should work, and I don't see why there would be any problems, unless I'm shortsighted.

P粉818125805
P粉818125805

reply all(1)
P粉762730205

I copied your code and added some logs to help you see what's going on. You only passed handleNewLikes from Home -> Main and Home -> Row 0.

The rest of the rows' handleNewLikes is correctly undefined because you didn't pass this function. Check out these logs to see what I mean:

When we get to the Media component, you try to use an handleNewLikes function of undefined when calling handleLikes.

// Media.tsx line 48
  const handleLikes = () => {
    const token = window.localStorage.getItem("trailerflix-jwt");
    if (token && contextValue?.user?.user) {
      axios
        .post("/auth/likes", item, {
          headers: {
            "Content-Type": "application/json",
            "X-Access-Token": token
          }
        })
        .then((response) => {
          console.log(handleNewLikes);
          // Undefined because not passed in to Row which passes to Media
          handleNewLikes(item);
          setIsLiked(true);
        })
        .catch((error) => {
          console.error("Fetch failed during POST", error);
        });
    } else {
      window.alert("You need to be signed in to save a movie!");
    }
  };

This is some logs when clicking the like button in the Media component:

The simplest solution is to update the parameters of the Row implementation in Home, including handleNewLikes:

<Row
  rowId='1'
  title='Top 10 Movies in the U.S. Today'
  fetchURL={requests.popular}
  handleNewLikes={handleNewLikes}
/>
//Same applies to row 2, 3, etc...

This is the log of clicking the like button after passing the function to Row 1

^ You should use some kind of state management. You don't need to use a library to do this. If the state is not very complex, you can use a React context similar to what you did in AuthContext.

Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template