React Forum
Blog & Guide Responses
Jan 18, 2023 · 04:00 PM

[Guide] Build a Video Course Platform - implementation notes

Started by Ari Novak
guideuseScopescope overlaysplayerprogressrouting

If you've used nested scopes for configuration, how do you keep them from turning into a confusing global-ish layer?

Read the guide

The guide uses scope overlays (app -> course -> lesson) so configuration stays route-visible without prop threading.

Do you use scopes for branding/gating, or mostly for player knobs (speed/quality/transcript posture)? Where do you keep progress evidence: a global vault, per-lesson docs, or both? Do you model the overlay stack as route state (overlay) or as local state per panel? And for duplicate fetching: do you accept flicker, or do you warm-start from a vault and let render fetch keep it fresh?

Comments (12)

Back to latest
Keira Santos
Jan 18, 2023 · 04:06 PM

Is a scope basically 'global config', or can it be different per nested route segment?

I'm trying to understand how overlaying scopes doesn't become confusing.

Ari Novak
Jan 18, 2023 · 04:11 PM

It helps to treat scopes as contracts, not just config bags.

App scope defines defaults. Course scope overwrites only course-level keys. Lesson scope overwrites only lesson-level keys.

If you keep the shapes stable, overlaying stays legible.

Keira Santos
Jan 18, 2023 · 04:15 PM

Ok, I was picturing it as "global defaults but with more steps".

Thinking of it as contract overlays (only overwrite a few keys) clicks more.

Camille Duarte
Jan 18, 2023 · 04:22 PM

I like storing progress in a vault because it’s cross-route behavior (catalog cards, course overview, player).

But I still keep a local per-lesson doc for UI posture (buffering, last action, local errors).

Noah Vance
Jan 18, 2023 · 04:33 PM

Scopes have been cheap for us as long as we don’t rebuild giant objects every render.

I’ve seen teams allocate a whole new scope object each render and then wonder why children remount.

Small, idempotent writes make the posture stable.

Priya Liao
Jan 18, 2023 · 04:44 PM

I strongly prefer that scope shapes are explicit and versioned.

If CourseScope.branding changes shape, I want that to be a deliberate contract change, not an incidental refactor.

Keira Santos
Jan 18, 2023 · 04:55 PM

Do you store the overlay stack itself anywhere?

For example, app overlay sets defaultSpeed, course overlay sets gating, lesson overlay sets startAtSeconds. Do you ever need to audit where a value came from?

Camille Duarte
Jan 18, 2023 · 05:03 PM

We render small scope signals (data attributes) for debugging: data-scope-course, data-scope-lesson.

It's consistent with making posture visible instead of hidden.

Ari Novak
Jan 18, 2023 · 05:16 PM

I keep some player knobs local (volume, captions toggle) and only promote them to scopes if multiple segments need them.

Scopes feel best when they stay contract-level values, not every micro-preference.

Noah Vance
Jan 18, 2023 · 05:28 PM

We used playerMode: full | mini as global route state and it was a big win.

The mini-player became pure motion without rewriting the file router map.

Priya Liao
Jan 18, 2023 · 05:41 PM

If the course overview fetches lessons and the player fetches the lesson again, do you see flicker?

I've been writing the overview result into a vault as a warm start, then letting render fetch bypass keep it fresh.

Dmitri Kline
Jan 18, 2023 · 06:01 PM

We made scopes less confusing by rendering a simple origin signal for any key that mattered. If speed came from lesson scope vs course scope, you could tell immediately.

tsx
function PlayerShell() {
  const app = useScope('app');
  const course = useScope('course');
  const lesson = useScope('lesson');

  const speedFrom = lesson.has('speed') ? 'lesson' : course.has('speed') ? 'course' : 'app';
  const speed = lesson.read('speed') ?? course.read('speed') ?? app.read('speed') ?? 1;

  return <VideoPlayer speed={speed} data-speed-from={speedFrom} />;
}
Salma Qureshi
Jan 18, 2023 · 06:10 PM

Big perf footgun with scopes: recreating the scope values every render and invalidating the whole subtree. We treat scope writes as idempotent and memoize the overlay object so identity stays stable.

tsx
const overlay = useMemo(() => ({ gating: courseGating, branding: courseBranding }), [courseGating, courseBranding]);
useScopeOverlay('course', overlay);

If you can't make the overlay stable, you end up blaming React for re-renders that are really just unstable surfaces.

Maya Henson
Jan 18, 2023 · 06:24 PM

Progress evidence felt best when we stored both a durable summary and a "last interaction" marker.

The summary is for other routes (catalog cards), the marker is for the player (resume posture).

Related: State Management (Best Practices).

Benji Rios
Jan 18, 2023 · 06:41 PM

The only way scopes didn't become "global-ish" for us was limiting what can enter a scope.

If a key isn't part of the player contract (speed, captions posture, gating, resume), it stays local and dies with the component.