React Native
render method
async functions
JavaScript
mobile development

Can you make render method in React Native async?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

No, you cannot make the render() method (or a functional component's return) async in React Native. React expects render() to return JSX synchronously. Making it async would return a Promise instead of a React element, which React cannot render. To handle asynchronous data in your components, fetch data in lifecycle methods (useEffect, componentDidMount) or event handlers, store results in state, and render based on that state.

Why render() Cannot Be Async

jsx
1// THIS DOES NOT WORK
2const MyComponent = async () => {
3  const data = await fetchData();  // Returns a Promise, not JSX
4  return <Text>{data.name}</Text>;
5};
6
7// React sees: MyComponent() returns Promise<ReactElement>
8// React needs: MyComponent() returns ReactElement
9// Result: Error or blank screen

An async function always returns a Promise. React's reconciler expects a React element (JSX) or null from render — it does not know how to render a Promise. The component either crashes or renders nothing.

The Correct Pattern: useEffect + State

jsx
1import React, { useState, useEffect } from 'react';
2import { View, Text, ActivityIndicator } from 'react-native';
3
4const UserProfile = () => {
5  const [user, setUser] = useState(null);
6  const [loading, setLoading] = useState(true);
7  const [error, setError] = useState(null);
8
9  useEffect(() => {
10    const loadUser = async () => {
11      try {
12        const response = await fetch('https://api.example.com/user/1');
13        const data = await response.json();
14        setUser(data);
15      } catch (err) {
16        setError(err.message);
17      } finally {
18        setLoading(false);
19      }
20    };
21
22    loadUser();
23  }, []);
24
25  if (loading) return <ActivityIndicator size="large" />;
26  if (error) return <Text>Error: {error}</Text>;
27
28  return (
29    <View>
30      <Text>{user.name}</Text>
31      <Text>{user.email}</Text>
32    </View>
33  );
34};

This is the standard React pattern: start loading on mount, update state when data arrives, render based on current state. The component renders multiple times — first with loading state, then with data or error.

Class Component Pattern

jsx
1import React, { Component } from 'react';
2import { View, Text, ActivityIndicator } from 'react-native';
3
4class UserProfile extends Component {
5  state = { user: null, loading: true, error: null };
6
7  async componentDidMount() {
8    try {
9      const response = await fetch('https://api.example.com/user/1');
10      const data = await response.json();
11      this.setState({ user: data, loading: false });
12    } catch (error) {
13      this.setState({ error: error.message, loading: false });
14    }
15  }
16
17  render() {
18    const { user, loading, error } = this.state;
19
20    if (loading) return <ActivityIndicator size="large" />;
21    if (error) return <Text>Error: {error}</Text>;
22
23    return (
24      <View>
25        <Text>{user.name}</Text>
26        <Text>{user.email}</Text>
27      </View>
28    );
29  }
30}

componentDidMount can be async because React does not use its return value. render() cannot, because React uses its return value (JSX).

Custom Hook for Async Data

jsx
1import { useState, useEffect } from 'react';
2
3function useAsync(asyncFn, deps = []) {
4  const [data, setData] = useState(null);
5  const [loading, setLoading] = useState(true);
6  const [error, setError] = useState(null);
7
8  useEffect(() => {
9    let cancelled = false;
10
11    const execute = async () => {
12      setLoading(true);
13      setError(null);
14      try {
15        const result = await asyncFn();
16        if (!cancelled) setData(result);
17      } catch (err) {
18        if (!cancelled) setError(err);
19      } finally {
20        if (!cancelled) setLoading(false);
21      }
22    };
23
24    execute();
25    return () => { cancelled = true; };
26  }, deps);
27
28  return { data, loading, error };
29}
30
31// Usage
32const UserProfile = ({ userId }) => {
33  const { data: user, loading, error } = useAsync(
34    () => fetch(`https://api.example.com/user/${userId}`).then(r => r.json()),
35    [userId]
36  );
37
38  if (loading) return <ActivityIndicator />;
39  if (error) return <Text>Error: {error.message}</Text>;
40  return <Text>{user.name}</Text>;
41};

The cancelled flag prevents state updates on unmounted components, avoiding the "Can't perform a React state update on an unmounted component" warning.

Suspense (Experimental in React Native)

React Suspense allows components to "wait" for async data, which is the closest React gets to async rendering:

jsx
1import React, { Suspense } from 'react';
2import { Text, ActivityIndicator } from 'react-native';
3
4// Resource factory (simplified)
5function createResource(promise) {
6  let status = 'pending';
7  let result;
8  let suspender = promise.then(
9    (r) => { status = 'success'; result = r; },
10    (e) => { status = 'error'; result = e; }
11  );
12  return {
13    read() {
14      if (status === 'pending') throw suspender;
15      if (status === 'error') throw result;
16      return result;
17    }
18  };
19}
20
21const userResource = createResource(
22  fetch('https://api.example.com/user/1').then(r => r.json())
23);
24
25const UserProfile = () => {
26  const user = userResource.read();  // Throws Promise if not ready
27  return <Text>{user.name}</Text>;
28};
29
30const App = () => (
31  <Suspense fallback={<ActivityIndicator />}>
32    <UserProfile />
33  </Suspense>
34);

Suspense catches the thrown Promise and shows the fallback until data is ready. This is the React team's long-term solution but has limited support in React Native as of React 18.

Conditional Rendering Based on Async State

jsx
1const DataList = () => {
2  const [items, setItems] = useState([]);
3  const [status, setStatus] = useState('idle'); // idle | loading | success | error
4
5  const loadData = async () => {
6    setStatus('loading');
7    try {
8      const res = await fetch('https://api.example.com/items');
9      const data = await res.json();
10      setItems(data);
11      setStatus('success');
12    } catch {
13      setStatus('error');
14    }
15  };
16
17  return (
18    <View>
19      <Button title="Load" onPress={loadData} />
20      {status === 'loading' && <ActivityIndicator />}
21      {status === 'error' && <Text>Failed to load</Text>}
22      {status === 'success' && items.map(item => (
23        <Text key={item.id}>{item.name}</Text>
24      ))}
25    </View>
26  );
27};

Using a status string instead of separate loading/error booleans prevents impossible states (like loading: true and error: true simultaneously).

Common Pitfalls

  • Async IIFE in useEffect: useEffect(async () => {...}) returns a Promise as the cleanup function, which React does not expect. Instead, define an async function inside and call it: useEffect(() => { const fn = async () => {...}; fn(); }, []).
  • Missing cleanup on unmount: If the component unmounts before the async call completes, calling setState causes a warning. Use a cancelled flag or AbortController to cancel pending requests.
  • Returning a Promise from render: If you accidentally make a component async, React may silently render nothing or show a cryptic error. If your component is blank, check whether it is marked async.
  • Infinite re-render loops: Calling an async function inside the component body (outside useEffect) triggers a state update, which triggers re-render, which calls the function again. Always fetch data inside useEffect or event handlers.
  • Race conditions with stale closures: If userId changes rapidly, multiple fetches run concurrently and the last one to complete wins — which may not be the latest userId. Use the cleanup function to ignore stale results.

Summary

  • React Native's render() must return JSX synchronously — it cannot be async
  • Fetch data in useEffect (functional) or componentDidMount (class), store in state, and render from state
  • Use a custom useAsync hook to encapsulate loading/error/data patterns
  • Always handle loading and error states in your JSX
  • Use cleanup functions (cancelled flag or AbortController) to prevent state updates on unmounted components
  • React Suspense is the future solution for async rendering but has limited React Native support currently

Course illustration
Course illustration

All Rights Reserved.