---
title: "React Router"
description: "Automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in React Router applications."
canonical_url: "https://www.evlog.dev/integrate/frameworks/react-router"
---
# React Router

> Automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in React Router applications.

The `evlog/react-router` middleware auto-creates a request-scoped logger accessible via `context.get(loggerContext)` or `useLogger()` and emits a wide event when the response completes.

<callout color="info" icon="i-lucide-info">

React Router has three [modes](https://reactrouter.com/start/modes): **Framework**, **Data**, and **Declarative**. The `evlog/react-router` middleware requires the middleware API, which is available in **Framework** and **Data** modes only. Declarative mode does not support middleware: use `evlog/client` for console logging and `evlog/http` if you need a batched HTTP drain to your server.

</callout>

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my React Router app" icon="i-custom-reactrouter">

Set up evlog in my React Router app.

- Install evlog: pnpm add evlog
- Create evlog.config.ts with export default defineEvlog({ service: 'my-api' })
- Call initLogger(toLoggerConfig(config)) at startup, importing config from evlog.config.ts
- Enable middleware in react-router.config.ts: future: { v8_middleware: true }
- Import evlog middleware and loggerContext from 'evlog/react-router'
- Add evlog() to root route's middleware array
- Access logger via context.get(loggerContext) in loaders/actions
- Or use useLogger() from services without passing context
- Put drain, enrich, include and keep in evlog.config.ts; toMiddlewareOptions(config) passes them to evlog()

Docs: [https://www.evlog.dev/integrate/frameworks/react-router](https://www.evlog.dev/integrate/frameworks/react-router)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog react-router @react-router/node @react-router/serve
```

```bash [bun]
bun add evlog react-router @react-router/node @react-router/serve
```

```bash [yarn]
yarn add evlog react-router @react-router/node @react-router/serve
```

```bash [npm]
npm install evlog react-router @react-router/node @react-router/serve
```

</code-group>

### 2. Create evlog.config.ts

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-api',
})
```

Every option lives in this file: sampling, drain, `enrich`, route filters. `npx evlog init` writes it, and the wiring below, for you. See [Configuration](https://www.evlog.dev/reference/configuration).

### 3. Enable middleware

```typescript [react-router.config.ts]
import type { Config } from '@react-router/dev/config'

export default {
  future: {
    v8_middleware: true,
  },
} satisfies Config
```

### 4. Initialize and register the middleware

```typescript [app/root.tsx]
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog } from 'evlog/react-router'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

export const middleware: Route.MiddlewareFunction[] = [
  evlog(toMiddlewareOptions(config)),
]

export default function Root() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}
```

### 5. Use the logger in loaders

```typescript [app/routes/health.tsx]
import { loggerContext } from 'evlog/react-router'

export async function loader({ context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ route: 'health' })
  return { ok: true }
}
```

<callout color="info" icon="i-custom-vite">

**Using Vite?** The [`evlog/vite` plugin](https://www.evlog.dev/reference/vite-plugin) strips `log.debug()` from production builds and injects source locations. It does not read `evlog.config.ts`, so keep the `initLogger(toLoggerConfig(config))` call: it runs after the plugin's auto-init and replaces it.

</callout>

The `loggerContext` provides typed access to the evlog logger in any loader or action via `context.get(loggerContext)`.

## Wide Events

Build up context progressively through your loader. One request = one wide event:

```typescript [app/routes/users.$id.tsx]
import { loggerContext } from 'evlog/react-router'

export async function loader({ params, context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  const userId = params.id

  log.set({ user: { id: userId } })

  const user = await db.findUser(userId)
  log.set({ user: { name: user.name, plan: user.plan } })

  const orders = await db.findOrders(userId)
  log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })

  return { user, orders }
}
```

All fields are merged into a single wide event emitted when the request completes:

```bash [Terminal output]
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
  ├─ orders: count=2 totalRevenue=6298
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...
```

## useLogger()

Access the logger from any server-side function without passing context:

```typescript [app/services/user.server.ts]
import { useLogger } from 'evlog/react-router'

export async function findUser(userId: string) {
  const log = useLogger()
  log.set({ db: { query: 'findUser', userId } })
  return await db.users.find(userId)
}
```

Then call the service from your loader: `useLogger()` returns the same logger instance:

```typescript [app/routes/users.$id.tsx]
import { loggerContext } from 'evlog/react-router'
import { findUser } from '~/services/user.server'

export async function loader({ params, context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ user: { id: params.id } })

  const user = await findUser(params.id!)
  return { user }
}
```

## Background work (`log.fork`)

The logger from `loggerContext` supports `fork` for child wide events. See [After emit](https://www.evlog.dev/learn/wide-events#after-emit-sealing-and-background-work) in Wide events.

```typescript [app/routes/orders.tsx]
import { loggerContext } from 'evlog/react-router'
import { useLogger } from 'evlog/react-router'
import type { Route } from './+types/orders'

export async function action({ context }: Route.ActionArgs) {
  const log = context.get(loggerContext)
  log.fork!('background', async () => {
    const child = useLogger()
    child.set({ step: 'complete' })
  })
  return { ok: true }
}
```

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields:

```typescript [app/routes/checkout.tsx]
import { loggerContext } from 'evlog/react-router'
import { createError } from 'evlog'

export async function loader({ context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ cart: { items: 3, total: 9999 } })

  throw createError({
    message: 'Payment failed',
    status: 402,
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
    link: 'https://docs.example.com/payments/declined',
  })
}
```

The error is captured and logged with both the custom context and structured error fields:

```bash [Terminal output]
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
  ├─ error: name=EvlogError message=Payment failed status=402
  ├─ cart: items=3 total=9999
  └─ requestId: 880a50ac-...
```

## Configuration

Every option goes in `evlog.config.ts`. See the [Configuration reference](https://www.evlog.dev/reference/configuration) for the full list.

## Drain & Enrichers

Set `drain` and `enrich` in `evlog.config.ts` so they reach the app through `toMiddlewareOptions(config)`. You can also pass them inline in the middleware options; an inline key replaces the one from the file instead of merging with it:

```typescript [app/root.tsx]
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    drain: createAxiomDrain(),
    enrich: (ctx) => {
      userAgent(ctx)
      ctx.event.region = process.env.FLY_REGION
    },
  }),
]
```

### Pipeline (Batching & Retry)

For production, wrap your adapter with `createDrainPipeline` to batch events and retry on failure:

```typescript [app/root.tsx]
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain }),
]
```

<callout color="info" icon="i-lucide-info">

Call `drain.flush()` on server shutdown to ensure all buffered events are sent. See the [Pipeline docs](https://www.evlog.dev/extend/drain-pipeline) for all options.

</callout>

## Tail Sampling

Use `keep` to force-retain specific events regardless of head sampling:

```typescript [app/root.tsx]
export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    drain: createAxiomDrain(),
    keep: (ctx) => {
      if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
    },
  }),
]
```

## Route Filtering

Control which routes are logged with `include` and `exclude` patterns:

```typescript [app/root.tsx]
export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    include: ['/api/**'],
    exclude: ['/_internal/**', '/health'],
    routes: {
      '/api/auth/**': { service: 'auth-service' },
      '/api/payment/**': { service: 'payment-service' },
    },
  }),
]
```

## Run Locally

```bash [Terminal]
git clone https://github.com/evloghq/evlog.git
cd evlog
pnpm install
pnpm example react-router
```

Open [http://localhost:5173](http://localhost:5173) to explore the interactive test UI.

<card-group>
<card icon="i-simple-icons-github" title="Source Code" to="https://github.com/evloghq/evlog/tree/main/examples/react-router">

Browse the complete React Router example source on GitHub.

</card>
</card-group>

## Next Steps

- [Wide Events](https://www.evlog.dev/learn/wide-events): Design comprehensive events with context layering
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): Send logs to Axiom, Sentry, PostHog, and more
- [Sampling](https://www.evlog.dev/learn/sampling): Control log volume with head and tail sampling
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Throw errors with `why`, `fix`, and `link` fields

---

- [Source Code](https://github.com/evloghq/evlog/tree/main/examples/react-router)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
