DEV Community

Cover image for 🧠 Smarter Data Fetching in React Without Extra Libraries: Prefetching & a Shared Store
Gervais Yao Amoah
Gervais Yao Amoah

Posted on

🧠 Smarter Data Fetching in React Without Extra Libraries: Prefetching & a Shared Store

When building modern React apps, especially ones that fetch external data (like a movie database), it's common to reach for useEffect() and let the component lifecycle do the work. But this often leads to issues like:

  • ❌ No server-side rendering
  • 🌊 Network waterfalls (delayed or chained requests)
  • 🌀 Race conditions
  • 🚫 No caching or preloading

Recently, I explored a pattern that drastically improves data fetching in React without adding a new library or framework. It's simple, scalable, and leverages JavaScript's core features like events and memory caching.

Let me walk you through it.


👎 The Old Way: useEffect() All Over

In the typical approach, we might have:

  • A getActorList() function that fetches a list of actors in a movie.
  • A getActorDetails(id) function for actor-specific data.
  • On each relevant page (e.g., /cast, /actor/:id), we call these functions inside useEffect.

But this means:

  • Data is fetched after component render.
  • Navigation between pages causes repetitive requests.
  • No easy way to share or cache data between components or navigation events.

✅ The New Way: Prefetching + Shared Store + Custom Hook

Here's the approach in a nutshell:

1. A Shared Data Store

We use a simple in-memory map (an object or Map) as a central store:

const store = new Map();
Enter fullscreen mode Exit fullscreen mode

2. Custom Hook: useData(key)

This hook looks for the data in the store. If it’s there, it returns it. If not, it listens for a dataFetched event and resolves when the data becomes available.

function useData(key) {
  const [data, setData] = useState(null);

  if (store.has(key)) return store.get(key);

  window.addEventListener('dataFetched', () => store.get(key));
  return data;
}
Enter fullscreen mode Exit fullscreen mode

3. Service Layer

We move all fetch logic into a dedicated file:

// services.js
export async function getActorList(movieId) { /* ... */ }
export async function getActorDetails(actorId) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

4. Prefetching Functions

function prefetchData(key, fn) {
  fn().then(data => {
    store.set(key, data);
    window.dispatchEvent(new Event('dataFetched'));
  });
}

function prefetchDataOnEvent(key, fn) {
  if (store.has(key)) return;
  prefetchData(key, fn);
}
Enter fullscreen mode Exit fullscreen mode

🎬 Real-World Example: "Inception" Movie Page

Let’s say we're building a movie site. The user lands on the "Inception" page and sees the cast.

On Parent Component

We prefetch cast data early:

prefetchData('inception-cast', () => getActorList('inception'));
Enter fullscreen mode Exit fullscreen mode

Then on the cast page:

function MovieHome() {
  const actors = useData('inception-cast');
  ...
}
Enter fullscreen mode Exit fullscreen mode

No useEffect, no delay — just immediate access if it's been prefetched.

On Hover (or Link Focus)

When the user hovers an actor's card:

<Link to={`/actor/${actor.id}`}
onMouseEnter={() => prefetchDataOnEvent(actorId, () => getActorDetails(actorId))}
>
  View Details
</Link>
Enter fullscreen mode Exit fullscreen mode

Then, on the actor details page:

function ActorDetails() {
  const actor = useData(actorId);
  ...
}
Enter fullscreen mode Exit fullscreen mode

If the data was prefetched, it’s instant. If not, it still works, just a bit slower.


⚡ Advantages

  • ✅ No external library
  • ✅ No useEffect hell
  • ✅ Data reuse across navigation
  • ✅ Easy caching & preloading
  • ✅ Simpler mental model

🧪 When Should You Use This?

This pattern works best in:

  • SPA apps without SSR (e.g., create-react-app, Vite)
  • Projects where route transitions are predictable
  • Pages with nested or repeated API calls

It's not a replacement for advanced tools like React Query or SWR — but it’s perfect for small to medium projects where you want control and simplicity.


🚀 Final Thoughts

This technique opened my eyes to how much smarter React apps can be with a bit of architectural discipline. It’s lean, effective, and feels like native React — just better structured.

That said, there's room for improvement:

  • 🛠️ Add error handling to catch failed fetches gracefully.
  • 📶 Track status like loading or success, so useData could return an object like { isLoading, data, error }.
  • 🔁 Add time-based cache invalidation or manual refresh mechanisms.

This is just a solid foundation — and from here, you can evolve it toward more robust patterns depending on your app’s needs.

I’d love to hear your thoughts! Have you tried this kind of setup before? What’s your go-to method for client-side data management in React?

Top comments (1)