Create use router hook (#13)

* refactor: useRouter to useInternalRouter

* feat: create useRouter hook

* feat: optimized payload sent to client

* feat: add support for query and pathname value in useRouter

* feat: update version to v0.4.6
This commit is contained in:
Valerio Ageno
2024-07-11 21:43:38 +02:00
committed by GitHub
parent 3e6aa540fa
commit 67d4e1bcac
23 changed files with 116 additions and 67 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.4.5",
"version": "0.4.6",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
@@ -35,8 +35,6 @@ describe('generator works', async () => {
getRouteTreeFileText(folderName),
])
console.log(generatedRouteTree)
expect(generatedRouteTree).equal(expectedRouteTree)
},
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-lazy-fn-vite-plugin",
"version": "0.4.5",
"version": "0.4.6",
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "tuono",
"version": "0.4.5",
"version": "0.4.6",
"description": "The react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
@@ -100,8 +100,8 @@
"@types/babel__traverse": "^7.20.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"prettier": "^3.2.4",
"jsdom": "^24.0.0",
"prettier": "^3.2.4",
"vitest": "^1.5.2"
},
"sideEffects": false,
+2
View File
@@ -5,6 +5,7 @@ import {
Link,
RouterProvider,
dynamic,
useRouter,
} from './router'
export type { TuonoProps } from './types'
@@ -16,4 +17,5 @@ export {
Link,
RouterProvider,
dynamic,
useRouter,
}
+5 -12
View File
@@ -1,25 +1,18 @@
import * as React from 'react'
import { useRouterStore } from '../hooks/useRouterStore'
import { useRouter } from '../hooks/useRouter'
import type { AnchorHTMLAttributes, MouseEvent } from 'react'
export default function Link(
props: AnchorHTMLAttributes<HTMLAnchorElement>,
): JSX.Element {
const router = useRouter()
const handleTransition = (e: MouseEvent<HTMLAnchorElement>): void => {
e.preventDefault()
props.onClick?.(e)
useRouterStore.setState({
// TODO: Refine store update
location: {
href: props.href || '',
pathname: props.href || '',
search: undefined,
searchStr: '',
hash: '',
},
})
history.pushState(props.href, '', props.href)
router.push(props.href || '')
}
return (
<a {...props} onClick={handleTransition}>
{props.children}
@@ -8,8 +8,8 @@ describe('Test getRouteByPathname fn', () => {
})
test('match routes by ids', () => {
vi.mock('../hooks/useRouter.tsx', () => ({
useRouter: (): { routesById: Record<string, any> } => {
vi.mock('../hooks/useInternalRouter.tsx', () => ({
useInternalRouter: (): { routesById: Record<string, any> } => {
return {
routesById: {
'/': { id: '/' },
@@ -1,5 +1,5 @@
import * as React from 'react'
import { useRouter } from '../hooks/useRouter'
import { useInternalRouter } from '../hooks/useInternalRouter'
import { useRouterStore } from '../hooks/useRouterStore'
import type { Route } from '../route'
import { RouteMatch } from './RouteMatch'
@@ -21,7 +21,7 @@ const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
* Optimizations should occour on both
*/
export function getRouteByPathname(pathname: string): Route | undefined {
const { routesById } = useRouter()
const { routesById } = useInternalRouter()
if (routesById[pathname]) return routesById[pathname]
@@ -1,10 +1,10 @@
import * as React from 'react'
import { useRouter } from '../hooks/useRouter'
import { RouteMatch } from './RouteMatch'
import Link from './Link'
import { useInternalRouter } from '../hooks/useInternalRouter'
export default function NotFound(): JSX.Element {
const router = useRouter()
const router = useInternalRouter()
const custom404Route = router.routesById['/404']
@@ -0,0 +1,7 @@
import * as React from 'react'
import { getRouterContext } from '../components/RouterContext'
import type { Router } from '../router'
export function useInternalRouter(): Router {
return React.useContext(getRouterContext())
}
@@ -1,7 +1,7 @@
import { useRouterStore } from './useRouterStore'
import { useEffect } from 'react'
/*
/**
* This hook is meant to handle just browser related location updates
* like the back and forward buttons.
*/
@@ -15,7 +15,7 @@ export const useListenBrowserUrlUpdates = (): void => {
hash,
href,
searchStr: search,
search: new URLSearchParams(search),
search: Object.fromEntries(new URLSearchParams(search)),
})
}
useEffect(() => {
+42 -5
View File
@@ -1,7 +1,44 @@
import * as React from 'react'
import { getRouterContext } from '../components/RouterContext'
import type { Router } from '../router'
import { useRouterStore } from './useRouterStore'
export function useRouter(): Router {
return React.useContext(getRouterContext())
interface UseRouterHook {
/**
* Redirects to the path passed as argument updating the browser history.
*/
push: (path: string) => void
/**
* This object contains all the query params of the current route
*/
query: Record<string, any>
/**
* Returns the current pathname
*/
pathname: string
}
export const useRouter = (): UseRouterHook => {
const [location, updateLocation] = useRouterStore((st) => [
st.location,
st.updateLocation,
])
const push = (path: string): void => {
const url = new URL(path, window.location.origin)
updateLocation({
href: url.href,
pathname: url.pathname,
search: Object.fromEntries(url.searchParams),
searchStr: url.search,
hash: url.hash,
})
history.pushState(path, '', path)
}
return {
push,
query: location.search,
pathname: location.pathname,
}
}
@@ -6,7 +6,7 @@ import type { ServerProps } from '../types'
export interface ParsedLocation {
href: string
pathname: string
search?: URLSearchParams
search: Record<string, string>
searchStr: string
hash: string
}
@@ -26,15 +26,20 @@ interface RouterState {
export const initRouterStore = (props?: ServerProps): void => {
const updateLocation = useRouterStore((st) => st.updateLocation)
// Init the store in the server in order to correctly
// SSR the components that depend on the router.
if (typeof window === 'undefined') {
updateLocation({
pathname: props?.router.pathname || '',
hash: '',
href: '',
searchStr: '',
href: props?.router.href || '',
searchStr: props?.router.searchStr || '',
search: {},
})
}
// Update the store on the client side before the first
// rendering
useLayoutEffect(() => {
const { pathname, hash, href, search } = window.location
updateLocation({
@@ -42,7 +47,7 @@ export const initRouterStore = (props?: ServerProps): void => {
hash,
href,
searchStr: search,
search: new URLSearchParams(search),
search: Object.fromEntries(new URLSearchParams(search)),
})
}, [])
}
@@ -54,7 +59,7 @@ export const useRouterStore = create<RouterState>()((set) => ({
location: {
href: '',
pathname: '',
search: undefined,
search: {},
searchStr: '',
hash: '',
},
+1
View File
@@ -3,3 +3,4 @@ export { default as Link } from './components/Link'
export { createRouter } from './router'
export { createRoute, createRootRoute } from './route'
export { dynamic } from './dynamic'
export { useRouter } from './hooks/useRouter'
+5 -1
View File
@@ -4,6 +4,10 @@ export interface Segment {
}
export interface ServerProps {
router: Location
router: {
href: string
pathname: string
searchStr: string
}
props: any
}
@@ -1,16 +1,12 @@
import type { ParsedLocation } from '../hooks/useRouterStore'
// TODO: improve the whole react/rust URL parsing logic
export function fromUrlToParsedLocation(href: string): ParsedLocation {
/*
* This function works on both server and client.
* For this reason we can't rely on the browser's URL api
*/
const location = new URL(href, window.location.origin)
return {
href,
pathname: href,
search: undefined,
searchStr: '',
hash: '',
href: location.href,
pathname: location.pathname,
search: Object.fromEntries(location.searchParams),
searchStr: location.search,
hash: location.hash,
}
}
+1 -2
View File
@@ -1,8 +1,7 @@
import 'fast-text-encoding' // Mandatory for React18
import * as React from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import { RouterProvider } from '../router'
import { createRouter } from '../router'
import { RouterProvider, createRouter } from '../router'
type RouteTree = any
type Mode = 'Dev' | 'Prod'