next
Version:
The React Framework
235 lines (234 loc) • 10.2 kB
JavaScript
import { ACTION_REFRESH, ACTION_SERVER_ACTION, ACTION_NAVIGATE, ACTION_RESTORE, ScrollBehavior } from './router-reducer/router-reducer-types';
import { reducer } from './router-reducer/router-reducer';
import { startTransition } from 'react';
import { isThenable } from '../../shared/lib/is-thenable';
import { navigate } from './app-router-state';
import { dispatchGestureState } from './use-action-queue';
import { FreshnessPolicy } from './render-tree';
import { addBasePath } from '../add-base-path';
import { isExternalURL } from './app-router-utils';
import { isJavaScriptURLString } from '../lib/javascript-url';
import { push, replace, refresh, hmrRefresh } from './navigator';
import { prefetchRoute } from './prefetch';
function runRemainingActions(actionQueue, settledAction, setState) {
// Only advance the queue if the settled action is still at its head. If a
// navigation discarded this action, the navigation took its place and is
// still in flight — starting the next queued action now would run it
// against router state that doesn't include the navigation yet.
if (actionQueue.pending === settledAction) {
actionQueue.pending = settledAction.next;
if (actionQueue.pending !== null) {
runAction({
actionQueue,
action: actionQueue.pending,
setState
});
return;
}
}
if (actionQueue.pending === null) {
if (actionQueue.wasPreempted) {
actionQueue.wasPreempted = false;
// When an action is preempted, later actions can update the queue's state without React rendering it.
// Once the queue is empty, publish the final state so the UI catches up.
startTransition(()=>setState(actionQueue.state));
}
if (actionQueue.needsRefresh) {
// The queue is idle; flush the refresh requested by a discarded server
// action that revalidated data.
actionQueue.needsRefresh = false;
actionQueue.dispatch({
type: ACTION_REFRESH
}, setState);
}
}
}
function runAction({ actionQueue, action, setState }) {
const prevState = actionQueue.state;
actionQueue.pending = action;
const payload = action.payload;
function handleError(err) {
runRemainingActions(actionQueue, action, setState);
action.reject(err);
}
function handleResult(nextState) {
// if we discarded this action, the state should also be discarded
if (action.discarded) {
// Check if the discarded server action revalidated data
if (action.payload.type === ACTION_SERVER_ACTION && action.payload.didRevalidate) {
// The server action was discarded but it revalidated data,
// mark that we need to refresh after all actions complete
actionQueue.needsRefresh = true;
}
// This can't advance the queue (this action is no longer its head), but
// if the queue has already drained, it flushes the refresh now.
runRemainingActions(actionQueue, action, setState);
return;
}
actionQueue.state = nextState;
runRemainingActions(actionQueue, action, setState);
action.resolve(nextState);
}
let actionResult;
try {
actionResult = actionQueue.action(prevState, payload);
} catch (err) {
handleError(err);
return;
}
// if the action is a promise, set up a callback to resolve it
if (isThenable(actionResult)) {
actionResult.then(handleResult, handleError);
} else {
handleResult(actionResult);
}
}
function dispatchAction(actionQueue, payload, setState) {
let resolvers = {
resolve: setState,
reject: ()=>{}
};
// most of the action types are async with the exception of restore
// it's important that restore is handled quickly since it's fired on the popstate event
// and we don't want to add any delay on a back/forward nav
// this only creates a promise for the async actions
if (payload.type !== ACTION_RESTORE) {
// Create the promise and assign the resolvers to the object.
const deferredPromise = new Promise((resolve, reject)=>{
resolvers = {
resolve,
reject
};
});
startTransition(()=>{
// we immediately notify React of the pending promise -- the resolver is attached to the action node
// and will be called when the associated action promise resolves
setState(deferredPromise);
});
}
const newAction = {
payload,
next: null,
resolve: resolvers.resolve,
reject: resolvers.reject
};
// Check if the queue is empty
if (actionQueue.pending === null) {
// The queue is empty, so add the action and start it immediately
// Mark this action as the last in the queue
actionQueue.last = newAction;
runAction({
actionQueue,
action: newAction,
setState
});
} else if (payload.type === ACTION_NAVIGATE || payload.type === ACTION_RESTORE) {
// Navigations (including back/forward) take priority over any pending actions.
// Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately.
actionQueue.pending.discarded = true;
actionQueue.wasPreempted = true;
// The rest of the current queue should still execute after this navigation.
// (Note that it can't contain any earlier navigations, because we always put those into `actionQueue.pending` by calling `runAction`)
newAction.next = actionQueue.pending.next;
if (actionQueue.last === actionQueue.pending) {
actionQueue.last = newAction;
}
runAction({
actionQueue,
action: newAction,
setState
});
} else {
// The queue is not empty, so add the action to the end of the queue
// It will be started by runRemainingActions after the previous action finishes
if (actionQueue.last !== null) {
actionQueue.last.next = newAction;
}
actionQueue.last = newAction;
}
}
let globalActionQueue = null;
export function createMutableActionQueue(initialState) {
const actionQueue = {
state: initialState,
dispatch: (payload, setState)=>dispatchAction(actionQueue, payload, setState),
action: reducer,
pending: null,
last: null
};
if (typeof window !== 'undefined') {
// The action queue is lazily created on hydration, but after that point
// it doesn't change. So we can store it in a global rather than pass
// it around everywhere via props/context.
if (globalActionQueue !== null) {
throw new Error('Internal Next.js Error: createMutableActionQueue was called more ' + 'than once');
}
globalActionQueue = actionQueue;
}
return actionQueue;
}
export function getCurrentAppRouterState() {
return globalActionQueue !== null ? globalActionQueue.state : null;
}
/**
* (Experimental) Perform a gesture navigation. This dispatches through React's
* useOptimistic instead of the main action queue, allowing the state to be
* shown during a gesture transition and discarded when the canonical navigation
* completes.
*
* Only available when experimental.gestureTransition is enabled.
*/ function gesturePush(href, options) {
if (process.env.__NEXT_GESTURE_TRANSITION) {
// TODO: Trigger a prefetch so the cache starts populating if there isn't
// already a prefetch for this route.
if (isJavaScriptURLString(href)) {
throw new Error('Next.js has blocked a javascript: URL as a security precaution.');
}
const state = getCurrentAppRouterState();
if (state === null) {
return;
}
const url = new URL(addBasePath(href), location.href);
if (isExternalURL(url)) {
return;
}
// Fork the router state for the duration of the gesture transition.
const currentUrl = new URL(state.canonicalUrl, location.href);
const scrollBehavior = options?.scroll === false ? ScrollBehavior.NoScroll : ScrollBehavior.Default;
// This is a special freshness policy that prevents dynamic requests from
// being spawned. During the gesture, we should only show the cached
// prefetched UI, not dynamic data.
// TODO: In the case of navigations to an unknown route, this will still
// end up performing a dynamic request. The plan is to do prefetch instead.
// There's a separate TODO for this.
const freshnessPolicy = FreshnessPolicy.Gesture;
const forkedGestureState = navigate(state, url, currentUrl, state.renderedSearch, state.root, state.tree, state.nextUrl, freshnessPolicy, scrollBehavior, 'push');
dispatchGestureState(forkedGestureState);
}
}
/**
* The app router that is exposed through `useRouter`. These are public API
* methods. Internal Next.js code should call the lower level methods directly
* (although there's lots of existing code that doesn't do that).
*/ export const publicAppRouterInstance = {
back: ()=>window.history.back(),
forward: ()=>window.history.forward(),
prefetch: prefetchRoute,
replace: replace,
push: push,
refresh: refresh,
hmrRefresh: hmrRefresh,
// Default value. Each route segment provides its own value at runtime. Refer
// to `useRouter()`.
bfcacheId: '0'
};
// Conditionally add experimental_gesturePush when gestureTransition is enabled
if (process.env.__NEXT_GESTURE_TRANSITION) {
;
publicAppRouterInstance.experimental_gesturePush = gesturePush;
}
// Exists for debugging purposes. Don't use in application code.
if (typeof window !== 'undefined' && window.next) {
window.next.router = publicAppRouterInstance;
}
//# sourceMappingURL=app-router-instance.js.map