Home > Software engineering >  Why can't I pass a prop from useState to another component?
Why can't I pass a prop from useState to another component?

Time:02-03

So I have an app which gets an API from Movie API, and I have a hero component, it gets movies array as props and when from the hero component I want to pass a random movie into the Poster component(which displays one random new movie as a poster).

Hero component:

const Hero = ({movies}: MovieProps) => {

    const movie_random = movies[Math.floor(Math.random() * movies.length)];
    const [movie, setMovie] = useState<MovieType>(movie_random);

    useEffect(() =>{
        setMovie(movie_random);
        console.log(movie);
    },[])

  return (
    <div className="hero">
        <Poster movie={movie}/>
        <Container movies={movies}/>
    </div>
  );
};

export default Hero;

Poster component:

const Poster = ({ movie }: { movie: MovieType }) => {
  return (
    <div className="poster">
        <div className="desc">
            <span className="title">{movie.title}</span>
        </div>
        <div className="image">

        </div>
    </div>
  );
};

And also the interfaces:

export type MovieType = {
        vote_average: string,
        title: string,
        tagline: string,
        date: string,
        poster_path: string,
    };

export type MovieResults = {
    results: MovieType[],
};


export interface MovieProps {
     movies: MovieType[],
      };

I guess it should be working, but I have the next error: Uncaught TypeError: can't access property "title", movie is undefined in the Poster component. So I think a single movie isn't passed as a prop to the Poster, but I can't figure out why.

Edit: My App component looks like this, I pass movies array from the API:

const App = () => {

  const [movies, setMovies] = useState<MovieType[]>([]);

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

  async function fetchMovies() {
    try{
      let apikey = 'api_key';
      let url: string = 'https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=';
      url = url   apikey;
      const response = await axios.get<MovieResults>(url);
      setMovies(response.data.results);
      console.log(response.data.results);
    }catch(e){
      alert(e);
    }
}

  return (
    <div className="App">
      <Header/>
      <Hero movies={movies}/>
    </div>
  );
}

Edit 2: so I did as the first answer suggested and I got an error:

<Poster movie={movie}/> Type 'MovieType | undefined' is not assignable to type 'MovieType'.
  Type 'undefined' is not assignable to type 'MovieType'

I tried to add default object properties in setState:

const [movie, setMovie] = useState<MovieType>({vote_average: '', title: '', tagline: '', date: '', poster_path: ''});

And I got an error:

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function

CodePudding user response:

I would place the randomiser in useEffect and pass movies as a parameter. So every time movies changes, the effect runs.

Note that you can't put console.log(movie) right after setMovie() because you won't know exactly when the state has changed.

const Hero = ({movies}: MovieProps) => {

    const [movie, setMovie] = useState<MovieType>();

    useEffect(() =>{
        const movie_random = movies[Math.floor(Math.random() * movies.length)];
        console.log(movie_random);
        setMovie(movie_random);
    },[movies])

  return (
    <div className="hero">
        {movie && <Poster movie={movie} />}
    </div>
  );
};

export default Hero;

Initializing movie as an empty state object here. It will change once useEffect has run.

CodePudding user response:

Your movies state in App is initially an empty array:

const [movies, setMovies] = useState<MovieType[]>([]); // Initial value []

So in Hero the movie_random is undefined:

// movies.length is 0
// Math.floor(Math.random() * movies.length) is therefore 0
// movies[0] is undefined
const movie_random = movies[Math.floor(Math.random() * movies.length)];

which leads to your error message in Poster when it tries to read a property of undefined:

movie.title

As suggested in comments and answer, the normal way to avoid such errors is simply to conditionally render the child component to avoid edge cases when the data is empty:

const App = () => {

  const [movies, setMovies] = useState<MovieType[]>([]);

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

  // etc.

  return (
    <div className="App">
      <Header/>
      {
        // Show Hero component only when there are some movies
        !!(movies?.length) && <Hero movies={movies}/>
      }
    </div>
  );
}

Of course you can also continue showing Hero even with empty data, and handle that case within that component (typically if you still want to display some other data from Hero). E.g. you could check for movies.length within Hero before setting its movie state, and conditionally render the Poster in the same way.

As for your last error message ("Can't perform a React state update on an unmounted component." etc.), it is very probably unrelated to the current issue. You probably just have navigated/reloaded the page/component while the server request was still pending, and when it finally got its response, your initial App component was no longer there / had changed.

  •  Tags:  
  • Related