The Render-as-you-fetch Pattern Explained Simply
Cover photo by Scott Rodgerson on Unsplash
Why is your app slower than it feels like it should be? Often, it is because you are fetching data in the wrong place.
The naive way
Most of us start by putting our fetch logic inside a useEffect hook. It looks like this:
function UserProfile({ id }) { const [user, setUser] = useState(null);
useEffect(() => { fetchUser(id).then(setUser); }, [id]);
if (!user) return <Loading />; return <div>{user.name}</div>;}This is the fetch-on-render pattern. The component mounts, the effect fires, and only then does the network request start. Your user sits there staring at a loading spinner because the component had to render first before the request happened. It is a waterfall.
Render-as-you-fetch
“But why does it matter if the browser starts the request a few milliseconds later?” That is a fair question, and for a single component, it really doesn’t. But as your tree grows, these tiny delays add up. If a child component also needs data, it won’t even know it needs to fetch until the parent has rendered. That is slow.
Render-as-you-fetch means kicking off the request the moment you know you need the data, usually before the component even renders. You trigger the fetch in your event handler or your router, not inside a component effect.
Here is the shift:
// Instead of useEffect, trigger the fetch immediatelyconst userPromise = fetchUser(id);
function UserProfile() { // Read the promise directly (or use a library wrapper) const user = use(userPromise); return <div>{user.name}</div>;}(I’m glossing over how the use hook actually works, but imagine it suspends the component until the promise resolves.)
Making it real
If you use a router that supports loaders, this happens automatically. You start the request in the loader while the browser is still figuring out which component to show. By the time the component code is executed, the data is already being fetched. The network and the JS engine are working in parallel instead of one waiting for the other.
Where this gets messy
I will admit, this pattern makes state management harder. If you move your fetching logic outside of the component, where do you put the error handling? How do you cancel requests easily if the user navigates away? It isn’t always a silver bullet. Sometimes, keeping the logic inside a hook is just easier for the team to reason about (and that is a valid engineering trade-off).
If you are just building a simple admin dashboard, you might find this pattern adds more complexity than it solves. Is it worth the setup overhead? Usually, yes, because the performance gains in larger apps are hard to ignore.
Moving forward
Do you really need to suspend your whole app? Probably not. Start by moving your data loading into your router or event handlers and see if your main metrics improve. Does this change the way you write your UI code entirely, or are you still finding yourself leaning back into effect hooks?