15

I have fetch method in useEffect hook:


export const CardDetails = () => {
  const [ card, getCardDetails ] = useState();

  const { id } = useParams();

  useEffect(() => {
    fetch(`http://localhost:3001/cards/${id}`)
    .then((res) => res.json())
    .then((data) => getCardDetails(data))
  }, [id])

  return (
     <DetailsRow data={card} />
  )
}

But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?

4 Answers 4

26

Just don't render it when the data is undefined:

export const CardDetails = () => {
  const [card, setCard] = useState();

  const { id } = useParams();

  useEffect(() => {
    fetch(`http://localhost:3001/cards/${id}`)
      .then((res) => res.json())
      .then((data) => setCard(data));
  }, [id]);

  if (card === undefined) {
    return <>Still loading...</>;
  }

  return <DetailsRow data={card} />;
};
Sign up to request clarification or add additional context in comments.

Comments

17

There are 3 ways to not render component if there aren't any data yet.

  1. {data && <Component data={data} />}
  2. Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
  3. Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}

Comments

0

If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.

import { useEffect, useState } from "react"

var receivedData: any = null

type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
    loadingStatus: boolean,
    data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
    addListener: AddListener,
    removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch

export const startTopFetch = function (fetcher: Fetcher) {
    let receivedData: any = null
    let listener: Listener[] = []
    function addListener(cb: Listener): number {
        if (receivedData) {
            cb(false, receivedData)
            return 0
        }
        else {
            listener.push(cb)
            console.log("listenre:", listener)
            return listener.length - 1
        }
    }
    function removeListener(id: number) {

        console.log("before remove listener: ", id)
        if (id && id >= 0 && id < listener.length) {
            listener.splice(id, 1)
        }
    }
    let res = fetcher()
    if (typeof res.then === "undefined") {
        receivedData = res
    }
    else {
        fetcher().then(
            (data: any) => {
                receivedData = data
            },
        ).finally(() => {
            listener.forEach((cb) => cb(false, receivedData))
        })

    }
    return { addListener, removeListener }
} as StartTopFetch

export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {

    const [loadingStatus, setLoadingStatus] = useState(true)
    useEffect(() => {
        const id = listener.addListener((v: boolean, data: any) => {
            setLoadingStatus(v)
            receivedData = data
        })
        console.log("add listener")
        return () => listener.removeListener(id)
    }, [listener])
    return [loadingStatus, receivedData]

}

This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:

import { startTopFetch, useTopFetch } from "./topFetch";

// a fakeFetch 
const fakeFetch = async () => {
    const p = new Promise<object>((resolve, reject) => {
        setTimeout(() => {
            resolve({ value: "Data from the server" })
        }, 1000)
    })
    return p
}

//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)

export const Demo = () => {
    const defaultData = { value: "Default Data" }
    
    //In your component , call useTopFetch and pass the return value from startTopFetch.    
    const [isloading, dataFromServer] = useTopFetch(myTopFetch)
    return <>
        {isloading ? (
        <div>{defaultData.value}</div>
        ) : (
        <div>{dataFromServer.value}</div>
        )}
    </>
}

Comments

0

Try this:

export const CardDetails = () => {
    const [card, setCard] = useState();
    const { id } = useParams();
    useEffect(() => {
        if (!data) {
            fetch(`http://localhost:3001/cards/${id}`)
                .then((res) => res.json())
                .then((data) => setCard(data))
        }
    }, [id, data]);
    return (
        <div>
            {data && <DetailsRow data={card} />}
            {!data && <p>loading...</p>}
        </div>
    );
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.