When Props Object is Undefined
How I Encountered This Problem
I was building a user profile page for my service. The parent component fetches user data from an API, then passes it to a child component.
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(data => setUser(data));
}, []);
return <UserCard user={user} />;
}
function UserCard({ user }) {
return (
<div>
<h1>{user.name}</h1> {/* 🔥 Cannot read property 'name' of null */}
<p>{user.email}</p>
</div>
);
}
Running this code crashes the app. The console shows a red error: "Cannot read property 'name' of null."
At first I thought "I'm clearly fetching data from the API, why is it null?" But the problem was timing. The component renders before the API response arrives.
What Confused Me Initially?
My misconception was: "When you fetch data in useEffect, the data is already there when the component renders"
But React's rendering order is:
- Component renders (
userisnull) - Display on screen
useEffectexecutes- API call
- Response received,
setUsercalled - Re-render (now
userhas data)
The problem is at step 1, we're already trying to read user.name. At this point user is still null, causing an error.
"So what should I do?" I thought. How do you handle when data isn't available?
The Aha Moment
I understood this problem with this analogy:
"A component is a restaurant kitchen, and props are ingredients. If you start cooking before ingredients arrive, you'll obviously have problems. Wait for ingredients or use substitutes."
Ah! That's why you need loading states or optional chaining.
Several solutions exist:
Solution 1: Add loading state
function ProfilePage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser()
.then(data => setUser(data))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <UserCard user={user} />;
}
Solution 2: Optional chaining
function UserCard({ user }) {
return (
<div>
<h1>{user?.name ?? 'Unknown'}</h1>
<p>{user?.email ?? 'No email'}</p>
</div>
);
}
Solution 3: Default values
function UserCard({ user = {} }) {
const { name = 'Unknown', email = 'No email' } = user;
return (
<div>
<h1>{name}</h1>
<p>{email}</p>
</div>
);
}
I usually prefer solution 1. It clearly shows users that loading is in progress.
Deep Dive
React Rendering Lifecycle
Understanding React component rendering order is crucial:
function Component() {
console.log('1. Render phase');
useEffect(() => {
console.log('3. Effect phase (after render)');
});
return <div>2. JSX returned</div>;
}
Execution order:
- Logs "1. Render phase"
- Returns JSX
- Draws to screen
- Logs "3. Effect phase"
useEffect runs after rendering. So on first render, data isn't available yet.
Type Safety with TypeScript
TypeScript catches these problems at compile time:
interface User {
name: string;
email: string;
}
interface UserCardProps {
user: User | null; // Explicitly allow null
}
function UserCard({ user }: UserCardProps) {
if (!user) {
return <div>Loading...</div>;
}
// Now user is definitely User type
return (
<div>
<h1>{user.name}</h1> {/* ✅ Type safe */}
<p>{user.email}</p>
</div>
);
}
TypeScript warns "user might be null, check it."
Handling Nested Objects
Real API responses usually have nested objects:
const user = {
profile: {
personal: {
name: 'John',
age: 30
},
contact: {
email: 'john@example.com'
}
}
};
Optional chaining shines here:
// 🔥 Dangerous: crashes if any intermediate is undefined
const name = user.profile.personal.name;
// ✅ Safe: returns undefined if any intermediate is undefined
const name = user?.profile?.personal?.name;
Handling Array Props
Same for arrays:
function UserList({ users }) {
return (
<ul>
{users.map(user => ( // 🔥 Crashes if users is undefined
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ✅ Safe version
function UserList({ users = [] }) {
if (users.length === 0) {
return <div>No users</div>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Suspense and Error Boundary
From React 18, you can handle this more elegantly with Suspense:
function ProfilePage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary fallback={<div>Error!</div>}>
<UserCard />
</ErrorBoundary>
</Suspense>
);
}
function UserCard() {
const user = use(fetchUser()); // React 19 use hook
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
This allows declarative loading and error handling.
How I Applied This to My Code
Improved Profile Page
I improved my profile page like this:
function ProfilePage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser()
.then(data => setUser(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!user) {
return <div>User not found</div>;
}
return <UserCard user={user} />;
}
Now handles all cases: loading, error, no data, success.
Custom Hook for Reuse
Since I use this pattern often, I made a custom hook:
function useFetch(fetchFn) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchFn()
.then(setData)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
return { data, loading, error };
}
// Usage
function ProfilePage() {
const { data: user, loading, error } = useFetch(fetchUser);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
if (!user) return <div>User not found</div>;
return <UserCard user={user} />;
}
Much cleaner!
Form Data Handling
Had similar issues in forms:
function EditProfile({ initialData }) {
const [formData, setFormData] = useState(initialData);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<form>
<input
name="name"
value={formData.name} // 🔥 Crashes if initialData is undefined
onChange={handleChange}
/>
</form>
);
}
Fixed with default values:
function EditProfile({ initialData = {} }) {
const [formData, setFormData] = useState({
name: '',
email: '',
...initialData // Overwrite if exists
});
// Now safe!
}
One-Line Summary
Props objects can be undefined on first render, so handle them safely with loading states, optional chaining, and default values.
10. Recommended Reading
- React Docs: Rendering Cycle: Understand when effects run.
- TypeScript Handbook: Optional Chaining: The
?.syntax is your best friend. - React Query Docs: How to handle loading/error states declaratively.