Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5.2k
fix(mobile): Stabilize native stack option updates#4037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
7996467f4457a8f35646626bb099f3d9691d73fe59File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -11,6 +11,7 @@ import { | ||
| useEffect, | ||
| useLayoutEffect, | ||
| useMemo, | ||
| useRef, | ||
| type ReactElement, | ||
| type ReactNode, | ||
| } from "react"; | ||
| @@ -61,20 +62,116 @@ function normalizeScreenOptions( | ||
| return normalized as NativeStackNavigationOptions; | ||
| } | ||
| function optionsSignature(value: unknown, seen = new WeakSet<object>()): string { | ||
| if (value === null) return "null"; | ||
| switch (typeof value) { | ||
| case "boolean": | ||
| case "number": | ||
| case "string": | ||
| return JSON.stringify(value); | ||
| case "undefined": | ||
| return "undefined"; | ||
| case "function": | ||
| // Header factories are frequently recreated inline. Their source is | ||
| // stable across equivalent renders, while a reference comparison would | ||
| // make navigation.setOptions re-enter the navigator indefinitely. | ||
| return `function:${Function.prototype.toString.call(value)}`; | ||
| case "symbol": | ||
| return `symbol:${String(value)}`; | ||
| case "bigint": | ||
| return `bigint:${String(value)}`; | ||
| case "object": { | ||
| const object = value as object; | ||
| if (seen.has(object)) return "[circular]"; | ||
| seen.add(object); | ||
| if (Array.isArray(value)) { | ||
| return `[${value.map((entry) => optionsSignature(entry, seen)).join(",")}]`; | ||
| } | ||
| // React refs carry mutable native instances that must not make static | ||
| // screen options appear different after every render. | ||
| if ("current" in object) return "[ref]"; | ||
| return `{${Object.keys(value as Record<string, unknown>) | ||
| .sort() | ||
| .map( | ||
| (key) => | ||
| `${JSON.stringify(key)}:${optionsSignature((value as Record<string, unknown>)[key], seen)}`, | ||
| ) | ||
| .join(",")}}`; | ||
| } | ||
| } | ||
| return String(value); | ||
| } | ||
| function stabilizeOptionFunctions( | ||
juliusmarminge marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| value: unknown, | ||
| path: string, | ||
| latestFunctions: Map<string, (...args: unknown[]) => unknown>, | ||
| wrappers: Map<string, (...args: unknown[]) => unknown>, | ||
| seen = new WeakSet<object>(), | ||
| ): unknown { | ||
| if (typeof value === "function") { | ||
| latestFunctions.set(path, value as (...args: unknown[]) => unknown); | ||
| let wrapper = wrappers.get(path); | ||
| if (!wrapper) { | ||
| wrapper = (...args: unknown[]) => { | ||
| return latestFunctions.get(path)?.(...args); | ||
| }; | ||
| wrappers.set(path, wrapper); | ||
| } | ||
| return wrapper; | ||
| } | ||
| if (Array.isArray(value)) { | ||
| if (seen.has(value)) return value; | ||
| seen.add(value); | ||
| return value.map((entry, index) => | ||
| stabilizeOptionFunctions(entry, `${path}[${index}]`, latestFunctions, wrappers, seen), | ||
| ); | ||
| } | ||
| if (value !== null && typeof value === "object") { | ||
| if (seen.has(value) || "current" in value) return value; | ||
| seen.add(value); | ||
| return Object.fromEntries( | ||
| Object.entries(value as Record<string, unknown>).map(([key, entry]) => [ | ||
| key, | ||
| stabilizeOptionFunctions(entry, `${path}.${key}`, latestFunctions, wrappers, seen), | ||
| ]), | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| export function NativeStackScreenOptions(props: { | ||
| readonly options?: AppNativeStackNavigationOptions; | ||
| readonly listeners?: Record<string, (event: never) => void>; | ||
| readonly name?: string; | ||
| }) { | ||
| const navigation = useNativeStackNavigation(); | ||
| const lastAppliedOptionsSignatureRef = useRef<string | undefined>(undefined); | ||
| const latestOptionFunctionsRef = useRef(new Map<string, (...args: unknown[]) => unknown>()); | ||
| const optionFunctionWrappersRef = useRef(new Map<string, (...args: unknown[]) => unknown>()); | ||
| const normalizedOptions = useMemo(() => normalizeScreenOptions(props.options), [props.options]); | ||
| const stableOptions = normalizedOptions | ||
| ? (stabilizeOptionFunctions( | ||
| normalizedOptions, | ||
| "options", | ||
| latestOptionFunctionsRef.current, | ||
| optionFunctionWrappersRef.current, | ||
| ) as NativeStackNavigationOptions) | ||
| : undefined; | ||
| useLayoutEffect(() => { | ||
| if (!navigation || !normalizedOptions) { | ||
| if (!navigation || !stableOptions) { | ||
| return; | ||
| } | ||
| const signature = optionsSignature(stableOptions); | ||
| // Avoid re-entering navigation state when semantically equal options are | ||
| // reapplied every layout (common when callers pass unstable object literals). | ||
| if (lastAppliedOptionsSignatureRef.current === signature) { | ||
| return; | ||
| } | ||
| navigation.setOptions(normalizedOptions); | ||
| }, [navigation, normalizedOptions]); | ||
| lastAppliedOptionsSignatureRef.current = signature; | ||
| navigation.setOptions(stableOptions); | ||
| }, [navigation, stableOptions]); | ||
| useEffect(() => { | ||
| if (!navigation || !props.listeners) { | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.