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 @@
[package]
name = "tuono"
version = "0.4.5"
version = "0.4.6"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib"
version = "0.4.5"
version = "0.4.6"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
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"] }
+9 -7
View File
@@ -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::<Uri>()
.unwrap(),
);
let uri = uri
.unwrap_or("http://localhost:3000/")
.parse::<Uri>()
.unwrap();
let location = Location::from(uri);
Payload {
router: location,
+6 -6
View File
@@ -8,9 +8,9 @@ use axum::http::{HeaderMap, Uri};
pub struct Location {
href: String,
pathname: String,
search: HashMap<String, String>,
#[serde(rename(serialize = "searchStr"))]
search_str: String,
hash: String,
search: HashMap<String, String>,
}
impl Location {
@@ -21,13 +21,13 @@ impl Location {
impl From<Uri> 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()),
}
}
}
+1 -1
View File
@@ -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"]
@@ -13,7 +13,11 @@ export default function PokemonLink({
id: number
}): JSX.Element {
return (
<Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
<Link
className={styles.link}
href={`/pokemons/${pokemon.name}`}
id={pokemon.name}
>
{pokemon.name}
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
+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'