Created
July 10, 2020 22:56
-
-
Save hamlim/908e9cad1ad0e6a886b758e31a84f93d to your computer and use it in GitHub Desktop.
Reroute Core and Reroute Dom (formerly Reroute Browser)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ~~ 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