Skip to content

Commit

Permalink
feat(packages/tuono-router): replace zustand with React.Context (#…
Browse files Browse the repository at this point in the history
…256)

Co-authored-by: Marco Pasqualetti <[email protected]>
  • Loading branch information
Valerioageno and marcalexiei authored Dec 24, 2024
1 parent d1b1217 commit d12d840
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 224 deletions.
5 changes: 2 additions & 3 deletions packages/tuono-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@
"react": ">=16.3.0"
},
"dependencies": {
"react-intersection-observer": "^9.13.0",
"vite": "^5.2.11",
"zustand": "4.4.7"
"react-intersection-observer": "^9.13.0"
},
"devDependencies": {
"@tanstack/config": "0.7.13",
Expand All @@ -59,6 +57,7 @@
"@vitejs/plugin-react-swc": "3.7.2",
"react": "18.3.1",
"jsdom": "^25.0.0",
"vite": "5.4.11",
"vitest": "^2.0.0"
}
}
4 changes: 2 additions & 2 deletions packages/tuono-router/src/components/Matches.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type * as React from 'react'

import { useRouterStore } from '../hooks/useRouterStore'
import useRoute from '../hooks/useRoute'

import { RouteMatch } from './RouteMatch'
import NotFound from './NotFound'
import { useRouterContext } from './RouterContext'

interface MatchesProps<TServerSideProps = unknown> {
// user defined props
serverSideProps: TServerSideProps
}

export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
const location = useRouterStore((st) => st.location)
const { location } = useRouterContext()

const route = useRoute(location.pathname)

Expand Down
4 changes: 2 additions & 2 deletions packages/tuono-router/src/components/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type * as React from 'react'

import { useInternalRouter } from '../hooks/useInternalRouter'
import { useRouterContext } from '../components/RouterContext'

import { RouteMatch } from './RouteMatch'
import Link from './Link'

