0

Currently trying to render data that is fetched from an URL. It works fine if I console.log it in the file itself. But now I want to call the fetched data on the page component, so I can render it as TSX.

This is the API file where I fetch the data, called api.ts:

export const getVideoInfo = async () => {
  try {
    const getVideos = await fetch(
      "url"
    );
    const videos = await getVideos.json();
    return videos;
  } catch (e) {
    console.log(e);
  }
};

Then there is another file (here I tried to find a matched with the hash in the url, called useCourseDetail.ts:

import { useLocation } from "react-router";
import { getVideoInfo } from "../../api/Api";
import { Slugify } from "../../components/Util/Slugify";

export const FindCourseDetail = async () => {
  const { hash } = useLocation();
  const slugifiedHash = Slugify(hash);
  const data = await getVideoInfo();


  if (hash) {
    const video = data.videos.find(
      (v) =>
        Slugify(v.title, {
          lowerCase: true,
          replaceDot: "-",
          replaceAmpersand: "and",
        }) === slugifiedHash
    );
    return video;

  } else {
    return data.videos[0];

  }
};

And in both files I got the object I want. Now I want to use the content inside the object to render some tsx in the page file, called: CourseDetail.tsx.

import { FindCourseDetail } from "./FindCourseDetail";

type Video = {
  title: string;
  description?: string;
  thumbnail?: string;
  videoid?: string;
  chapter: boolean;
  duration: number;
  subtitles: [];
};

export const CourseDetail = () => {
  const videoObject = FindCourseDetail(); 


  return (
    <div>
      <h2>Content player</h2>
      <h2>{videoObject.title}</h2>
    </div>
  );
};

And 'videoObject.title' will give me the error: Property 'title' does not exist on type 'Promise'.

That's fair I think, because if I console.log it, it is a promise. But I'm not sure how I can write it something like this and make it work. So it should be something like: const videoObject = await FindCourseDetail(); . But that's not possible because the component is not async and it's called directly from react router.

I tried to copy paste the function of useCourseDetail inside the CourseDetail. And that works if I use an useState and useEffect. But that's a little bit messy and doesn't feel good, because the default state is an object with null. See code below:

import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { getVideoInfo } from "../../api/Api";
import { Slugify } from "../../components/Util/Slugify";

type Video = {
  title: string;
  description?: string;
  thumbnail?: string;
  videoid?: string;
  chapter: boolean;
  duration: number;
  subtitles: [];
};

export const CourseDetail = () => {
  const { hash } = useLocation();

  const [videoData, setVideoData] = useState<Video>({
    title: null,
    description: null,
    thumbnail: null,
    videoid: null,
    chapter: null,
    duration: null,
    subtitles: null,
  });

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

   const findCourseData = async () => {
     const slugifiedHash = Slugify(hash);
     const data = await getVideoInfo();

     if (hash) {
       const video = data.videos.find(
         (v) =>
           Slugify(v.title, {
             lowerCase: true,
             replaceDot: "-",
             replaceAmpersand: "and",
           }) === slugifiedHash
       );
       setVideoData(video);
     }
   };

  return (
    <div>
      <h2>Content player</h2>
      <h2>{videoObject.title}</h2>
    </div>
  );
};

I have a feeling that this isn't a big problem to solve, but I can't see where it's going wrong or how I can solve this.

Edit: I tried the following:

const [newData, setNewData] = useState({});

  const getData = async () => {
    try {
      const apiValue = await FindCourseDetail();
      setNewData(apiValue);
    } catch (err) {
      // error handling
    }
  };
  getData();

return (
    <div>
      <h2>Content player</h2>
      {newData && <h2>{newData.title}</h2>}
    </div>
  );

or: FindCourseDetail().then(videoObject => setNewData(videoObject))

And now it will throw me the same error: Property 'title' does not exist on type '{}'.

If I remove the empty object as default state, it will say: it's possibly undefined.

And if do write null in the default state, it works of course. But I don't think that's the right way:

const [newData, setNewData] = useState({
    title: null,
    description: null,
    thumbnail: null,
    videoid: null,
    chapter: null,
    duration: null,
    subtitles: null,
  });

Edit 2:

import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { getVideoInfo } from "../../api/Api";
import { Slugify } from "../../components/Util/Slugify";

type Video = {
  title: string;
  description?: string;
  thumbnail?: string;
  videoid?: string;
  chapter: boolean;
  duration: number;
  subtitles: [];
};

export const CourseDetail = () => {
  const { hash } = useLocation();
  
  const [videoData, setVideoData] = useState<Video>({
    title: null,
    description: null,
    thumbnail: null,
    videoid: null,
    chapter: null,
    duration: null,
    subtitles: null,
  });

  const findCourseData = async () => {
    const slugifiedHash = Slugify(hash);
    const data = await getVideoInfo();

    if (hash) {
      const video = data.videos.find(
        (v) =>
          Slugify(v.title, {
            lowerCase: true,
            replaceDot: "-",
            replaceAmpersand: "and",
          }) === slugifiedHash
      );
      setVideoData(video);
    }
  };
  useEffect(() => {
    findCourseData();
  }, []);

  return (
    <div>
      <h2>Content player</h2>
      {videoData&& <h2>{videoData.title}</h2>}
    </div>
  );
};

This currently works, but as you can see I copied and pasted the function itself in the component. So I tried the following code:

type Video = {
  title: string;
  description?: string;
  thumbnail?: string;
  videoid?: string;
  chapter: boolean;
  duration: number;
  subtitles: [];
};

export const CourseDetail = () => {

  const [newData, setNewData] = useState<Video>(null);

  
  const getData = async () => {
    try {
      const apiValue = await FindCourseDetail();
      console.log(apiValue);
      setNewData(apiValue);
    } catch (e) {
      console.log('catch')
      console.log(e)
    }
  };

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

  return (
    <div>
      <h2>Content player</h2>
      {newData&& <h2>{newData.title}</h2>}
    </div>
  );

And this won't run, the catch is triggered and this is the error log:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
 1. You might have mismatching versions of React and the renderer (such as React DOM)
 2. You might be breaking the Rules of Hooks
 3. You might have more than one copy of React in the same app

Not sure why. This is the code of FindCourseDetail that is called:

import { useLocation } from "react-router";
import { getVideoInfo } from "../../api/Api";
import { Slugify } from "../../components/Util/Slugify";

export const FindCourseDetail = async () => {
  const { hash } = useLocation();
  const slugifiedHash = Slugify(hash);
  const data = await getVideoInfo();

  if (hash) {
    const video = data.videos.find(
      (v) =>
        Slugify(v.title, {
          lowerCase: true,
          replaceDot: "-",
          replaceAmpersand: "and",
        }) === slugifiedHash
    );
    return video;

  } else {
    return data.videos[0];

  }
};
Rowin_nb2
  • 164
  • 1
  • 13
  • Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Heretic Monkey Apr 02 '21 at 12:56
  • Not fully, I think. I understand why TypeScript will throw me the error, because it's a promise as soon as it's rendered. But I don't see a way to solve this in the article, right? I should use await, because it's an async call. But the page component is called from react router, so I can't change it to an async function. – Rowin_nb2 Apr 02 '21 at 13:03
  • You have to account for the asynchronous nature of `FindCourseDetail` somehow. Either use `FindCourseDetail().then(videoObject => /* do something with videoObject */)` or use something like the answers to [Render async component in React Router v4](https://stackoverflow.com/q/47878049/215552) – Heretic Monkey Apr 02 '21 at 13:08
  • Tried it and editted the main post. Somehow I will get the same error. – Rowin_nb2 Apr 02 '21 at 13:24
  • Use `{newData.title &&

    {newData.title}

    }` or `useState()` instead of `useState({})` so that `newData` will return something falsy.
    – Heretic Monkey Apr 02 '21 at 13:37
  • Yeah if I do that, it will give me this error: `Object is possibly 'undefined'`. And an infinite loop haha – Rowin_nb2 Apr 02 '21 at 13:40
  • I don't know React too well, sorry. Maybe `useState(null)`? Or `useState({title:''})`? – Heretic Monkey Apr 02 '21 at 13:43
  • Does this answer your question? [React how to render async data from api?](https://stackoverflow.com/questions/51116716/react-how-to-render-async-data-from-api) – Jared Smith Apr 02 '21 at 14:58

3 Answers3

1

Your case is a bit more specific and complex, but I bumped into this same issue and generally speaking I understood the following: this seems to happen because you are trying to access a property on a callback value that has returned yet. example:

const user = getUser(id); // <= async function
user.property // <= ts will complain as user is going to be a Promise<any>

Solution

const user = await getUser(id); // <= add await
user.property // <= now it will be avaiable
Fed
  • 1,696
  • 22
  • 29
0

You should probably continue with your useEffect approach but declare the (generic) type of useState to be a union, including either null or the Video data. Then you use a type guard to eliminate the case where it's null (don't render it).

Like this...

const [videoData, setVideoData] = useState<Video|null>(null);

...

  return (
    <div>
      <h2>Content player</h2>
      {videoData && <h2>{videoData.title}</h2>}
    </div>
  );
cefn
  • 2,895
  • 19
  • 28
  • Yeah I think I just did that (see edit 2 in the main post). It works as long as I copy/paste the code of the 'FindCourseDetail' function in the component itself. As soon as I call the function (FindCourseDetail), the catch is triggered. – Rowin_nb2 Apr 02 '21 at 15:43
  • Although I kind of see your point, (looks like a clear path in which FindCourseDetail is called synchronously from inside your function) I would second-guess myself and try to push all hooks calls into the function component call. That is to say the useLocation should happen there, and its result be passed into the FindCourseDetail call. Also I'd be creating the getData function inside the useEffect as it should only be defined once. – cefn Apr 02 '21 at 16:44
0

Fixed it! I was calling the useLocation hook inside the FindCourseDetail. I fixed it by calling useLocation hook in the current file and give it as a prop to FindCourseDetail.

Rowin_nb2
  • 164
  • 1
  • 13