[Guide] Build a Video Course Platform - implementation notes
If you've used nested scopes for configuration, how do you keep them from turning into a confusing global-ish layer?
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 latestIs 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.
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.
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.
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).
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.
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.
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?
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.
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.
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.
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.
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} />;
}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.
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).
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.