export default function NotFound(): React.JSX.Element {
const router = useInternalRouter()
const { router } = useRouterContext()

const custom404Route = router.routesById['/404']

Expand Down
109 changes: 99 additions & 10 deletions packages/tuono-router/src/components/RouterContext.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,111 @@
import React from 'react'
import { createContext, useState, useEffect, useContext, useMemo } from 'react'
import type { ReactNode } from 'react'

import type { Router } from '../router'
import type { ServerRouterInfo } from '../types'

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const routerContext = React.createContext<Router>(null!)
export interface ParsedLocation {
href: string
pathname: string
search: Record<string, string>
searchStr: string
hash: string
}

interface RouterContextValue {
router: Router
location: ParsedLocation
updateLocation: (loc: ParsedLocation) => void
}

const TUONO_CONTEXT_GLOBAL_NAME = '__TUONO_CONTEXT__'
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const RouterContext = createContext<RouterContextValue>(null!)

export function getRouterContext(): React.Context<Router> {
function getInitialLocation(
serverSideProps?: ServerRouterInfo,
): ParsedLocation {
if (typeof document === 'undefined') {
return routerContext
return {
pathname: serverSideProps?.pathname || '',
hash: '',
href: serverSideProps?.href || '',
searchStr: serverSideProps?.searchStr || '',
// TODO: Polyfill URLSearchParams
search: {},
}
}

if (window[TUONO_CONTEXT_GLOBAL_NAME]) {
return window[TUONO_CONTEXT_GLOBAL_NAME]
const { location } = window
return {
pathname: location.pathname,
hash: location.hash,
href: location.href,
searchStr: location.search,
search: Object.fromEntries(new URLSearchParams(location.search)),
}
}

interface RouterContextProviderProps {
router: Router
children: ReactNode
serverSideProps?: ServerRouterInfo
}

window[TUONO_CONTEXT_GLOBAL_NAME] = routerContext
export function RouterContextProvider({
router,
children,
serverSideProps,
}: RouterContextProviderProps): ReactNode {
// Allow the router to update options on the router instance
router.update({ ...router.options } as Parameters<typeof router.update>[0])

const [location, setLocation] = useState<ParsedLocation>(() =>
getInitialLocation(serverSideProps),
)

/**
* Listen browser navigation events
*/
useEffect(() => {
const updateLocationOnPopStateChange = ({
target,
}: PopStateEvent): void => {
const { location: targetLocation } = target as typeof window
const { pathname, hash, href, search } = targetLocation

setLocation({
pathname,
hash,
href,
searchStr: search,
search: Object.fromEntries(new URLSearchParams(search)),
})
}

window.addEventListener('popstate', updateLocationOnPopStateChange)

return (): void => {
window.removeEventListener('popstate', updateLocationOnPopStateChange)
}
}, [])

const contextValue: RouterContextValue = useMemo(
() => ({
router,
location,
updateLocation: setLocation,
}),
[location, router],
)

return (
<RouterContext.Provider value={contextValue}>
{children}
</RouterContext.Provider>
)
}

return routerContext
/** @warning DO NOT EXPORT THIS TO USER LAND */
export function useRouterContext(): RouterContextValue {
return useContext(RouterContext)
}
47 changes: 14 additions & 33 deletions packages/tuono-router/src/components/RouterProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,32 @@
import React from 'react'
import type { ReactNode, JSX } from 'react'
import type { JSX } from 'react'
import { Suspense } from 'react'

import { useListenBrowserUrlUpdates } from '../hooks/useListenBrowserUrlUpdates'
import { useInitRouterStore } from '../hooks/useRouterStore'
import type { ServerProps } from '../types'
import type { Router } from '../router'

import { getRouterContext } from './RouterContext'
import { RouterContextProvider } from './RouterContext'
import { Matches } from './Matches'

interface RouterContextProviderProps {
router: Router
children: ReactNode
}

function RouterContextProvider({
router,
children,
}: RouterContextProviderProps): JSX.Element {
// Allow the router to update options on the router instance
router.update({ ...router.options } as Parameters<typeof router.update>[0])

const routerContext = getRouterContext()

return (
<React.Suspense>
<routerContext.Provider value={router}>{children}</routerContext.Provider>
</React.Suspense>
)
}

interface RouterProviderProps {
router: Router
serverProps?: ServerProps
}

/**
* This component is used in the tuono app entry point
*/
export function RouterProvider({
router,
serverProps,
}: RouterProviderProps): JSX.Element {
useInitRouterStore(serverProps)

useListenBrowserUrlUpdates()

return (
<RouterContextProvider router={router}>
<Matches serverSideProps={serverProps?.props} />
</RouterContextProvider>
<Suspense>
<RouterContextProvider
router={router}
serverSideProps={serverProps?.router}
>
<Matches serverSideProps={serverProps?.props} />
</RouterContextProvider>
</Suspense>
)
}
4 changes: 0 additions & 4 deletions packages/tuono-router/src/globals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type React from 'react'

import type { Router } from './router'

declare global {
Expand All @@ -8,7 +6,5 @@ declare global {
__TUONO_SSR_PROPS__?: {
props?: unknown
}

__TUONO_CONTEXT__?: React.Context<Router>
}
}
8 changes: 0 additions & 8 deletions packages/tuono-router/src/hooks/useInternalRouter.tsx

This file was deleted.

34 changes: 0 additions & 34 deletions packages/tuono-router/src/hooks/useListenBrowserUrlUpdates.tsx

This file was deleted.

26 changes: 15 additions & 11 deletions packages/tuono-router/src/hooks/useRoute.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ import { cleanup } from '@testing-library/react'

import useRoute from './useRoute'

describe('Test useRoute fn', () => {
describe('useRoute', () => {
afterEach(cleanup)

test('match routes by ids', () => {
vi.mock('./useInternalRouter.tsx', () => ({
useInternalRouter: (): { routesById: Record<string, { id: string }> } => {
vi.mock('../components/RouterContext.tsx', () => ({
useRouterContext: (): {
router: { routesById: Record<string, { id: string }> }
} => {
return {
routesById: {
'/': { id: '/' },
'/about': { id: '/about' },
'/posts': { id: '/posts' }, // posts/index
'/posts/[post]': { id: '/posts/[post]' },
'/posts/defined-post': { id: '/posts/defined-post' },
'/posts/[post]/[comment]': { id: '/posts/[post]/[comment]' },
'/blog/[...catch_all]': { id: '/blog/[...catch_all]' },
router: {
routesById: {
'/': { id: '/' },
'/about': { id: '/about' },
'/posts': { id: '/posts' }, // posts/index
'/posts/[post]': { id: '/posts/[post]' },
'/posts/defined-post': { id: '/posts/defined-post' },
'/posts/[post]/[comment]': { id: '/posts/[post]/[comment]' },
'/blog/[...catch_all]': { id: '/blog/[...catch_all]' },
},
},
}
},
Expand Down
6 changes: 4 additions & 2 deletions packages/tuono-router/src/hooks/useRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Route } from '../route'

import { useInternalRouter } from './useInternalRouter'
import { useRouterContext } from '../components/RouterContext'

const DYNAMIC_PATH_REGEX = /\[(.*?)\]/

Expand All @@ -25,7 +25,9 @@ export function sanitizePathname(pathname: string): string {
* Optimizations should occour on both
*/
export default function useRoute(pathname?: string): Route | undefined {
const { routesById } = useInternalRouter()
const {
router: { routesById },
} = useRouterContext()

if (!pathname) return

Expand Down
Loading

0 comments on commit d12d840

Please sign in to comment.