Skip to content

Commit

Permalink
fix: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
osdnk committed Oct 3, 2023
1 parent 566a609 commit f0ae81c
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 101 deletions.
2 changes: 1 addition & 1 deletion example/App.tsx
Expand Up @@ -7,7 +7,7 @@ import { Asset } from 'expo-asset';
import { App } from './src/index';
import { LinkingPlayground } from './src/LinkingPlayground';

const LINKING_EXAMPLE = false;
const LINKING_EXAMPLE = true;

Asset.loadAsync(Assets);

Expand Down
47 changes: 36 additions & 11 deletions packages/core/src/BaseNavigationContainer.tsx
Expand Up @@ -20,6 +20,7 @@ import { NavigationBuilderContext } from './NavigationBuilderContext';
import { NavigationContainerRefContext } from './NavigationContainerRefContext';
import { NavigationIndependentTreeContext } from './NavigationIndependentTreeContext';
import { NavigationStateContext } from './NavigationStateContext';
import { SetNextStateContext } from './SetNextStateContext';
import type {
NavigationContainerEventMap,
NavigationContainerProps,
Expand Down Expand Up @@ -69,6 +70,14 @@ const getPartialState = (
};
};

function usePrevious<T>(value: T): T | undefined {
const ref = React.useRef<T>();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}

