import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { attachActionListener } from './actionListener';
import { ObserveReactNavigationIntegrationContext } from './context';
import { createStateChangeHandler } from './handleStateChange';
import { isInitialized } from './init';
import { optionalReactNavigation } from './reactNavigation';
import { createReactNavigationIntegrationStorage } from './storage';
import type { NavigationContainerRefLike, NavigationStateLike } from './types';
import { useAssertValueDoesNotChange } from '../../useAssertValueDoesNotChange';
export interface ObserveNavigationProviderProps {
/**
* The navigation ref the app passes to its navigation container. Create it
* with `useNavigationContainerRef()` (or `createNavigationContainerRef()`)
* and pass the same ref to the container, for example
* ``.
*/
navigationRef: NavigationContainerRefLike;
children: ReactNode;
}
/**
* Instruments React Navigation through a caller-provided `navigationRef`, for
* setups where the container isn't yours to replace — most notably
* [static configuration](https://reactnavigation.org/docs/static-configuration/).
* Must be an ancestor of every screen so `useObserve()` works inside them.
*
* ```tsx
*
*
*
* ```
*/
export function ObserveNavigationProvider({
navigationRef,
children,
}: ObserveNavigationProviderProps) {
if (!optionalReactNavigation) {
throw new Error(
'[expo-observe] ObserveNavigationProvider requires @react-navigation/native, ' +
"but the package couldn't be resolved. Install @react-navigation/native, or " +
"remove the React Navigation integration if it's not needed."
);
}
if (
typeof navigationRef?.addListener !== 'function' ||
typeof navigationRef?.getRootState !== 'function' ||
typeof navigationRef?.isReady !== 'function'
) {
throw new Error(
'[expo-observe] ObserveNavigationProvider received a `navigationRef` that is not a ' +
'navigation container ref, so it cannot listen to navigation events. Create the ref ' +
'with `useNavigationContainerRef()` (or `createNavigationContainerRef()`) and pass ' +
'the same ref to both your navigation container and ObserveNavigationProvider.'
);
}
const initialized = isInitialized();
const [internal] = useState(() => {
if (!initialized) return null;
const storage = createReactNavigationIntegrationStorage();
return {
storage,
handleStateChange: createStateChangeHandler(storage, performance.now()),
};
});
useAssertValueDoesNotChange(
initialized,
`[expo-observe] React Navigation integration was toggled after ObserveNavigationProvider mounted. Call \`Observe.configure({ integrations: { 'react-navigation': true } })\` before rendering ObserveNavigationProvider.`
);
useEffect(() => {
if (!internal) return;
// The `state` event payload can be partial on the initial commit, so
// always read the hydrated state from the ref.
const processRootState = () => {
const rootState = navigationRef.getRootState() as unknown as NavigationStateLike | undefined;
if (rootState) {
internal.handleStateChange(rootState);
}
};
const detachActionListener = attachActionListener(navigationRef, internal.storage);
// Unlike the `onStateChange` prop, the `state` ref event also fires for
// the initial commit — but child effects run before parent effects, so by
// the time this effect subscribes, the container rendered below has
// already emitted it. The `isReady()` catch-up reads that missed initial
// state directly; `handleStateChange` dedupes by focused route key, so if
// the listener also sees the same state, the first screen counts once.
const detachStateListener = navigationRef.addListener('state', processRootState);
if (navigationRef.isReady()) {
processRootState();
}
return () => {
detachActionListener();
detachStateListener();
};
}, [internal, navigationRef]);
const contextValue = useMemo(() => (internal ? { storage: internal.storage } : null), [internal]);
return (
{children}
);
}