From 67d4e1bcac762807996f2ec156ac88c78ef7d307 Mon Sep 17 00:00:00 2001 From: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:43:38 +0200 Subject: [PATCH] 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 --- crates/tuono/Cargo.toml | 2 +- crates/tuono_lib/Cargo.toml | 5 +- crates/tuono_lib/src/payload.rs | 16 ++++--- crates/tuono_lib/src/request.rs | 12 ++--- crates/tuono_lib_macros/Cargo.toml | 2 +- .../tutorial/src/components/PokemonLink.tsx | 6 ++- packages/fs-router-vite-plugin/package.json | 2 +- .../tests/generator.spec.ts | 2 - packages/lazy-fn-vite-plugin/package.json | 2 +- packages/tuono/package.json | 4 +- packages/tuono/src/index.ts | 2 + packages/tuono/src/router/components/Link.tsx | 17 ++----- .../src/router/components/Matches.spec.tsx | 4 +- .../tuono/src/router/components/Matches.tsx | 4 +- .../tuono/src/router/components/NotFound.tsx | 4 +- .../src/router/hooks/useInternalRouter.tsx | 7 +++ .../hooks/useListenBrowserUrlUpdates.tsx | 4 +- packages/tuono/src/router/hooks/useRouter.tsx | 47 +++++++++++++++++-- .../tuono/src/router/hooks/useRouterStore.tsx | 15 ++++-- packages/tuono/src/router/index.tsx | 1 + packages/tuono/src/router/types.ts | 6 ++- .../utils/from-url-to-parsed-location.ts | 16 +++---- packages/tuono/src/ssr/index.tsx | 3 +- 23 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 packages/tuono/src/router/hooks/useInternalRouter.tsx diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index 0bc812c4..79a7c4f7 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono" -version = "0.4.5" +version = "0.4.6" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index cf36bc88..567ca8f9 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib" -version = "0.4.5" +version = "0.4.6" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" @@ -25,13 +25,14 @@ tokio = { version = "1.37.0", features = ["full"] } serde = { version = "1.0.202", features = ["derive"] } erased-serde = "0.4.5" serde_json = "1.0" +serde_urlencoded = "0.7.1" reqwest = {version = "0.12.4", features = ["json", "stream"]} once_cell = "1.19.0" regex = "1.10.5" either = "1.13.0" tower-http = {version = "0.5.2", features = ["fs"]} -tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.4.5"} +tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.4.6"} # Match the same version used by axum tokio-tungstenite = "0.21.0" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } diff --git a/crates/tuono_lib/src/payload.rs b/crates/tuono_lib/src/payload.rs index 067e7db5..609a4891 100644 --- a/crates/tuono_lib/src/payload.rs +++ b/crates/tuono_lib/src/payload.rs @@ -24,7 +24,7 @@ pub struct Payload<'a> { } impl<'a> Payload<'a> { - pub fn new(req: &Request, props: &'a dyn Serialize) -> Payload<'a> { + pub fn new(req: &'a Request, props: &'a dyn Serialize) -> Payload<'a> { Payload { router: req.location(), props, @@ -135,7 +135,7 @@ mod tests { use crate::manifest::BundleInfo; - fn prepare_payload(uri: Option<&str>, mode: Mode) -> Payload<'static> { + fn prepare_payload<'a>(uri: Option<&'a str>, mode: Mode) -> Payload<'a> { let mut manifest_mock = HashMap::new(); manifest_mock.insert( "client-main".to_string(), @@ -185,11 +185,13 @@ mod tests { }, ); MANIFEST.get_or_init(|| manifest_mock); - let location = Location::from( - uri.unwrap_or("http://localhost:3000/") - .parse::() - .unwrap(), - ); + + let uri = uri + .unwrap_or("http://localhost:3000/") + .parse::() + .unwrap(); + + let location = Location::from(uri); Payload { router: location, diff --git a/crates/tuono_lib/src/request.rs b/crates/tuono_lib/src/request.rs index 2bcfb741..1a36c46f 100644 --- a/crates/tuono_lib/src/request.rs +++ b/crates/tuono_lib/src/request.rs @@ -8,9 +8,9 @@ use axum::http::{HeaderMap, Uri}; pub struct Location { href: String, pathname: String, - search: HashMap, + #[serde(rename(serialize = "searchStr"))] search_str: String, - hash: String, + search: HashMap, } impl Location { @@ -21,13 +21,13 @@ impl Location { impl From for Location { fn from(uri: Uri) -> Self { + let query = uri.query().unwrap_or(""); Location { + // TODO: build correct href href: uri.to_string(), pathname: uri.path().to_string(), - // TODO: handler search map - search: HashMap::new(), - search_str: uri.query().unwrap_or("").to_string(), - hash: "".to_string(), + search_str: query.to_string(), + search: serde_urlencoded::from_str(query).unwrap_or(HashMap::new()), } } } diff --git a/crates/tuono_lib_macros/Cargo.toml b/crates/tuono_lib_macros/Cargo.toml index f0bd7240..f95c2632 100644 --- a/crates/tuono_lib_macros/Cargo.toml +++ b/crates/tuono_lib_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib_macros" -version = "0.4.5" +version = "0.4.6" edition = "2021" description = "The react/rust fullstack framework" keywords = [ "react", "typescript", "fullstack", "web", "ssr"] diff --git a/examples/tutorial/src/components/PokemonLink.tsx b/examples/tutorial/src/components/PokemonLink.tsx index 4e7c8b9f..1266e8df 100644 --- a/examples/tutorial/src/components/PokemonLink.tsx +++ b/examples/tutorial/src/components/PokemonLink.tsx @@ -13,7 +13,11 @@ export default function PokemonLink({ id: number }): JSX.Element { return ( - + {pokemon.name} { getRouteTreeFileText(folderName), ]) - console.log(generatedRouteTree) - expect(generatedRouteTree).equal(expectedRouteTree) }, ) diff --git a/packages/lazy-fn-vite-plugin/package.json b/packages/lazy-fn-vite-plugin/package.json index fde4d712..5db1c33c 100644 --- a/packages/lazy-fn-vite-plugin/package.json +++ b/packages/lazy-fn-vite-plugin/package.json @@ -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", diff --git a/packages/tuono/package.json b/packages/tuono/package.json index 75cd5dd6..4ff91bd5 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -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, diff --git a/packages/tuono/src/index.ts b/packages/tuono/src/index.ts index 1862ed1d..33c1c9a5 100644 --- a/packages/tuono/src/index.ts +++ b/packages/tuono/src/index.ts @@ -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, } diff --git a/packages/tuono/src/router/components/Link.tsx b/packages/tuono/src/router/components/Link.tsx index 8f2b7562..0332ccbf 100644 --- a/packages/tuono/src/router/components/Link.tsx +++ b/packages/tuono/src/router/components/Link.tsx @@ -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, ): JSX.Element { + const router = useRouter() + const handleTransition = (e: MouseEvent): 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 ( {props.children} diff --git a/packages/tuono/src/router/components/Matches.spec.tsx b/packages/tuono/src/router/components/Matches.spec.tsx index 74a3e3a0..3051524c 100644 --- a/packages/tuono/src/router/components/Matches.spec.tsx +++ b/packages/tuono/src/router/components/Matches.spec.tsx @@ -8,8 +8,8 @@ describe('Test getRouteByPathname fn', () => { }) test('match routes by ids', () => { - vi.mock('../hooks/useRouter.tsx', () => ({ - useRouter: (): { routesById: Record } => { + vi.mock('../hooks/useInternalRouter.tsx', () => ({ + useInternalRouter: (): { routesById: Record } => { return { routesById: { '/': { id: '/' }, diff --git a/packages/tuono/src/router/components/Matches.tsx b/packages/tuono/src/router/components/Matches.tsx index d070428f..278a8c8e 100644 --- a/packages/tuono/src/router/components/Matches.tsx +++ b/packages/tuono/src/router/components/Matches.tsx @@ -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] diff --git a/packages/tuono/src/router/components/NotFound.tsx b/packages/tuono/src/router/components/NotFound.tsx index 3618c8e8..48533668 100644 --- a/packages/tuono/src/router/components/NotFound.tsx +++ b/packages/tuono/src/router/components/NotFound.tsx @@ -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'] diff --git a/packages/tuono/src/router/hooks/useInternalRouter.tsx b/packages/tuono/src/router/hooks/useInternalRouter.tsx new file mode 100644 index 00000000..08f659b6 --- /dev/null +++ b/packages/tuono/src/router/hooks/useInternalRouter.tsx @@ -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()) +} diff --git a/packages/tuono/src/router/hooks/useListenBrowserUrlUpdates.tsx b/packages/tuono/src/router/hooks/useListenBrowserUrlUpdates.tsx index 7f5fafab..f87a8245 100644 --- a/packages/tuono/src/router/hooks/useListenBrowserUrlUpdates.tsx +++ b/packages/tuono/src/router/hooks/useListenBrowserUrlUpdates.tsx @@ -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(() => { diff --git a/packages/tuono/src/router/hooks/useRouter.tsx b/packages/tuono/src/router/hooks/useRouter.tsx index b200a6a9..24a6ba60 100644 --- a/packages/tuono/src/router/hooks/useRouter.tsx +++ b/packages/tuono/src/router/hooks/useRouter.tsx @@ -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 + + /** + * 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, + } } diff --git a/packages/tuono/src/router/hooks/useRouterStore.tsx b/packages/tuono/src/router/hooks/useRouterStore.tsx index faf4a5cc..180969e9 100644 --- a/packages/tuono/src/router/hooks/useRouterStore.tsx +++ b/packages/tuono/src/router/hooks/useRouterStore.tsx @@ -6,7 +6,7 @@ import type { ServerProps } from '../types' export interface ParsedLocation { href: string pathname: string - search?: URLSearchParams + search: Record 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()((set) => ({ location: { href: '', pathname: '', - search: undefined, + search: {}, searchStr: '', hash: '', }, diff --git a/packages/tuono/src/router/index.tsx b/packages/tuono/src/router/index.tsx index b3d2a535..e0a902fc 100644 --- a/packages/tuono/src/router/index.tsx +++ b/packages/tuono/src/router/index.tsx @@ -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' diff --git a/packages/tuono/src/router/types.ts b/packages/tuono/src/router/types.ts index 2daff8cc..c4cc8612 100644 --- a/packages/tuono/src/router/types.ts +++ b/packages/tuono/src/router/types.ts @@ -4,6 +4,10 @@ export interface Segment { } export interface ServerProps { - router: Location + router: { + href: string + pathname: string + searchStr: string + } props: any } diff --git a/packages/tuono/src/router/utils/from-url-to-parsed-location.ts b/packages/tuono/src/router/utils/from-url-to-parsed-location.ts index b269acc3..2b861dc2 100644 --- a/packages/tuono/src/router/utils/from-url-to-parsed-location.ts +++ b/packages/tuono/src/router/utils/from-url-to-parsed-location.ts @@ -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, } } diff --git a/packages/tuono/src/ssr/index.tsx b/packages/tuono/src/ssr/index.tsx index 770bd668..428e578d 100644 --- a/packages/tuono/src/ssr/index.tsx +++ b/packages/tuono/src/ssr/index.tsx @@ -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'