Last active
May 27, 2019 09:12
-
-
Save buhichan/0c7060c71f60dae0d844c08e4f4bdc66 to your computer and use it in GitHub Desktop.
Type-safe (typescript) react web router
This file contains hidden or 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
import { createBrowserHistory } from "history"; | |
import { BehaviorSubject } from 'rxjs'; | |
import * as React from "react" | |
type RouteConfig<P=any> = { | |
key:string, | |
label?:string, | |
icon?:string, | |
component?:()=>Promise<{default:React.ComponentType<P>}>, | |
} | |
type BaseRoute<P=any> = { | |
key:string, | |
level:number, | |
icon?:string, | |
label?:string, | |
parent: IRoute | null, | |
children: IRoute[] | null, | |
component?:()=>Promise<{default:React.ComponentType<P>}> | |
} | |
export type SimpleRoute = BaseRoute & { | |
params:null | |
} | |
export type ParamedRoute<P> = BaseRoute<P> & { | |
params:(keyof P)[], | |
} | |
export type IRoute = SimpleRoute | ParamedRoute<any> | |
export function makeRoute(routeConfig:RouteConfig,parent?:IRoute):SimpleRoute{ | |
const res:SimpleRoute = { | |
parent:parent || null, | |
children:null, | |
level: parent ? parent.level + 1 : 0, | |
params:null, | |
...routeConfig, | |
} | |
if(parent){ | |
if(!parent.children) | |
parent.children = [] | |
parent.children.push(res) | |
} | |
return res | |
} | |
export function makeRouteWithParams<P>(routeConfig:RouteConfig<P>,params:(keyof P)[],parent?:IRoute):ParamedRoute<P>{ | |
let route = makeRoute(routeConfig,parent) as any | |
route.params = params | |
return route | |
} | |
function traverseRoute(routes:IRoute[],visiter:(route:IRoute)=>void){ | |
for(let route of routes){ | |
visiter(route) | |
if(route.children){ | |
traverseRoute(route.children,visiter) | |
} | |
} | |
} | |
export function makeRouter(rootRoutes:IRoute[]){ | |
const history = createBrowserHistory() | |
const componentMap = new Map<string,any>() | |
const pathToRouteMap = new Map<string,IRoute>() | |
traverseRoute(rootRoutes,(route)=>{ | |
pathToRouteMap.set(route.key,route) | |
route.component && componentMap.set(route.key,React.lazy(route.component)) | |
}) | |
const route$ = new BehaviorSubject(pathToRouteMap.get(history.location.pathname) || null) | |
let initialQuery:Record<string,string> = {} | |
new URLSearchParams(history.location.search).forEach((v,k)=>{ | |
initialQuery[k] = v | |
},{}) | |
const params$ = new BehaviorSubject(initialQuery) | |
function pushHistory<P>(route:ParamedRoute<P>,query:{[p in keyof P]: string}):void | |
function pushHistory(route:SimpleRoute):void | |
function pushHistory(route:IRoute,query?:any){ | |
if(route$.value !== route){ | |
route$.next(route) | |
} | |
let search = "" | |
if(query){ | |
params$.next(query) | |
search = "?" + new URLSearchParams(query as any) | |
} | |
route && history.push(route.key+search) | |
} | |
params$.subscribe((newParams)=>{ | |
const urlParams = new URLSearchParams(newParams) | |
history.push(history.location.pathname+"?"+urlParams) | |
}) | |
history.listen((location)=>{ | |
if(!route$.value || location.pathname !== route$.value.key){ | |
const nextRoute = pathToRouteMap.get(history.location.pathname) | |
nextRoute && route$.next(nextRoute) | |
} | |
}) | |
function WithParams({Comp}:{Comp:React.ComponentType<any>}){ | |
const [params,setParams] = React.useState(params$.value) | |
React.useEffect(()=>{ | |
const sub = params$.subscribe(setParams) | |
return ()=>sub.unsubscribe() | |
},[]) | |
return <React.Suspense fallback={null}> | |
<Comp {...params} /> | |
</React.Suspense> | |
} | |
return { | |
pushHistory, | |
route$, | |
history, | |
params$, | |
Router:()=>{ | |
const [CurComp,setCurComp] = React.useState(null as null | any) | |
const [curRoute,setCurRoute] = React.useState(route$.value) | |
React.useEffect(()=>{ | |
const sub = route$.subscribe(v=>{ | |
if(v && v.component){ | |
if(!componentMap.has(v.key)){ | |
componentMap.set(v.key,React.lazy(v.component)) | |
} | |
setCurRoute(v) | |
setCurComp(componentMap.get(v.key)) | |
}else{ | |
setCurRoute(null) | |
setCurComp(null) | |
} | |
}) | |
return ()=>sub.unsubscribe() | |
},[]) | |
if(CurComp && curRoute){ | |
return curRoute.params ? <WithParams Comp={CurComp} /> : | |
<React.Suspense fallback={null}> | |
<CurComp />} | |
</React.Suspense> | |
}else{ | |
return null | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment