Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

.with

  • Раздел: Methods

On this page

  1. .with as a query injector
  2. .with as a state manager
  3. .with as a props injector
  4. .with as a wrapper
  5. .with as an idea
  6. The argument that .with(fn) receives
  7. What you can return
  8. resolve forms
  9. When the .with body is stripped
  10. Security: gate access in .with, not .ctx

.with is the point builder's swiss-army knife: inject a query, drive the loading and error states by hand, pass computed props down the chain, or wrap the render. It's available on every mountable — page, layout, component, provider — and on root, base, and plugin, but not on a mutation or a standalone query.

You've already seen the simple shape: inject a query, map the route into its input, and read the loaded data in .page:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(ideaViewQuery, ({ params }) => ({ id: params.id }))
  .page(({ data: { idea } }) => <h1>{idea.title}</h1>)

The page renders only after the injected query loads — the component gets data, never a loading branch. You can stick more than one query into a single point; they run in parallel by default, you see the loading state until all of them have loaded, and on error you see the first one that comes up. To bring several queries' data into one shape before the render, reach for .mapper.

Strip category: server-ssr-and-client. Cut from the server bundle when ssr: false (or after a .clientOnly() earlier in the chain) — its body and the imports it uses are then removed from the server build; kept in the client build always, and in the server build only when SSR is on. (It runs at render: in the browser always, on the server only under SSR.) The full mechanics are in When the .with body is stripped below.

The rest of this page covers the other shapes: feeding one query's data into another's input, deriving a query's input from an external hook, and reacting to each query's status before it loads.

.with as a query injector

Injecting a single query into a page — the same shape as above, now also reading queries:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(ideaViewQuery, ({ params }) => ({ id: params.id }))
  .page(
    ({
      // here we have the loaded data from the query
      data: { idea },
      // here we have the loaded queries themselves
      queries: [ideaViewQueryResult],
    }) => <h1>{idea.title}</h1>,
  )

The point you inject doesn't have to be a standalone .query(). Any point that carries a query works — including a component (or a layout/page). It's read as its query: the data is injected and flows into data, while its own UI is not rendered. So a component built for one screen can be reused elsewhere purely for its data, and the input is required exactly when that point's input is — the same rule as a query:

export const ideaPage = root.lets
  .page('/ideas/:id')
  // IdeaStats is a component with its own loader and UI, but here we inject it
  // for its query alone: its data lands in `data`, its UI is never rendered
  .with(IdeaStats, ({ params }) => ({ id: params.id }))
  .page(({ data: { fans } }) => <h1>{fans} fans</h1>)

The same thing can be written by passing a function — each .with acts as a component wrapper around the component below it, so you can call hooks inside:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(({ params }) => {
    // it's specifically by returning the result of useQuery()
    // that we get data and queries in the following methods
    return ideaViewQuery.useQuery({ id: params.id })
  })
  // from here on nothing changes
  .page(({ data: { idea }, queries: [ideaViewQueryResult] }) => (
    <h1>{idea.title}</h1>
  ))

To inject several queries from one function, return an array of them:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(({ params }) => {
    // you can return an array of queries straight from a single .with()
    return [ideaViewQuery.useQuery({ id: params.id }), ideaBestQuery.useQuery()]
  })
  .page(({ queries: [ideaViewQueryResult, ideaBestQueryResult] }) => (
    <>
      <h1>{ideaViewQueryResult.data.idea.title}</h1>
      <h2>{ideaBestQueryResult.data.bestIdea.title}</h2>
    </>
  ))

The first way to use one query's data as another's input:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(({ params }) => {
    const ideaViewQueryResult = ideaViewQuery.useQuery({ id: params.id })
    // say ideaViewQueryResult.data.idea.similarIds holds the ids of similar ideas
    const ideaListQueryResult = ideaListQuery.useQuery(
      // the first argument is the input as usual,
      // but this isn't very convenient — the input probably expects ids strictly as
      // an array, not "array or undefined", so we'd have to tack on `as never`,
      // which is awful; that's why another way exists, covered below
      { ids: ideaViewQueryResult.data?.idea.similarIds } as never,
      // in the second argument we can pass the options of the classic useQuery,
      // and we simply say not to enable this query until ideaViewQueryResult loaded
      { enabled: !!ideaViewQueryResult.data },
    )
    return [ideaViewQueryResult, ideaListQueryResult]
  })
  // a disabled query still has a real status of pending,
  // so we won't get here until ideaViewQueryResult has loaded
  .page(({ queries: [ideaViewQueryResult, ideaListQueryResult] }) => (
    <div>
      <h1>{ideaViewQueryResult.data.idea.title}</h1>
      <h2>Similar ideas</h2>
      <ul>
        {ideaListQueryResult.data.ideas.map((idea) => (
          <li key={idea.id}>{idea.title}</li>
        ))}
      </ul>
    </div>
  ))

The enabled trick is clunky; the resolve sections below cover a way I like more.

.with as a state manager

Inside a .with function you can short-circuit the chain. Returning a React element renders it; returning 'loading' or an Error renders the loading or error component declared up the chain:

