라우터 컨텍스트
TanStack Router의 라우터 컨텍스트는 의존성 주입을 비롯한 여러 용도로 사용할 수 있는 매우 강력한 도구입니다. 이름 그대로 라우터 컨텍스트는 라우터를 거쳐 매칭되는 각 라우트로 전달됩니다. 계층 구조의 각 라우트에서 컨텍스트를 수정하거나 추가할 수 있습니다. 라우터 컨텍스트를 실제로 사용하는 몇 가지 방법은 다음과 같습니다.
- 의존성 주입
- 라우트와 모든 하위 라우트가 직접 가져오거나 생성하지 않고 접근해 사용할 수 있도록 의존성(예: 로더 함수, 데이터 가져오기 클라이언트, 뮤테이션 서비스)을 제공할 수 있습니다.
- 브레드크럼
- 각 라우트의 기본 컨텍스트 객체는 아래로 내려가면서 병합되고, 각 라우트의 고유 컨텍스트도 저장되므로 라우트 컨텍스트에 브레드크럼이나 메서드를 연결할 수 있습니다.
- 동적 메타 태그 관리
- 각 라우트의 컨텍스트에 메타 태그를 연결한 다음 메타 태그 관리자를 사용해 사이트를 탐색할 때 페이지의 메타 태그를 동적으로 업데이트할 수 있습니다.
이는 라우터 컨텍스트를 사용하는 방법의 일부 예시일 뿐입니다. 원하는 용도로 사용할 수 있습니다.
타입이 지정된 라우터 컨텍스트
다른 모든 것과 마찬가지로 루트 라우터 컨텍스트에도 엄격한 타입이 지정됩니다. 이 타입은 라우트 매치 트리를 따라 병합되므로 모든 라우트의 beforeLoad 옵션을 통해 확장할 수 있습니다. 루트 라우터 컨텍스트의 타입을 제한하려면 createRootRouteWithContext<YourContextTypeHere>()(routeOptions) 함수를 사용해 새 라우터 컨텍스트를 생성해야 하며, 루트 라우트를 생성하는 createRootRoute() 함수 대신 사용합니다. 예시는 다음과 같습니다.
React
import {
createRootRouteWithContext,
createRouter,
} from '@tanstack/react-router'
interface MyRouterContext {
user: User
}
// Use the routerContext to create your root route
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
const routeTree = rootRoute.addChildren([
// ...
])
// Use the routerContext to create your router
const router = createRouter({
routeTree,
})
Solid
import {
createRootRouteWithContext,
createRouter,
} from '@tanstack/solid-router'
interface MyRouterContext {
user: User
}
// Use the routerContext to create your root route
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
const routeTree = rootRoute.addChildren([
// ...
])
// Use the routerContext to create your router
const router = createRouter({
routeTree,
})
[!TIP]
MyRouterContext에는 아래의createRouter에 직접 전달할 내용만 포함하면 됩니다.beforeLoad에서 추가하는 다른 모든 컨텍스트는 추론됩니다.
초기 라우터 컨텍스트 전달
라우터 컨텍스트는 인스턴스화할 때 라우터에 전달됩니다. context 옵션을 통해 라우터에 초기 라우터 컨텍스트를 전달할 수 있습니다.
[!TIP] 컨텍스트에 필수 속성이 있으면 초기 라우터 컨텍스트에 해당 속성을 전달하지 않을 때 TypeScript 오류가 발생합니다. 모든 컨텍스트 속성이 선택 사항이면 TypeScript 오류가 발생하지 않으며 컨텍스트 전달도 선택 사항입니다. 라우터 컨텍스트를 전달하지 않으면 기본값은
{}입니다.
React
import { createRouter } from '@tanstack/react-router'
// Use the routerContext you created to create your router
const router = createRouter({
routeTree,
context: {
user: {
id: '123',
name: 'John Doe',
},
},
})
Solid
import { createRouter } from '@tanstack/solid-router'
// Use the routerContext you created to create your router
const router = createRouter({
routeTree,
context: {
user: {
id: '123',
name: 'John Doe',
},
},
})
라우터 컨텍스트 무효화
라우터에 전달하는 컨텍스트 상태를 무효화해야 한다면 invalidate 메서드를 호출해 라우터에 컨텍스트를 다시 계산하도록 알릴 수 있습니다. 컨텍스트 상태를 업데이트하고 라우터가 모든 라우트의 컨텍스트를 다시 계산하도록 해야 할 때 유용합니다.
React
function useAuth() {
const router = useRouter()
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setUser(user)
router.invalidate()
})
return unsubscribe
}, [])
return user
}
Solid
function useAuth() {
const router = useRouter()
const [user, setUser] = createSignal<User | null>(null)
createEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setUser(user)
router.invalidate()
})
return unsubscribe
}, [])
return user()
}
라우터 컨텍스트 사용
라우터 컨텍스트 타입을 정의하면 라우트 정의에서 사용할 수 있습니다.
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
component: Todos,
loader: ({ context }) => fetchTodosByUserId(context.user.id),
})
데이터 가져오기 및 뮤테이션 구현 자체를 주입할 수도 있습니다. 실제로 이 방법을 매우 권장합니다. 😜
할 일 목록을 가져오는 간단한 함수로 시도해 보겠습니다.
const fetchTodosByUserId = async ({ userId }) => {
const response = await fetch(`/api/todos?userId=${userId}`)
const data = await response.json()
return data
}
const router = createRouter({
routeTree: rootRoute,
context: {
userId: '123',
fetchTodosByUserId,
},
})
그런 다음 라우트에서 다음과 같이 사용합니다.
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
component: Todos,
loader: ({ context }) => context.fetchTodosByUserId(context.userId),
})
외부 데이터 가져오기 라이브러리는 어떨까요?
React
import {
createRootRouteWithContext,
createRouter,
} from '@tanstack/react-router'
interface MyRouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
const queryClient = new QueryClient()
const router = createRouter({
routeTree: rootRoute,
context: {
queryClient,
},
})
Solid
import {
createRootRouteWithContext,
createRouter,
} from '@tanstack/solid-router'
interface MyRouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
const queryClient = new QueryClient()
const router = createRouter({
routeTree: rootRoute,
context: {
queryClient,
},
})
그런 다음 라우트에서 다음과 같이 사용합니다.
// src/routes/todos.tsx
export const Route = createFileRoute('/todos')({
component: Todos,
loader: async ({ context }) => {
await context.queryClient.ensureQueryData({
queryKey: ['todos', { userId: user.id }],
queryFn: fetchTodos,
})
},
})
React
React Context/Hooks를 사용하는 방법
라우트의 beforeLoad 또는 loader 함수에서 React Context나 Hooks를 사용하려면 React의 Hooks 규칙을 기억해야 합니다. React 함수가 아닌 곳에서는 훅을 사용할 수 없으므로 beforeLoad 또는 loader 함수에서도 훅을 사용할 수 없습니다.
그렇다면 라우트의 beforeLoad 또는 loader 함수에서 React Context나 Hooks를 어떻게 사용할까요? 라우터 컨텍스트를 사용해 React Context나 Hooks를 라우트의 beforeLoad 또는 loader 함수로 전달할 수 있습니다.
useNetworkStrength 훅을 라우트의 loader 함수로 전달하는 예시의 설정을 살펴보겠습니다.
// First, make sure the context for the root route is typed
import { createRootRouteWithContext } from '@tanstack/react-router'
import { useNetworkStrength } from '@/hooks/useNetworkStrength'
interface MyRouterContext {
networkStrength: ReturnType<typeof useNetworkStrength>
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
이 예시에서는 <RouterProvider />를 사용해 라우터를 렌더링하기 전에 훅을 인스턴스화합니다. 이렇게 하면 훅이 React 영역에서 호출되므로 Hooks 규칙을 준수할 수 있습니다.
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({
routeTree,
context: {
networkStrength: undefined!, // We'll set this in React-land
},
})
그런 다음 useNetworkStrength 훅을 App 컴포넌트에서 호출하고 반환된 값을 <RouterProvider />를 통해 라우터 컨텍스트로 전달할 수 있습니다.
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
import { useNetworkStrength } from '@/hooks/useNetworkStrength'
function App() {
const networkStrength = useNetworkStrength()
// Inject the returned value from the hook into the router context
return <RouterProvider router={router} context={{ networkStrength }} />
}
// ...
이제 라우트의 loader 함수에서 라우터 컨텍스트를 통해 networkStrength 훅에 접근할 수 있습니다.
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
component: Posts,
loader: ({ context }) => {
if (context.networkStrength === 'STRONG') {
// Do something
}
},
})
라우터 컨텍스트 수정
라우터 컨텍스트는 라우트 트리를 따라 전달되며 각 라우트에서 병합됩니다. 따라서 각 라우트에서 컨텍스트를 수정할 수 있고 수정 사항은 모든 하위 라우트에서 사용할 수 있습니다. 예시는 다음과 같습니다.
React
import { createRootRouteWithContext } from '@tanstack/react-router'
interface MyRouterContext {
foo: boolean
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
context: {
foo: true,
},
})
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/todos')({
component: Todos,
beforeLoad: () => {
return {
bar: true,
}
},
loader: ({ context }) => {
context.foo // true
context.bar // true
},
})
Solid
import { createRootRouteWithContext } from '@tanstack/solid-router'
interface MyRouterContext {
foo: boolean
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: App,
})
import { createRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
context: {
foo: true,
},
})
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/todos')({
component: Todos,
beforeLoad: () => {
return {
bar: true,
}
},
loader: ({ context }) => {
context.foo // true
context.bar // true
},
})
누적된 라우트 컨텍스트 처리
컨텍스트, 특히 격리된 라우트 context 객체를 사용하면 매칭된 모든 라우트의 라우트 컨텍스트 객체를 쉽게 누적하고 처리할 수 있습니다. 다음은 매칭된 모든 라우트 컨텍스트를 사용해 브레드크럼 경로를 생성하는 예시입니다.
export const Route = createRootRoute({
component: () => {
const matches = useRouterState({ select: (s) => s.matches })
const breadcrumbs = matches
.filter((match) => match.context.getTitle)
.map(({ pathname, context }) => {
return {
title: context.getTitle(),
path: pathname,
}
})
// ...
},
})
같은 라우트 컨텍스트를 사용해 페이지의 <head>에 대한 title 태그도 생성할 수 있습니다.
export const Route = createRootRoute({
component: () => {
const matches = useRouterState({ select: (s) => s.matches })
const matchWithTitle = [...matches]
.reverse()
.find((d) => d.context.getTitle)
const title = matchWithTitle?.context.getTitle() || 'My App'
return (
<html>
<head>
<title>{title}</title>
</head>
<body>{/* ... */}</body>
</html>
)
},
})