rsnext/packages/next/client/router.ts
JJ Kasper bbc1a21c74
Update to have default locale matched on root (#17669)
Follow-up PR to https://github.com/vercel/next.js/pull/17370 when the path is not prefixed with a locale and the default locale is the detected locale it doesn't redirect to locale prefixed variant. If the default locale path is visited and the default locale is visited this also redirects to the root removing the un-necessary locale in the URL. 

This also exposes the `defaultLocale` on the router since the RFC mentions `Setting a defaultLocale is required in every i18n library so it'd be useful for Next.js to provide it to the application.` although doesn't explicitly spec where we want to expose it. If we want to expose it differently this can be updated.
2020-10-08 11:12:17 +00:00

170 lines
4.5 KiB
TypeScript

/* global window */
import React from 'react'
import Router, { NextRouter } from '../next-server/lib/router/router'
import { RouterContext } from '../next-server/lib/router-context'
type ClassArguments<T> = T extends new (...args: infer U) => any ? U : any
type RouterArgs = ClassArguments<typeof Router>
type SingletonRouterBase = {
router: Router | null
readyCallbacks: Array<() => any>
ready(cb: () => any): void
}
export { Router, NextRouter }
export type SingletonRouter = SingletonRouterBase & NextRouter
const singletonRouter: SingletonRouterBase = {
router: null, // holds the actual router instance
readyCallbacks: [],
ready(cb: () => void) {
if (this.router) return cb()
if (typeof window !== 'undefined') {
this.readyCallbacks.push(cb)
}
},
}
// Create public properties and methods of the router in the singletonRouter
const urlPropertyFields = [
'pathname',
'route',
'query',
'asPath',
'components',
'isFallback',
'basePath',
'locale',
'locales',
'defaultLocale',
]
const routerEvents = [
'routeChangeStart',
'beforeHistoryChange',
'routeChangeComplete',
'routeChangeError',
'hashChangeStart',
'hashChangeComplete',
]
const coreMethodFields = [
'push',
'replace',
'reload',
'back',
'prefetch',
'beforePopState',
]
// Events is a static property on the router, the router doesn't have to be initialized to use it
Object.defineProperty(singletonRouter, 'events', {
get() {
return Router.events
},
})
urlPropertyFields.forEach((field) => {
// Here we need to use Object.defineProperty because, we need to return
// the property assigned to the actual router
// The value might get changed as we change routes and this is the
// proper way to access it
Object.defineProperty(singletonRouter, field, {
get() {
const router = getRouter() as any
return router[field] as string
},
})
})
coreMethodFields.forEach((field) => {
// We don't really know the types here, so we add them later instead
;(singletonRouter as any)[field] = (...args: any[]) => {
const router = getRouter() as any
return router[field](...args)
}
})
routerEvents.forEach((event) => {
singletonRouter.ready(() => {
Router.events.on(event, (...args) => {
const eventField = `on${event.charAt(0).toUpperCase()}${event.substring(
1
)}`
const _singletonRouter = singletonRouter as any
if (_singletonRouter[eventField]) {
try {
_singletonRouter[eventField](...args)
} catch (err) {
console.error(`Error when running the Router event: ${eventField}`)
console.error(`${err.message}\n${err.stack}`)
}
}
})
})
})
function getRouter(): Router {
if (!singletonRouter.router) {
const message =
'No router instance found.\n' +
'You should only use "next/router" inside the client side of your app.\n'
throw new Error(message)
}
return singletonRouter.router
}
// Export the singletonRouter and this is the public API.
export default singletonRouter as SingletonRouter
// Reexport the withRoute HOC
export { default as withRouter } from './with-router'
export function useRouter(): NextRouter {
return React.useContext(RouterContext)
}
// INTERNAL APIS
// -------------
// (do not use following exports inside the app)
// Create a router and assign it as the singleton instance.
// This is used in client side when we are initilizing the app.
// This should **not** use inside the server.
export const createRouter = (...args: RouterArgs): Router => {
singletonRouter.router = new Router(...args)
singletonRouter.readyCallbacks.forEach((cb) => cb())
singletonRouter.readyCallbacks = []
return singletonRouter.router
}
// This function is used to create the `withRouter` router instance
export function makePublicRouterInstance(router: Router): NextRouter {
const _router = router as any
const instance = {} as any
for (const property of urlPropertyFields) {
if (typeof _router[property] === 'object') {
instance[property] = Object.assign(
Array.isArray(_router[property]) ? [] : {},
_router[property]
) // makes sure query is not stateful
continue
}
instance[property] = _router[property]
}
// Events is a static property on the router, the router doesn't have to be initialized to use it
instance.events = Router.events
coreMethodFields.forEach((field) => {
instance[field] = (...args: any[]) => {
return _router[field](...args)
}
})
return instance
}