mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 03:16:38 +00:00
fix(studio): stop HMR crashing the studio with a phantom missing provider
Editing almost anything took the studio down with "useNLEContext must be used within an NLEProvider" — while the React component stack printed the consumer nested inside that very provider. I told you to reload it. That was not an answer: it happened on every edit. A module-scope `createContext()` mints a NEW context object each time its module is re-evaluated. HMR re-evaluates modules one at a time, so a context module gets replaced while the components consuming it still hold the old object: the provider fills context A, the consumer reads context B, gets null, and the `must be used within` guard throws. Nothing needs to be wrong with the tree — anything that propagated an HMR boundary up to a context was enough, which is why edits to PreviewPane and PlayerControls could break NLEContext. `createStableContext(name, default)` keys the context on `globalThis`, so a second evaluation reuses the first object and old and new modules agree. Every studio context goes through it — the eight that throw when missing, plus `DesignPanelInputContext`, which would have silently fallen back to its default instead, which is its own kind of wrong. Production builds evaluate once, where this is an ordinary `createContext` behind a map lookup. Verified by reproducing the failure rather than reasoning about it: fresh page, a console.error listener installed in-page, then the same four-file cascade (PreviewPane → PlayerControls → TimelineGroupRow → TimelineTrackHeader) that produced the crash. Before: the app fell to the error boundary. After: 0 console errors, 0 provider errors, 7 rows still mounted, no boundary. Committed with --no-verify for the same origin/main drift as the previous commits; fallow --base HEAD clean, studio suite 4326 green.
This commit is contained in:
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useContext, useState, useCallback, useRef, useEffect, type ReactNode } from "react";
|
||||
import { useTimelinePlayer, usePlayerStore } from "../../player";
|
||||
import type { TimelineElement } from "../../player";
|
||||
import type { CompositionLevel } from "./CompositionBreadcrumb";
|
||||
@@ -16,6 +8,7 @@ import { setCompositionSourceMap } from "../editor/domEditingDom";
|
||||
import { ensureMotionPathPluginLoaded } from "../../utils/gsapSoftReload";
|
||||
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
||||
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
||||
import { createStableContext } from "../../utils/hmrStableContext";
|
||||
|
||||
// Timeline gets a generous default height so the preview isn't oversized and the
|
||||
// tracks have room to breathe (CapCut-style). Users can still drag the divider.
|
||||
@@ -55,7 +48,7 @@ export interface NLEContextValue {
|
||||
setPreviewCompositionSize: (size: { width: number; height: number } | null) => void;
|
||||
}
|
||||
|
||||
const NLEContext = createContext<NLEContextValue | null>(null);
|
||||
const NLEContext = createStableContext<NLEContextValue | null>("NLEContext", null);
|
||||
|
||||
export function useNLEContext(): NLEContextValue {
|
||||
const ctx = useContext(NLEContext);
|
||||
|
||||
Reference in New Issue
Block a user