export const strangePage = root.lets
  .page('/strange')
  .with(({ LoadingComponent, ErrorComponent }) => {
    // LoadingComponent holds what we declared earlier in .loading()
    // ErrorComponent holds what we declared earlier in .error()
    const [isLoading, setIsLoading] = useState(true)
    const [error] = useState(() =>
      Math.random() > 0.5 ? new Error('Oh, how wrong I was') : undefined,
    )

    useEffect(() => {
      setTimeout(() => setIsLoading(false), 1000)
    }, [])

    if (isLoading) return <LoadingComponent />
    if (error) return <ErrorComponent error={error} />

    // return undefined — or just nothing — to render the following methods
  })
  // we won't reach the render until all .with() calls have resolved
  .page(() => (
    <h1>I loaded something, I don't know what, but I pulled it off</h1>
  ))

There's a shorter notation I like more — the reserved word 'loading' is the same as <LoadingComponent />, and any instanceof Error is the same as <ErrorComponent error={...} />:

export const strangePage = root.lets
  .page('/strange')
  .with(() => {
    const [isLoading, setIsLoading] = useState(true)
    const [error] = useState(() =>
      Math.random() > 0.5 ? new Error('Oh, how wrong I was') : undefined,
    )

    useEffect(() => {
      setTimeout(() => setIsLoading(false), 1000)
    }, [])

    // same as return <LoadingComponent />
    if (isLoading) return 'loading'
    // same as return <ErrorComponent error={error} />
    if (error) return error

    // return nothing to render the following methods
  })
  .page(() => (
    <h1>I loaded something, I don't know what, but I pulled it off</h1>
  ))

An error returned this way can carry an HTTP status, honored during the SSR render — return new ErrorPoint0('Failed', { status: 500 }). Returning any string other than 'loading' is a type error, and so is an array of anything other than query results — a frequent mistake is returning a query's data array instead of the query itself.

.with as a props injector

To pass a computed value from one .with hook to another — or to the page itself — return a plain object: it becomes props, merged into props for every later method. The merge is a shallow spread ({ ...prev, ...next }), so a later .with can overwrite an earlier key — even change its type:

export const strangePage = root.lets
  .page('/')
  .with(() => {
    // anything that isn't a react element, an Error, the word 'loading',
    // or a query result, is treated as props for the following methods
    return { x: 1, y: 2 }
  })
  .with(({ props: { x, y } }) => {
    return {
      a: x * 10,
      b: y * 100,
      // props can be overwritten even with a different type, because in essence
      // this is nextProps = { ...prevProps, ...newProps }
      x: 'I decided I will be a string',
    }
  })
  .page(({ props: { a, b, x, y } }) => (
    <h1>
      {a} {b} {x} {y}
    </h1>
  ))

.with as a wrapper

The function's argument also carries children — the rendered rest of the chain. Return them wrapped and you get a boundary around everything downstream:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(({ children }) => {
    return <div style={{ border: '1px solid red' }}>{children}</div>
  })
  .page(() => <div id="page">Hello!</div>)

This differs from returning a self-contained element: an element that doesn't include children blocks the chain — the rest never renders, and any queries it would have injected never run. Wrap with children to keep the chain; return a standalone element to stop it.

.with as an idea

Combining these shapes gives another way to feed one query's data into another's input — the pattern I settled on while building a real project on the framework:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(ideaViewQuery, ({ params }) => ({ id: params.id }))
  .with(({ queries: [ideaViewQueryResult] }) => {
    // unlike .page(), every .with() receives queries in an indeterminate state —
    // here it could be an error, loading, or the loaded result, and we handle it

    // covered in ".with as a state manager"
    if (ideaViewQueryResult.isError) return ideaViewQueryResult.error
    if (ideaViewQueryResult.isLoading) return 'loading'

    // covered in ".with as a props injector"
    return { similarIds: ideaViewQueryResult.data.idea.similarIds }
  })
  // we won't reach this .with() while the previous one is intercepting control
  .with(ideaListQuery, ({ props: { similarIds } }) => ({ ids: similarIds }))
  .page(({ queries: [ideaViewQueryResult, ideaListQueryResult] }) => (
    <div>
      <h1>{ideaViewQueryResult.data.idea.title}</h1>
      <h2>Similar ideas</h2>
      <ul>
        {ideaListQueryResult.data.ideas.map((idea) => (
          <li key={idea.id}>{idea.title}</li>
        ))}
      </ul>
    </div>
  ))

All this manual handling of query states is verbose, so there's a resolve helper. It takes a query result; while the query is loading or erroring it returns the loading or error state, and on success it maps the data to props passed further down. The same example, shorter:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(ideaViewQuery, ({ params }) => ({ id: params.id }))
  .with(({ queries: [ideaViewQueryResult], resolve }) => {
    // the same as above, but short
    return resolve(ideaViewQueryResult, ({ data }) => ({
      similarIds: data.idea.similarIds,
    }))
  })
  .with(ideaListQuery, ({ props: { similarIds } }) => ({ ids: similarIds }))
  .page(({ queries: [ideaViewQueryResult, ideaListQueryResult] }) => (
    <div>
      <h1>{ideaViewQueryResult.data.idea.title}</h1>
      {/* ... */}
    </div>
  ))

