mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
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:
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
Reference in New Issue
Block a user