DEV Community

Cover image for ๐Ÿš€ New React Challenge: Build an External Store
ReactChallenges
ReactChallenges

Posted on

๐Ÿš€ New React Challenge: Build an External Store

Most React apps reach for a state library the moment state has to live outside a component. Building one yourself is the fastest way to understand what useSyncExternalStore actually expects โ€” and why a store that returns a fresh object on every getSnapshot call loops forever.


๐Ÿงฉ Overview

Build the external store behind a live metrics dashboard: immutable snapshots, subscribe/getSnapshot, five-second polling, stale-response protection, and pause/resume with real cleanup โ€” all in store.ts.

๐Ÿ‘‰ https://www.reactchallenges.com/challenges/build-external-store


โœ… Requirements

  • createMetricsStore(fetcher) returns subscribe, getSnapshot, start, refresh, pause, and resume; fetcher defaults to fetchMetrics.
  • The initial snapshot is { metrics: [], updatedAt: null, status: "idle", error: null }.
  • getSnapshot returns the same object reference until the state changes.
  • subscribe(listener) registers a listener and returns a function that removes it.
  • Subscribers are notified after every state change.
  • start() fetches immediately, then every five seconds, and returns a cleanup that stops polling.
  • Calling start() while polling is active does not create a second interval or another fetch.
  • While a request is in flight and the store is not paused, the status is loading.
  • refresh() ends in live with the new metrics and updatedAt, or in error with the message.
  • When a request fails, the last successful metrics stay in the snapshot.
  • After an error, the next poll retries the request.
  • When two requests overlap, only the latest response is applied.
  • pause() stops polling, sets the status to paused, and ignores an in-flight response.
  • resume() fetches immediately, restarts polling, and does nothing if the store is not paused.
  • A manual refresh() while paused updates the data and keeps the status paused.
  • The dashboardStore singleton is exported and used by the provided App.tsx.

๐Ÿ’ก Notes

  • The state lives in a closure outside React, and every update must create a new snapshot object. getSnapshot is compared with Object.is, so returning a fresh object on every call causes an infinite render loop.
  • The store is DOM-agnostic: it never reads document.hidden. The provided App.tsx calls pause() and resume() when tab visibility changes.
  • Pausing is not only about clearing the interval. Invalidate the request already in flight, or its response can flip the status back to live after the tab is hidden.

๐Ÿงช Tests

  1. exports the dashboardStore singleton
  2. renders the provided dashboard with the store
  3. starts with an idle snapshot and a stable reference
  4. notifies subscribers when the state changes
  5. stops notifying after unsubscribing
  6. fetches immediately when started
  7. shows a loading status while a request is in flight
  8. polls every five seconds
  9. starts only one interval and one fetch when start is called twice
  10. stops polling after stop is called
  11. keeps the last metrics when a request fails
  12. recovers on the next poll after an error
  13. discards a stale response that arrives after a newer one
  14. pauses polling and marks the dashboard as paused
  15. ignores an in-flight response that arrives after pausing
  16. resumes with an immediate fetch and restarts polling
  17. does nothing when resumed while already running
  18. stays paused when refreshed manually while paused
  19. keeps the snapshot reference stable between updates

By the end you'll know exactly what useSyncExternalStore expects from a store: stable snapshots, safe subscriptions, idempotent polling, and cleanup that survives StrictMode, tab switches, and out-of-order responses.

๐Ÿ”ฅ Start the Challenge Now

Top comments (0)