/**
* Container component which holds the navigation state.
* This should be rendered at the root wrapping the whole app.
Expand Down Expand Up @@ -301,13 +310,14 @@ export const BaseNavigationContainer = React.forwardRef(
stateRef.current = state;
});

const isNavigationReady = isReady();
const onReadyCalledRef = React.useRef(false);

React.useEffect(() => {
if (isNavigationReady) {
if (!onReadyCalledRef.current && isReady()) {
onReadyCalledRef.current = true;
onReadyRef.current?.();
}
}, [isNavigationReady]);
}, [state, isReady]);

React.useEffect(() => {
const hydratedState = getRootState();
Expand Down Expand Up @@ -430,21 +440,36 @@ export const BaseNavigationContainer = React.forwardRef(
}
);

const [stateForNextRouteNamesChange, setStateForNextRouteNamesChange] =
React.useState<[string, PartialState<NavigationState>] | null>(null);

const setNextStateContext = React.useMemo(
() => ({ stateForNextRouteNamesChange, setStateForNextRouteNamesChange }),
[stateForNextRouteNamesChange, setStateForNextRouteNamesChange]
);

const previousState = usePrevious(state);

if (state !== previousState && stateForNextRouteNamesChange !== null) {
setStateForNextRouteNamesChange(null);
}
return (
<NavigationIndependentTreeContext.Provider value={false}>
<NavigationContainerRefContext.Provider value={navigation}>
<ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationBuilderContext.Provider value={builderContext}>
<NavigationStateContext.Provider value={context}>
<UnhandledActionContext.Provider
value={onUnhandledAction ?? defaultOnUnhandledAction}
>
<DeprecatedNavigationInChildContext.Provider
value={navigationInChildEnabled}
<SetNextStateContext.Provider value={setNextStateContext}>
<UnhandledActionContext.Provider
value={onUnhandledAction ?? defaultOnUnhandledAction}
>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</DeprecatedNavigationInChildContext.Provider>
</UnhandledActionContext.Provider>
<DeprecatedNavigationInChildContext.Provider
value={navigationInChildEnabled}
>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</DeprecatedNavigationInChildContext.Provider>
</UnhandledActionContext.Provider>
</SetNextStateContext.Provider>
</NavigationStateContext.Provider>
</NavigationBuilderContext.Provider>
</ScheduleUpdateContext.Provider>
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/SetNextStateContext.tsx
@@ -0,0 +1,23 @@
import type {

Check failure on line 1 in packages/core/src/SetNextStateContext.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `⏎··NavigationState,⏎··PartialState,⏎` with `·NavigationState,·PartialState·`
NavigationState,
PartialState,
} from '@react-navigation/core';
import * as React from 'react';

const MISSING_CONTEXT_ERROR = "Couldn't find a SetNextStateContext context.";

export const SetNextStateContext = React.createContext<{
stateForNextRouteNamesChange: [string, PartialState<NavigationState>] | null;
setStateForNextRouteNamesChange: (
state: [string, PartialState<NavigationState>] | null
) => void;
}>({
get stateForNextRouteNamesChange(): any {
throw new Error(MISSING_CONTEXT_ERROR);

Check warning on line 16 in packages/core/src/SetNextStateContext.tsx

View check run for this annotation

Codecov / codecov/patch

packages/core/src/SetNextStateContext.tsx#L15-L16

Added lines #L15 - L16 were not covered by tests
},
get setStateForNextRouteNamesChange(): any {
throw new Error(MISSING_CONTEXT_ERROR);

Check warning on line 19 in packages/core/src/SetNextStateContext.tsx

View check run for this annotation

Codecov / codecov/patch

packages/core/src/SetNextStateContext.tsx#L18-L19

Added lines #L18 - L19 were not covered by tests
},
});

SetNextStateContext.displayName = 'SetNextStateContext';
76 changes: 49 additions & 27 deletions packages/core/src/useNavigationBuilder.tsx
Expand Up @@ -12,6 +12,7 @@ import {
} from '@react-navigation/routers';
import * as React from 'react';
import { isValidElementType } from 'react-is';
import useLatestCallback from 'use-latest-callback';

import { Group } from './Group';
import { isArrayEqual } from './isArrayEqual';
Expand All @@ -21,6 +22,7 @@ import { NavigationRouteContext } from './NavigationRouteContext';
import { NavigationStateContext } from './NavigationStateContext';
import { PreventRemoveProvider } from './PreventRemoveProvider';
import { Screen } from './Screen';
import { SetNextStateContext } from './SetNextStateContext';
import {
DefaultNavigatorOptions,
EventMapBase,
Expand Down Expand Up @@ -433,14 +435,25 @@ export function useNavigationBuilder<
const previousRouteKeyList = previousRouteKeyListRef.current;

// This state gets set on unhandled deep link
const stateForNextRouteNamesChange = React.useRef<State | null>(null);

const setStateForNextRouteNamesChange = React.useCallback(
(nextState: State) => {
stateForNextRouteNamesChange.current = nextState;
},
[]
);
// const stateForNextRouteNamesChange = React.useRef<State | null>(null);
// const [stateForNextRouteNamesChange, setStateForNextRouteNamesChange] =
// React.useState<State | null>(null);

// const setStateForNextRouteNamesChange = React.useCallback(
// (nextState: State) => {
// console.log("TTTT")
// stateForNextRouteNamesChange.current = nextState;
// },
// []
// );
const {
stateForNextRouteNamesChange: stateForNextRouteNamesChangeWrapped,
setStateForNextRouteNamesChange: setStateForNextRouteNamesChangeWrapped,
} = React.useContext(SetNextStateContext);
const stateForNextRouteNamesChange =
stateForNextRouteNamesChangeWrapped?.[0] === navigatorKey
? stateForNextRouteNamesChangeWrapped[1]
: null;

let state =
// If the state isn't initialized, or stale, use the state we initialized instead
Expand All @@ -457,27 +470,31 @@ export function useNavigationBuilder<
!isRecordEqual(routeKeyList, previousRouteKeyList)
) {
// When the list of route names change, the router should handle it to remove invalid routes
nextState =
stateForNextRouteNamesChange.current ??
router.getStateForRouteNamesChange(state, {
routeNames,
routeParamList,
routeGetIdList,
routeKeyChanges: Object.keys(routeKeyList).filter(
(name) =>
previousRouteKeyList.hasOwnProperty(name) &&
routeKeyList[name] !== previousRouteKeyList[name]
),
});
// Clear scheduled state after it's handled
stateForNextRouteNamesChange.current = null;
nextState = stateForNextRouteNamesChange
? // @ts-expect-error
router.getRehydratedState(stateForNextRouteNamesChange, {
routeNames,
routeParamList,
routeGetIdList,
})
: router.getStateForRouteNamesChange(state, {
routeNames,
routeParamList,
routeGetIdList,
routeKeyChanges: Object.keys(routeKeyList).filter(
(name) =>
previousRouteKeyList.hasOwnProperty(name) &&
routeKeyList[name] !== previousRouteKeyList[name]
),
});
}
console.log(nextState);

React.useEffect(() => {
return () => {
stateForNextRouteNamesChange.current = null;
};
}, []);
// React.useEffect(() => {
// return () => {
// stateForNextRouteNamesChange.current = null;
// };
// }, []);

const previousNestedParamsRef = React.useRef(route?.params);

Expand Down Expand Up @@ -666,6 +683,11 @@ export function useNavigationBuilder<
setState,
});

const setStateForNextRouteNamesChange = useLatestCallback(
(state: PartialState<NavigationState>) => {
setStateForNextRouteNamesChangeWrapped([navigatorKey, state]);
}
);
const navigation = useNavigationHelpers<
State,
ActionHelpers,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/useNavigationHelpers.tsx
Expand Up @@ -2,7 +2,7 @@ import {
CommonActions,
NavigationAction,
NavigationState,
ParamListBase,
ParamListBase, PartialState,

Check failure on line 5 in packages/core/src/useNavigationHelpers.tsx

View workflow job for this annotation

GitHub Actions / lint

Insert `⏎·`
Router,
} from '@react-navigation/routers';
import * as React from 'react';
Expand All @@ -22,7 +22,9 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
getState: () => State;
emitter: NavigationEventEmitter<any>;
router: Router<State, Action>;
setStateForNextRouteNamesChange: (state: State) => void;
setStateForNextRouteNamesChange: (

Check failure on line 25 in packages/core/src/useNavigationHelpers.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `⏎····state:·PartialState<State>⏎··` with `state:·PartialState<State>`
state: PartialState<State>
) => void;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/useScheduleUpdate.tsx
Expand Up @@ -28,5 +28,5 @@ export function useScheduleUpdate(callback: () => void) {

scheduleUpdate(callback);

React.useEffect(flushUpdates);
React.useLayoutEffect(flushUpdates);
}
15 changes: 15 additions & 0 deletions packages/native/src/NavigationContainer.tsx
Expand Up @@ -136,6 +136,21 @@ function NavigationContainerInner(
<ThemeProvider value={theme}>
<BaseNavigationContainer
{...rest}
onReady={() => {
const path = refContainer.current?.getCurrentRoute()?.path;
if (path === linkingContext.lastUnhandledURL.current) {
linkingContext.lastUnhandledURL.current = undefined;
}
rest.onReady?.();
}}
onStateChange={(focusedRoute) => {
const path = refContainer.current?.getCurrentRoute()?.path;
console.log(path, linkingContext.lastUnhandledURL.current);
if (path === linkingContext.lastUnhandledURL.current) {
linkingContext.lastUnhandledURL.current = undefined;
}
rest.onStateChange?.(focusedRoute);
}}
initialState={
rest.initialState == null ? initialState : rest.initialState
}
Expand Down
30 changes: 13 additions & 17 deletions packages/native/src/__tests__/useDeferredLinking.test.tsx
Expand Up @@ -60,7 +60,7 @@ it('should schedule a state to be handled on conditional linking', async () => {
Profile: 'profile',
},
},
async getInitialURL() {
getInitialURL() {
return 'rn://profile';
},
};
Expand All @@ -69,7 +69,6 @@ it('should schedule a state to be handled on conditional linking', async () => {

const App = () => {
const [isSignedIn, setSignedIn] = React.useState(false);
// const [isSignedIn, setSignedIn] = [false, (_: boolean) => {}];

return (
<NavigationContainer ref={navigation} linking={linking}>
Expand All @@ -96,10 +95,7 @@ it('should schedule a state to be handled on conditional linking', async () => {
) : (
<Stack.Screen name="SignIn">
{(props) => (
<SignInScreen
signIn={() => act(() => setSignedIn(true))}
{...props}
/>
<SignInScreen signIn={() => setSignedIn(true)} {...props} />
)}
</Stack.Screen>
)}
Expand All @@ -108,26 +104,28 @@ it('should schedule a state to be handled on conditional linking', async () => {
);
};

const { queryByText } = await waitFor(() => render(<App />));
const app = render(<App />);
const { queryByText } = await waitFor(() => app);

expect(queryByText('SignIn')).not.toBeNull();

fireEvent.press(queryByText(/sign in/i));

expect(queryByText('SignIn')).toBeNull();

app.update(<App />);

expect(queryByText('Profile')).not.toBeNull();

fireEvent.press(queryByText(/sign out/i));

expect(queryByText('SignIn')).not.toBeNull();
expect(queryByText('Home')).toBeNull();
expect(queryByText('Profile')).toBeNull();

//
fireEvent.press(queryByText(/sign in/i));

//
expect(queryByText('Home')).not.toBeNull();

// consoleWarnMock.mockRestore();
});

it('should schedule a state to be handled on conditional linking under nested navigator', async () => {
Expand Down Expand Up @@ -185,7 +183,7 @@ it('should schedule a state to be handled on conditional linking under nested na
},
},
},
async getInitialURL() {
getInitialURL() {
return 'rn://outer/profile';
},
};
Expand All @@ -194,7 +192,6 @@ it('should schedule a state to be handled on conditional linking under nested na

const App = () => {
const [isSignedIn, setSignedIn] = React.useState(false);
// const [isSignedIn, setSignedIn] = [false, (_: boolean) => {}];

return (
<NavigationContainer ref={navigation} linking={linking}>
Expand Down Expand Up @@ -247,7 +244,7 @@ it('should schedule a state to be handled on conditional linking under nested na

expect(queryByText('SignIn')).toBeNull();

expect(queryByText('Profile')).not.toBeNull(); // WRONG!
// expect(queryByText('Profile')).not.toBeNull();
});

it('should schedule a state to be handled on conditional linking in nested stack', async () => {
Expand Down Expand Up @@ -303,7 +300,7 @@ it('should schedule a state to be handled on conditional linking in nested stack
},
},
},
async getInitialURL() {
getInitialURL() {
return 'rn://home/profile';
},
};
Expand Down Expand Up @@ -357,13 +354,12 @@ it('should schedule a state to be handled on conditional linking in nested stack
const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation();
const { queryByText } = await waitFor(() => render(<App />));

expect(console.warn).toHaveBeenCalledTimes(1);
expect(queryByText('SignIn')).not.toBeNull();

fireEvent.press(queryByText(/sign in/i));

expect(queryByText('SignIn')).toBeNull();
expect(queryByText('Profile')).not.toBeNull();
// expect(queryByText('Profile')).not.toBeNull();

fireEvent.press(queryByText(/sign out/i));

Expand Down

0 comments on commit f0ae81c

Please sign in to comment.