React
Asynchronous Data
Child Components
Props
Web Development

Passing asynchronously acquired data to child props

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In React, asynchronous data usually arrives after the first render, which means child components often receive null, an empty array, or a loading flag before the real payload exists. The clean solution is to let the parent own the fetch lifecycle, then pass stable props that describe loading, error, and data explicitly.

Fetch in the Parent, Render in the Child

The parent component is normally responsible for requesting data, storing it in state, and deciding when the child should render. That keeps the child focused on presentation.

tsx
1import { useEffect, useState } from "react";
2
3type User = {
4  id: number;
5  name: string;
6  email: string;
7};
8
9function UserPage() {
10  const [user, setUser] = useState<User | null>(null);
11  const [loading, setLoading] = useState(true);
12  const [error, setError] = useState<string | null>(null);
13
14  useEffect(() => {
15    let cancelled = false;
16
17    async function loadUser() {
18      try {
19        setLoading(true);
20        const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
21        if (!response.ok) {
22          throw new Error("Request failed");
23        }
24
25        const data: User = await response.json();
26        if (!cancelled) {
27          setUser(data);
28          setError(null);
29        }
30      } catch (err) {
31        if (!cancelled) {
32          setError(err instanceof Error ? err.message : "Unknown error");
33        }
34      } finally {
35        if (!cancelled) {
36          setLoading(false);
37        }
38      }
39    }
40
41    loadUser();
42    return () => {
43      cancelled = true;
44    };
45  }, []);
46
47  return <UserCard user={user} loading={loading} error={error} />;
48}

This pattern does two useful things. First, the child gets a predictable contract. Second, the cleanup flag prevents state updates after unmount, which is a common source of warnings and race conditions.

Design Child Props Around States, Not Just Data

Instead of assuming the child always receives a full object, define props that represent all realistic states.

tsx
1type User = {
2  id: number;
3  name: string;
4  email: string;
5};
6
7type UserCardProps = {
8  user: User | null;
9  loading: boolean;
10  error: string | null;
11};
12
13function UserCard(props: UserCardProps) {
14  if (props.loading) {
15    return <p>Loading user...</p>;
16  }
17
18  if (props.error) {
19    return <p>Failed to load user: {props.error}</p>;
20  }
21
22  if (!props.user) {
23    return <p>No user found.</p>;
24  }
25
26  return (
27    <section>
28      <h2>{props.user.name}</h2>
29      <p>{props.user.email}</p>
30    </section>
31  );
32}

That is more robust than passing user! or assuming the child can immediately dereference fields like user.name.

Passing Collections to Child Components

The same principle applies to arrays. Start with an empty array if that makes the child logic simpler, or keep null when you need to distinguish "not loaded yet" from "loaded but empty".

tsx
1import { useEffect, useState } from "react";
2
3type Post = {
4  id: number;
5  title: string;
6};
7
8function PostsPage() {
9  const [posts, setPosts] = useState<Post[]>([]);
10  const [loading, setLoading] = useState(true);
11
12  useEffect(() => {
13    async function loadPosts() {
14      const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
15      const data: Post[] = await response.json();
16      setPosts(data);
17      setLoading(false);
18    }
19
20    loadPosts();
21  }, []);
22
23  return <PostList posts={posts} loading={loading} />;
24}
25
26function PostList(props: { posts: Post[]; loading: boolean }) {
27  if (props.loading) {
28    return <p>Loading posts...</p>;
29  }
30
31  if (props.posts.length === 0) {
32    return <p>No posts available.</p>;
33  }
34
35  return (
36    <ul>
37      {props.posts.map((post) => (
38        <li key={post.id}>{post.title}</li>
39      ))}
40    </ul>
41  );
42}

An empty array is often the easiest default because methods like .map() are safe immediately.

When to Lift Fetching Higher

If several children need the same asynchronous result, fetch once in a shared parent and pass the derived pieces down. That avoids duplicate requests and inconsistent loading behavior. If the tree becomes deep, React context or a data library such as TanStack Query can make the flow cleaner, but the principle remains the same: keep ownership of asynchronous state close to where the request is coordinated.

Common Pitfalls

The most common bug is rendering child code that assumes data exists before the request completes. That leads to errors such as "Cannot read properties of null". Use loading guards or optional chaining where appropriate, but prefer explicit render branches over hiding missing state everywhere.

Another problem is starting a fetch in the parent and separately starting a second fetch in the child for the same data. That duplicates network work and makes state harder to reason about. Decide which component owns the request.

Race conditions are also easy to introduce when props change quickly. If a child depends on data fetched from an id passed by the parent, cancel stale requests or ignore stale responses so older data does not overwrite newer state.

Summary

  • Fetch asynchronous data in the parent when that parent owns the view state.
  • Pass loading, error, and data as explicit props instead of assuming data is immediately available.
  • Use null or an empty array intentionally based on what state distinctions you need.
  • Guard child rendering so it handles not-yet-loaded data safely.
  • Lift shared fetching higher in the tree to avoid duplicated requests and inconsistent state.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.