Skip to content

Instantly share code, notes, and snippets.

@hamlim
Created July 10, 2020 22:56
Show Gist options
  • Save hamlim/908e9cad1ad0e6a886b758e31a84f93d to your computer and use it in GitHub Desktop.
Save hamlim/908e9cad1ad0e6a886b758e31a84f93d to your computer and use it in GitHub Desktop.
Reroute Core and Reroute Dom (formerly Reroute Browser)
// ~~ reroute ~~
type Location = {
pathname: string
}
let {
createContext,
useContext,
useState,
useMemo,
useLayoutEffect,
useRef,
useCallback,
// @ts-ignore
unstable_useTransition: useTransition,
Children,
isValidElement,
cloneElement,
useEffect,
forwardRef,
} = React
type History = {
location: Location
push: (path: string, state: any) => void
}
type HistoryContext = {
history: null | History
location: Location
isPending: boolean
}
let historyContext = createContext<HistoryContext>({
history: null,
location: null,
isPending: false,
})
interface RouterProps {
children: React.ReactNode
createHistory: () => History
timeoutMs?: number
}
function Router({ children, createHistory, timeoutMs = 2000 }: RouterProps) {
if (typeof createHistory !== 'function') {
throw new Error(
'createHistory prop was either not provided, or is not a function.',
)
}
let { current: history } = useLazyRef(createHistory)
let [location, setLocation] = useState(history.location)
let [startTransition, isPending] = useTransition({ timeoutMs })
let { current: listener } = useClientSideRef(() => {
return history.listen((location: Location) => {
startTransition(() => {
setLocation(location)
})
})
})
useClientSideLayoutEffect(() => {
return () => {
if (typeof listener === 'function') {
listener()
}
}
}, [])
let contextValue = useMemo(
() => ({
history,
location,
isPending,
}),
[location, isPending],
)
return (
<historyContext.Provider value={contextValue}>
{children}
</historyContext.Provider>
)
}
interface SwitchProps {
children?: React.ReactNode
matcher?: (path: string, location: Location) => boolean
}
function Switch(
{ children, matcher = defaultPathMatcher }: SwitchProps = {
matcher: defaultPathMatcher,
},
) {
let history = useHistory()
if (history.location === null) {
throw new Error(`Rendered a <Switch> component out of the context of a <Router> component.
Ensure the <Switch> is rendered as a child of the <Router>.`)
}
let { location } = history
let element, match
// We use React.Children.forEach instead of React.Children.toArray().find()
// here because toArray adds keys to all child elements and we do not want
// to trigger an unmount/remount for two <Route>s that render the same
// component at different URLs.
Children.forEach(children, (child) => {
if (!match && isValidElement(child)) {
element = child
const path = child.props.path
if (typeof path === 'string') {
match = matcher(path, location)
}
}
})
return match ? element : null
}
function useHistory(): HistoryContext {
return useContext(historyContext)
}
interface GetLinkProps {
onClick?: (event: any) => void
disabled?: boolean
}
interface AnchorProps {
href: string
role: string
'aria-disabled'?: 'true'
onClick: (event: any) => void
onKeyDown: (event: any) => void
onKeyUp: (event: any) => void
tabIndex: number
}
function useLink(path: string, state?: any) {
let { history } = useHistory()
let linkClick = useCallback(
function linkClick(event) {
if (event.defaultPrevented) {
return
}
event.preventDefault()
if (history === null || history === undefined) {
throw new Error(`Link attempted to route to path: '${path}' but no history was found in context.
Check to ensure the link is rendered within a Router.`)
}
console.log(path)
history.push(path, state)
},
[history],
)
return function getProps(props: GetLinkProps = {}): AnchorProps {
let handler = applyToAll(props.onClick, linkClick)
return {
...props,
href: path,
role: props.disabled ? 'presentation' : 'anchor',
'aria-disabled': props.disabled ? 'true' : undefined,
onClick: handler,
onKeyDown: keyDown(handler),
onKeyUp: keyUp(handler),
tabIndex: props.disabled ? -1 : 0,
}
}
}
interface Route {
match: boolean
}
function useRoute(path: string, { matcher = defaultPathMatcher } = {}): Route {
let { history, location } = useHistory()
return {
...history,
...location,
match: matcher(path, location),
}
}
// Utils
interface BasicRef {
current: any
}
function useLazyRef(initializer: () => any): BasicRef {
let ref = useRef(null)
if (ref.current === null) {
ref.current = initializer()
}
return ref
}
function useClientSideLayoutEffect(cb: () => void, deps: Array<any>) {
let effect = typeof window !== 'undefined' ? useLayoutEffect : noop
effect(cb, deps)
}
function useClientSideRef(initializer: () => any): BasicRef {
let ref = useRef(null)
useClientSideLayoutEffect(() => {
ref.current = initializer()
}, [])
return ref
}
function applyToAll(...fns: Array<(...args: Array<any>) => void>) {
return function (...values: Array<any>) {
fns.forEach((fn) => fn && fn(...values))
}
}
function keyDown(handler: (event: any) => void) {
return function (event: any) {
if (event.key === ' ') {
handler(event)
}
}
}
function keyUp(handler: (event: any) => void) {
return function (event: any) {
if (event.key === 'Enter') {
handler(event)
}
}
}
function noop() {}
function defaultPathMatcher(path: string, location: Location) {
return path === location.pathname
}
// ~~ End Reroute v2 ~~
// ~~ Reroute Dom v2 ~~
import { createBrowserHistory, createMemoryHistory } from 'history'
interface LinkProps {
to: string
children: React.ReactNode
onClick?: (event: any) => void
disabled?: boolean
}
let Link = forwardRef(function Link(
{ to, children, ...rest }: LinkProps,
ref: React.Ref<any>,
) {
let getLinkRestProps = useLink(to)
return (
<StyledLink forwardedAs="a" ref={ref} {...getLinkRestProps(rest)}>
{children}
</StyledLink>
)
})
function Route({ path, children }) {
let routeProps = useRoute(path)
if (typeof children === 'function') {
return children(routeProps)
}
if (routeProps.match) {
return children
}
return null
}
function BrowserRouter({ children, createHistory = createBrowserHistory }) {
return <Router createHistory={createHistory}>{children}</Router>
}
function SSRSafeRouter({ children }) {
if (typeof window !== 'undefined' || typeof document !== 'undefined') {
return <BrowserRouter>{children}</BrowserRouter>
}
return (
<Router
createHistory={() =>
createMemoryHistory({
initialEntries: ['/'],
})
}
>
{children}
</Router>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment