expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
183 lines • 8.32 kB
JavaScript
'use client';
Object.defineProperty(exports, "__esModule", { value: true });
exports.StackRouter = exports.stackRouterOverride = void 0;
const native_1 = require("@react-navigation/native");
const native_stack_1 = require("@react-navigation/native-stack");
const withLayoutContext_1 = require("./withLayoutContext");
const useScreens_1 = require("../useScreens");
const Protected_1 = require("../views/Protected");
const NativeStackNavigator = (0, native_stack_1.createNativeStackNavigator)().Navigator;
const RNStack = (0, withLayoutContext_1.withLayoutContext)(NativeStackNavigator);
function isStackAction(action) {
return (action.type === 'PUSH' ||
action.type === 'NAVIGATE' ||
action.type === 'POP' ||
action.type === 'POP_TO_TOP' ||
action.type === 'REPLACE');
}
/**
* React Navigation matches a screen by its name or a 'getID' function that uniquely identifies a screen.
* When a screen has been uniquely identified, the Stack can only have one instance of that screen.
*
* Expo Router allows for a screen to be matched by name and path params, a 'getID' function or a singular id.
*
* Instead of reimplementing the entire StackRouter, we can override the getStateForAction method to handle the singular screen logic.
*
*/
const stackRouterOverride = (original) => {
return {
getStateForAction: (state, action, options) => {
if (action.target && action.target !== state.key) {
return null;
}
if (!isStackAction(action)) {
return original.getStateForAction(state, action, options);
}
// The dynamic getId added to an action, `router.push('screen', { singular: true })`
const actionSingularOptions = action.payload && 'singular' in action.payload
? action.payload.singular
: undefined;
// Handle if 'getID' or 'singular' is set.
function getIdFunction(fn) {
// Actions can be fired by the user, so we do need to validate their structure.
if (!('payload' in action) ||
!action.payload ||
!('name' in action.payload) ||
typeof action.payload.name !== 'string') {
return;
}
const name = action.payload.name;
return (
// The dynamic singular added to an action, `router.push('screen', { singular: () => 'id' })`
getActionSingularIdFn(actionSingularOptions, name) ||
// The static getId added as a prop to `<Screen singular />` or `<Screen getId={} />`
options.routeGetIdList[name] ||
// The custom singular added by Expo Router to support its concept of `navigate`
fn);
}
switch (action.type) {
case 'PUSH': {
/**
* PUSH should always push
*
* If 'getID' or 'singular' is set and a match is found, instead of pushing a new screen,
* the existing screen will be moved to the HEAD of the stack. If there are multiple matches, the rest will be removed.
*/
const nextState = original.getStateForAction(state, action, {
...options,
routeGetIdList: {
...options.routeGetIdList,
[action.payload.name]: getIdFunction(),
},
});
/**
* React Navigation doesn't support dynamic getId function on the action. Because of this,
* can you enter a state where the screen is pushed multiple times but the normal getStateForAction
* doesn't remove the duplicates. We need to filter the state to only have singular screens.
*/
return actionSingularOptions
? filterSingular(nextState, actionSingularOptions)
: nextState;
}
case 'NAVIGATE': {
/**
* NAVIGATE should push unless the current name & route params of the current and target screen match.
* Search params and hashes should be ignored.
*
* If the name, route params & search params match, no action is taken.
* If both the name and route params match, the screen is replaced.
* If the name / route params do not match, the screen is pushed.
*
* If 'getID' or 'singular' is set and a match is found, instead of pushing a new screen,
* the existing screen will be moved to the HEAD of the stack. If there are multiple matches, the rest will be removed.
*/
const nextState = original.getStateForAction(state, action, {
...options,
routeGetIdList: {
...options.routeGetIdList,
[action.payload.name]: getIdFunction((options) => {
return (0, useScreens_1.getSingularId)(action.payload.name, options);
}),
},
});
/**
* React Navigation doesn't support dynamic getId function on the action. Because of this,
* can you enter a state where the screen is pushed multiple times but the normal getStateForAction
* doesn't remove the duplicates. We need to filter the state to only have singular screens.
*/
return actionSingularOptions
? filterSingular(nextState, actionSingularOptions)
: nextState;
}
default: {
return original.getStateForAction(state, action, options);
}
}
},
};
};
exports.stackRouterOverride = stackRouterOverride;
function getActionSingularIdFn(actionGetId, name) {
if (typeof actionGetId === 'function') {
return (options) => actionGetId(name, options.params ?? {});
}
else if (actionGetId === true) {
return (options) => (0, useScreens_1.getSingularId)(name, options);
}
return undefined;
}
/**
* If there is a dynamic singular on an action, then we need to filter the state to only have singular screens.
* As multiples may have been added before we did the singular navigation.
*/
function filterSingular(state, singular) {
if (!state || !singular) {
return state;
}
if (!state.routes) {
return state;
}
const currentIndex = state.index || state.routes.length - 1;
const current = state.routes[currentIndex];
const name = current.name;
const getId = getActionSingularIdFn(singular, name);
if (!getId) {
return state;
}
const id = getId({ params: current.params });
if (!id) {
return state;
}
// TypeScript needs a type assertion here for the filter to work.
let routes = state.routes;
routes = routes.filter((route, index) => {
// If the route is the current route, keep it.
if (index === currentIndex) {
return true;
}
// Remove all other routes with the same name and id.
return name !== route.name || id !== getId({ params: route.params });
});
return {
...state,
index: routes.length - 1,
routes,
};
}
const Stack = Object.assign((props) => {
return <RNStack {...props} UNSTABLE_router={exports.stackRouterOverride}/>;
}, {
Screen: RNStack.Screen,
Protected: Protected_1.Protected,
});
exports.default = Stack;
const StackRouter = (options) => {
const router = (0, native_1.StackRouter)(options);
return {
...router,
...(0, exports.stackRouterOverride)(router),
};
};
exports.StackRouter = StackRouter;
//# sourceMappingURL=StackClient.js.map
;