Skip to content

Instantly share code, notes, and snippets.

@buhichan
Last active May 27, 2019 09:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buhichan/0c7060c71f60dae0d844c08e4f4bdc66 to your computer and use it in GitHub Desktop.
Save buhichan/0c7060c71f60dae0d844c08e4f4bdc66 to your computer and use it in GitHub Desktop.
Type-safe (typescript) react web router
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