Resolving before continuing is needed often enough that an even shorter notation bakes resolve right into the query injection — as the fourth argument:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(
    ideaViewQuery,
    ({ params }) => ({ id: params.id }),
    undefined, // useQuery options could go here, not needed now
    ({ data }) => ({ similarIds: data.idea.similarIds }),
  )
  .with(ideaListQuery, ({ props: { similarIds } }) => ({ ids: similarIds }))
  .page(/* ... */)

resolve also helps when you want to call a query but don't want it to land in the queries array or in data. That's handy for the current user — you want it in props, while data stays free for the page's own data:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .with(({ resolve }) => {
    // since we don't return the useQuery result from this .with(),
    // it won't land in queries or data — only the props from resolve() survive
    return resolve(getMeQuery.useQuery(), ({ data }) => ({ me: data.me }))
  })
  .with(ideaViewQuery, ({ params }) => ({ id: params.id }))
  .page(({ data: { idea }, props: { me } }) => (
    <div>
      <h1>{idea.title}</h1>
      <p>Hello, {me.name}!</p>
    </div>
  ))

The argument that .with(fn) receives

The function form receives one object. Here's everything on it:

KeyWhat
status / loading / error / datathe accumulated state so far
propsprops contributed by earlier .with calls
queriesinjected queries — in an indeterminate state here (may be loading or errored)
params / search / inputpresent when the matching schema exists
locationpresent on pages and layouts
resolvethe resolve helper
childrenthe rendered remainder of the chain
LoadingComponent / ErrorComponentthe components you set with .loading / .error

There is no ctx here — ctx is server-only and lives in .ctx and loaders (.ctx is cut from the client bundle, body and imports removed — the opposite strip category from .with). If you need a ctx value at render, merge it into props in a plugin first.

What you can return

ReturnEffect
a query result, or an array of themappended to queries (data = the first)
'loading'render the loading component
an Errorrender the error component (status honored in SSR)
a RedirectTask, or an Error with .redirectredirect
a React element (with children)wrap the rest of the chain
a React element (standalone)render it and stop the chain
a plain objectshallow-merge into props
undefined / nothingproceed to the next method
a non-'loading' string, or a non-query arraytype error — those shapes are reserved

Throwing works too. Every mountable render is wrapped in an error boundary, so a throw inside a .with callback (or anywhere else in the render) is caught and renders the nearest .error declared above it in the chain — the rest of the page stays alive. A thrown error carries the same SSR effects as a returned one: its HTTP status reaches the response, and a thrown redirect becomes the real HTTP redirect. Returning is still nicer: it's typed, it short-circuits the chain as data (no boundary involved), and it puts .error() into the server-rendered HTML itself — a throw ships the loading fallback there and the client renders .error() after hydration.

resolve forms

resolve(query) // wait, contribute nothing to props
resolve(query, true) // wait, then spread the query's data into props
resolve(query, ({ data }) => props) // wait, then map success to props

The key difference from injecting a query with .with(query, …): a query you resolve does not land in queries or data — only the props you derive from it survive. The fourth-argument form (above) is the opposite: it injects the query and maps extra props alongside it.

When the .with body is stripped

.with is server-ssr-and-client render code, so the rule is symmetric to other render methods (.page, .layout, .component, .provider, .loading, .error, .wrapper, .mapper, .head):

  • From the server build, the .with body — and the imports it pulls in — are cut when ssr: false, or after a .clientOnly() earlier in the chain (which makes the rest of the point client-only). It stays in the server build only when SSR is on, so the point can render server-side.
  • From the client build, nothing is cut — .with always ships and runs at render in the browser. (Server-only methods around it, .ctx, server .loader, …, are the ones cut from the client bundle; .with itself is not.)

That's also why .with runs "at render, on the client and — under SSR — on the server too": under SSR the first paint is server-rendered, and after that, page navigations are client-side (SPA-style), running .with in the browser.

Security: gate access in .with, not .ctx

A .ctx gate runs only when the point has a loader, so a loader-less page wouldn't run it at all. .with, by contrast, runs at render on every point. For an authorization gate that always fires, return an Error from .with:

export const authorizedPlugin = Point0.lets
  .plugin()
  .with(({ props: { me } }) => {
    if (!me) {
      return new ErrorPoint0('Only for authorized users', {
        code: 'UNAUTHORIZED',
      })
    }
    return { me }
  })
  .plugin()

ErrorPoint0 is the framework's default error class; you can swap it for your own class of the same-or-wider shape via .errorClass(...) — see error handling. A common production shape combines both gates: a .ctx that resolves the user for server loaders and a .with that resolves it for the render — see Plugin.

An Error you return from .with is normalized through that error class and rendered by the error component. A thrown one lands in the same .error — the mountable's error boundary catches it — and carries the same HTTP status into the SSR response; returning stays the better gate simply because it is typed. See How a thrown error reaches the error component.

НазадLoading & errorДалееMapper

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter