mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 12:55:21 +08:00
fix: workaround warning about setState in another component in render
This commit is contained in:
@@ -9,14 +9,15 @@ import {
|
||||
} from '@react-navigation/routers';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
||||
import useFocusedListeners from './useFocusedListeners';
|
||||
import useDevTools from './useDevTools';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useSyncState from './useSyncState';
|
||||
import isSerializable from './isSerializable';
|
||||
|
||||
import { NavigationContainerRef, NavigationContainerProps } from './types';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useSyncState from './useSyncState';
|
||||
|
||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
|
||||
@@ -112,7 +113,13 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
);
|
||||
}
|
||||
|
||||
const [state, getState, setState] = useSyncState<State>(() =>
|
||||
const [
|
||||
state,
|
||||
getState,
|
||||
setState,
|
||||
scheduleUpdate,
|
||||
flushUpdates,
|
||||
] = useSyncState<State>(() =>
|
||||
getPartialState(initialState == null ? undefined : initialState)
|
||||
);
|
||||
|
||||
@@ -218,6 +225,11 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
[addFocusedListener, trackAction, addStateGetter]
|
||||
);
|
||||
|
||||
const scheduleContext = React.useMemo(
|
||||
() => ({ scheduleUpdate, flushUpdates }),
|
||||
[scheduleUpdate, flushUpdates]
|
||||
);
|
||||
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
state,
|
||||
@@ -263,11 +275,13 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
}, [onStateChange, trackState, getRootState, emitter, state]);
|
||||
|
||||
return (
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</NavigationStateContext.Provider>
|
||||
</NavigationBuilderContext.Provider>
|
||||
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</NavigationStateContext.Provider>
|
||||
</NavigationBuilderContext.Provider>
|
||||
</ScheduleUpdateContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -626,7 +626,7 @@ it('updates route params with setParams applied to parent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles change in route names', () => {
|
||||
it('handles change in route names', async () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
@@ -635,7 +635,7 @@ it('handles change in route names', () => {
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const root = render(
|
||||
<BaseNavigationContainer onStateChange={onStateChange}>
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator initialRouteName="bar">
|
||||
<Screen name="foo" component={jest.fn()} />
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from './types';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import useOnGetState from './useOnGetState';
|
||||
import useScheduleUpdate from './useScheduleUpdate';
|
||||
|
||||
// This is to make TypeScript compiler happy
|
||||
// eslint-disable-next-line babel/no-unused-expressions
|
||||
@@ -308,16 +309,13 @@ export default function useNavigationBuilder<
|
||||
}
|
||||
|
||||
if (
|
||||
previousRouteRef.current &&
|
||||
route &&
|
||||
route.params &&
|
||||
typeof route.params.screen === 'string' &&
|
||||
route.params !== previousRouteRef.current.params
|
||||
typeof route?.params?.screen === 'string' &&
|
||||
route.params !== previousRouteRef.current?.params
|
||||
) {
|
||||
// If the route was updated with new name and/or params, we should navigate there
|
||||
// The update should be limited to current navigator only, so we call the router manually
|
||||
const updatedState = router.getStateForAction(
|
||||
state,
|
||||
nextState,
|
||||
CommonActions.navigate(route.params.screen, route.params.params),
|
||||
{
|
||||
routeNames,
|
||||
@@ -331,17 +329,17 @@ export default function useNavigationBuilder<
|
||||
routeNames,
|
||||
routeParamList,
|
||||
})
|
||||
: state;
|
||||
: nextState;
|
||||
}
|
||||
|
||||
const shouldUpdate = state !== nextState;
|
||||
|
||||
React.useEffect(() => {
|
||||
useScheduleUpdate(() => {
|
||||
if (shouldUpdate) {
|
||||
// If the state needs to be updated, we'll schedule an update with React
|
||||
// If the state needs to be updated, we'll schedule an update
|
||||
setState(nextState);
|
||||
}
|
||||
}, [nextState, setState, shouldUpdate]);
|
||||
});
|
||||
|
||||
// The up-to-date state will come in next render, but we don't need to wait for it
|
||||
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
|
||||
|
||||
32
packages/core/src/useScheduleUpdate.tsx
Normal file
32
packages/core/src/useScheduleUpdate.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const MISSING_CONTEXT_ERROR = "Couldn't find a schedule context.";
|
||||
|
||||
export const ScheduleUpdateContext = React.createContext<{
|
||||
scheduleUpdate: (callback: () => void) => void;
|
||||
flushUpdates: () => void;
|
||||
}>({
|
||||
scheduleUpdate() {
|
||||
throw new Error(MISSING_CONTEXT_ERROR);
|
||||
},
|
||||
flushUpdates() {
|
||||
throw new Error(MISSING_CONTEXT_ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* When screen config changes, we want to update the navigator in the same update phase.
|
||||
* However, navigation state is in the root component and React won't let us update it from a child.
|
||||
* This is a workaround for that, the scheduled update is stored in the ref without actually calling setState.
|
||||
* It lets all subsequent updates access the latest state so it stays correct.
|
||||
* Then we call setState during after the component updates.
|
||||
*/
|
||||
export default function useScheduleUpdate(callback: () => void) {
|
||||
const { scheduleUpdate, flushUpdates } = React.useContext(
|
||||
ScheduleUpdateContext
|
||||
);
|
||||
|
||||
scheduleUpdate(callback);
|
||||
|
||||
React.useEffect(flushUpdates);
|
||||
}
|
||||
@@ -2,8 +2,12 @@ import * as React from 'react';
|
||||
|
||||
const UNINTIALIZED_STATE = {};
|
||||
|
||||
/**
|
||||
* This is definitely not compatible with concurrent mode, but we don't have a solution for sync state yet.
|
||||
*/
|
||||
export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||
const stateRef = React.useRef<T>(UNINTIALIZED_STATE as any);
|
||||
const isSchedulingRef = React.useRef(false);
|
||||
|
||||
if (stateRef.current === UNINTIALIZED_STATE) {
|
||||
stateRef.current =
|
||||
@@ -11,7 +15,7 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||
typeof initialState === 'function' ? initialState() : initialState;
|
||||
}
|
||||
|
||||
const [state, setTrackingState] = React.useState(stateRef.current);
|
||||
const [trackingState, setTrackingState] = React.useState(stateRef.current);
|
||||
|
||||
const getState = React.useCallback(() => stateRef.current, []);
|
||||
|
||||
@@ -21,8 +25,35 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
|
||||
}
|
||||
|
||||
stateRef.current = state;
|
||||
setTrackingState(state);
|
||||
|
||||
if (!isSchedulingRef.current) {
|
||||
setTrackingState(state);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [state, getState, setState] as const;
|
||||
const scheduleUpdate = React.useCallback((callback: () => void) => {
|
||||
isSchedulingRef.current = true;
|
||||
|
||||
try {
|
||||
callback();
|
||||
} finally {
|
||||
isSchedulingRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const flushUpdates = React.useCallback(() => {
|
||||
// Make sure that the tracking state is up-to-date.
|
||||
// We call it unconditionally, but React should skip the update if state is unchanged.
|
||||
setTrackingState(stateRef.current);
|
||||
}, []);
|
||||
|
||||
// If we're rendering and the tracking state is out of date, update it immediately
|
||||
// This will make sure that our updates are applied as early as possible.
|
||||
if (trackingState !== stateRef.current) {
|
||||
setTrackingState(stateRef.current);
|
||||
}
|
||||
|
||||
const state = stateRef.current;
|
||||
|
||||
return [state, getState, setState, scheduleUpdate, flushUpdates] as const;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user