♻️(frontend) trying out cleaner way to handle routes

it was a bit cumbersome to navigate by paths accross the app as if one
path changes a tiny bit we have to make sure we updated everything
everywhere and it's kind of error-prone and cumbersome to build paths by
hand.

now we have a routes file that describes everything: what is the path to
each route, and how to navigate to them.

We use a new navigateTo helper that helps us. It could be better typed
but i'm a newbie.
This commit is contained in:
Emmanuel Pelletier
2024-07-26 00:36:10 +02:00
parent afd2f9a299
commit 8f81318ecf
17 changed files with 88 additions and 59 deletions

View File

@@ -5,12 +5,11 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useLang } from 'hoofd'
import { Route, Switch } from 'wouter'
import { HomeRoute } from '@/features/home'
import { RoomRoute, roomRouteRegex } from '@/features/rooms'
import { NotFound } from './routes/NotFound'
import './i18n/init'
import { Switch, Route } from 'wouter'
import { NotFoundScreen } from './layout/NotFoundScreen'
import { RenderIfUserFetched } from './features/auth'
import { routes } from './routes'
import './i18n/init'
const queryClient = new QueryClient()
@@ -22,9 +21,10 @@ function App() {
<Suspense fallback={null}>
<RenderIfUserFetched>
<Switch>
<Route path="/" component={HomeRoute} />
<Route path={roomRouteRegex} component={RoomRoute} />
<Route component={NotFound} />
{Object.entries(routes).map(([, route], i) => (
<Route key={i} path={route.path} component={route.Component} />
))}
<Route component={NotFoundScreen} />
</Switch>
</RenderIfUserFetched>
<ReactQueryDevtools initialIsOpen={false} />

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'
import { Field, Ul, H, P, Form, Dialog } from '@/primitives'
import { isRoomValid, navigateToRoom } from '@/features/rooms'
import { navigateTo } from '@/navigation/navigateTo'
import { isRoomValid } from '@/features/rooms'
export const JoinMeetingDialog = () => {
const { t } = useTranslation('home')
@@ -8,7 +9,7 @@ export const JoinMeetingDialog = () => {
<Dialog title={t('joinMeeting')}>
<Form
onSubmit={(data) => {
navigateToRoom((data.roomId as string).trim())
navigateTo('room', data.roomId as string)
}}
submitLabel={t('joinInputSubmit')}
>

View File

@@ -1,2 +1 @@
export { navigateToHome } from './navigation/navigateToHome'
export { Home as HomeRoute } from './routes/Home'

View File

@@ -1,5 +0,0 @@
import { navigate } from 'wouter/use-browser-location'
export const navigateToHome = () => {
navigate(`/`)
}

View File

@@ -2,8 +2,9 @@ import { useTranslation } from 'react-i18next'
import { DialogTrigger } from 'react-aria-components'
import { Button, Div, Text, VerticallyOffCenter } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { navigateTo } from '@/navigation/navigateTo'
import { generateRoomId } from '@/features/rooms'
import { authUrl, useUser } from '@/features/auth'
import { navigateToNewRoom } from '@/features/rooms'
import { Screen } from '@/layout/Screen'
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
@@ -28,7 +29,11 @@ export const Home = () => {
<HStack gap="gutter">
<Button
variant="primary"
onPress={isLoggedIn ? () => navigateToNewRoom() : undefined}
onPress={
isLoggedIn
? () => navigateTo('room', generateRoomId())
: undefined
}
href={isLoggedIn ? undefined : authUrl()}
>
{isLoggedIn ? t('createMeeting') : t('login', { ns: 'global' })}

View File

@@ -7,8 +7,8 @@ import {
} from '@livekit/components-react'
import { Room, RoomOptions } from "livekit-client";
import { keys } from '@/api/queryKeys'
import { navigateTo } from '@/navigation/navigateTo'
import { QueryAware } from '@/layout/QueryAware'
import { navigateToHome } from '@/features/home'
import { fetchRoom } from '../api/fetchRoom'
export const Conference = ({
@@ -50,7 +50,7 @@ export const Conference = ({
audio={userConfig.audioEnabled}
video={userConfig.videoEnabled}
onDisconnected={() => {
navigateToHome()
navigateTo('home')
}}
>
<VideoConference />

View File

@@ -1,4 +1,3 @@
export { navigateToNewRoom } from './navigation/navigateToNewRoom'
export { navigateToRoom } from './navigation/navigateToRoom'
export { Room as RoomRoute } from './routes/Room'
export { roomRouteRegex, isRoomValid } from './utils/isRoomValid'
export { roomIdPattern, isRoomValid } from './utils/isRoomValid'
export { generateRoomId } from './utils/generateRoomId'

View File

@@ -1,6 +0,0 @@
import { navigate } from 'wouter/use-browser-location'
import { generateRoomId } from '../utils/generateRoomId'
export const navigateToNewRoom = () => {
navigate(`/${generateRoomId()}`)
}

View File

@@ -1,5 +0,0 @@
import { navigate } from 'wouter/use-browser-location'
export const navigateToRoom = (roomId: string) => {
navigate(`/${roomId}`)
}

View File

@@ -1,5 +1,4 @@
const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
export const roomRouteRegex = new RegExp(`^[/](?<roomId>${roomIdPattern})$`)
export const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
export const isRoomValid = (roomId: string) =>
new RegExp(`^${roomIdPattern}$`).test(roomId)

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import { A, Button, Popover, PopoverList, Text } from '@/primitives'
import { SettingsButton } from '@/features/settings'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { useMatchesRoute } from '@/utils/useMatchesRoute'
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
import { Feedback } from '@/components/Feedback'
export const Header = () => {

View File

@@ -0,0 +1,9 @@
import { type RouteName, routes } from '@/routes'
export const getRouteByName = (routeName: RouteName) => {
const route = routes[routeName]
if (!route) {
throw new Error(`Route "${routeName}" does not exist`)
}
return route
}

View File

@@ -0,0 +1,21 @@
import { RouteName } from '@/routes'
import { navigate } from 'wouter/use-browser-location'
import { getRouteByName } from './getRouteByName'
export const navigateTo = <S = unknown>(
routeName: RouteName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any,
options?: { replace?: boolean; state?: S }
) => {
const route = getRouteByName(routeName)
const to = route.to
? route.to(params)
: typeof route.path === 'string'
? route.path
: null
if (!to) {
throw new Error(`Can't find path to navigate to for ${routeName}`)
}
return navigate(to, options)
}

View File

@@ -0,0 +1,7 @@
import { useRoute } from 'wouter'
import { type RouteName, routes } from '../routes'
export const useMatchesRoute = (route: RouteName) => {
const [match] = useRoute(routes[route].path)
return match
}

View File

@@ -0,0 +1,27 @@
import { RoomRoute, roomIdPattern } from '@/features/rooms'
import { HomeRoute } from '@/features/home'
export const routes: Record<
'home' | 'room',
{
name: RouteName
path: RegExp | string
Component: () => JSX.Element
// eslint-disable-next-line @typescript-eslint/no-explicit-any
to?: (...args: any[]) => string | URL
}
> = {
home: {
name: 'home',
path: '/',
Component: HomeRoute,
},
room: {
name: 'room',
path: new RegExp(`^[/](?<roomId>${roomIdPattern})$`),
to: (roomId: string) => `/${roomId.trim()}`,
Component: RoomRoute,
},
}
export type RouteName = keyof typeof routes

View File

@@ -1,5 +0,0 @@
import { NotFoundScreen } from '@/layout/NotFoundScreen'
export const NotFound = () => {
return <NotFoundScreen />
}

View File

@@ -1,17 +0,0 @@
import { useRoute } from 'wouter'
import { roomRouteRegex } from '@/features/rooms'
type RouteName = 'home' | 'room'
const routeMap = {
home: '/',
room: roomRouteRegex,
}
export const useMatchesRoute = (route: RouteName) => {
const [match] = useRoute(routeMap[route])
if (!(route in routeMap)) {
throw new Error(`Route ${route} not found`)
}
return match
}