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 😎

Mapper

  • Раздел: Methods

On this page

  1. What the mapper receives
  2. data shadows queries[0].data
  3. Combine queries with props
  4. Where data comes from (the self query)
  5. Which points have .mapper
  6. Edge cases and gotchas
  7. Security

.mapper turns the raw loader and query results into the final data your render reads. By default data is just the first query's data; once you add a .mapper, data is whatever the mapper returns. Reach for it when one point pulls several queries together, or when a query's shape (paginated, nested) isn't the shape your component wants.

export const ideaListPage = generalLayout.lets
  .page('/ideas')
  .search(
    z.object({
      page: z.coerce.number().default(0),
      limit: z.coerce.number().default(2),
    }),
  )
  .loader(async ({ search: { page, limit } }) => {
    const ideasCount = await prisma.idea.count()
    const ideas = await prisma.idea.findMany({
      take: limit,
      skip: page * limit,
    })
    const nextCursor = ideasCount > (page + 1) * limit ? page + 1 : undefined
    return { ideas, ideasCount, nextCursor }
  })
  .infiniteQuery({
    getNextPageParam: (last) => last.nextCursor,
    initialPageParam: 0,
    pageParamFromInput: '?.page',
  })
  .mapper(({ data }) => ({
    ideas: data.pages.flatMap((page) => page.ideas), // flatten the paginated shape
    total: data.pages[0].ideasCount,
  }))
  .head(({ data: { total } }) => `${total} ideas`)
  .page(({ data: { ideas, total } }) => (
    <IdeaList ideas={ideas} total={total} />
  ))

An infinite query hands you data.pages — an array of page objects. The mapper flattens that into { ideas, total }, and from there .head and .page read the clean shape, not the pagination machinery.

Stripping: server-ssr-and-client — cut from the SERVER bundle when ssr:false (or after an earlier .clientOnly() in the chain): the mapper's body and the imports it uses are removed from the server build, where the framework substitutes an identity passthrough. Kept in the client build always, and in the server build only when SSR is on (it runs on the client always, and on the server only when SSR is on).

What the mapper receives

The mapper is one synchronous function. It gets a single object and returns a plain object — the new data:

.mapper(({ data, queries, props, location }) => {
  return { /* the new data */ }
})
KeyWhat
datathe current data — the first query's data, or the previous mapper's output
queriesevery query, in .with order, all loaded (success state)
propsprops contributed by .with wrappers and plugins
locationpresent whenever the point has a route location (pages and layouts); components and providers have none

There is no separate params, search, or input key on the mapper argument at runtime — reach for location.params / location.search instead, as the self-query example below does.

The mapper runs only on success — by the time it runs, every query has loaded, so queries[n].data is always there. There is no loading, error, status, or ctx key here: a mapper transforms data that is already resolved, it doesn't decide loading or error (that's .with and Loading & error).

A mapper that throws IS caught: the mapper runs inside the render (in a useMemo), and every mountable render is wrapped in an error boundary — the throw renders the nearest .error above it while the rest of the page stays alive, and during SSR its HTTP status (or a thrown redirect) still reaches the response. Still, prefer keeping the mapper total: signal errors from the data phase by returning one from a .with — that path is typed and puts .error into the SSR HTML itself; the render boundary is the safety net.

data shadows queries[0].data

Without a mapper, data is a shorthand for the first query's data:

.with(ideaQuery, ({ params }) => ({ id: Number(params.id) }))
.page(({ data, queries }) => {
  // data === queries[0].data
})

A mapper replaces it. After .mapper, data is the mapper's return value, while queries still hold the raw per-query results:

.with(ideaQuery, ({ params }) => ({ id: Number(params.id) }))      // queries[0]
.with(commentsQuery, ({ params }) => ({ ideaId: Number(params.id) })) // queries[1]
.mapper(({ queries: [idea, comments] }) => ({
  idea: idea.data.idea,
  comments: comments.data.comments,
}))
.page(({ data }) => <Idea idea={data.idea} comments={data.comments} />)
// data is the mapper's shape; queries[0]/queries[1] still hold the raw results

This is why the mapper exists for multiple .with queries: each query lands at its own index, but a render usually wants one merged object, not an array of results to thread through by hand.

With no loader and no queries, data is the empty object {} — the mapper still runs and its return becomes data.

Combine queries with props

The mapper sees props too, so you can fold a value from an upstream .with (or a plugin) into the same object as your query data:

export const ideaNewsPage = ideaLayout.lets
  .page('/news')
  .loader(async ({ params, search }) => {
    /* ... */ return { newsPosts, newsCount, nextCursor }
  })
  .infiniteQuery({
    getNextPageParam: (last) => last.nextCursor,
    initialPageParam: 0,
    pageParamFromInput: '?.page',
  })
  .with(() => ({ idea: ideaLayout.useValue('idea') })) // idea comes from the layout
  .mapper(({ data, props }) => ({
    newsPosts: data.pages.flatMap((page) => page.newsPosts),
    total: data.pages[0]?.newsCount ?? 0, // first page may be empty — guard it
    idea: props.idea, // merge the prop in
  }))
  .head(({ data: { total, idea } }) => `${total} news for idea "${idea.title}"`)
  .page(({ data: { newsPosts, total, idea } }) => (
    <IdeaNews idea={idea} posts={newsPosts} total={total} />
  ))

The render now reads one object — query data and the layout's idea side by side — and so does .head.

Where data comes from (the self query)

Calling .mapper (like .with and .head) finalizes a page's or component's own loader into its self query. After a .loader, that loader's output becomes queries[0] and is what data shadows by default:

.loader(() => ({ z: 3 }))              // becomes the self query → queries[0]
.with(query, ({ location }) => ({ y: +location.params.y })) // → queries[1]
.mapper(({ data, queries, location }) => ({
  x: queries[1].data.x,        // the injected query
  y: location.params.y,        // route param
  z: data.z,                   // the page's own loader output, via shadowed data
}))

So a page with a loader and extra .with queries reads its own data through data (the self query) and the injected ones through queries[1…]. See Page for the self-query model in full.

Which points have .mapper

.mapper is a method on the mountables — the points that produce UI:

  • page, layout, component, and provider.

It is not on a query, infinite-query, mutation, or action — those return data, they don't map it for a render. It's also off on root and plugin.

On a provider, the final .provider(mapperFn) argument is the same as a trailing .mapper — a shorthand for the common case:

export const meProvider = Point0.lets
  .provider()
  .with(({ resolve }) =>
    resolve(getMeQuery.useQuery(), ({ data }) => ({ me: data.me })),
  )
  .provider(({ data: { me }, props: { x } }) => ({ me, x })) // === a trailing .mapper

The trailing .provider(mapperFn) is also server-ssr-and-client, stripped the same way as a .mapper (see the strip note at the top).

Edge cases and gotchas

  • Return an object. The mapper's return is the new data, which must be a plain object — not an array, primitive, or Response.
  • Synchronous only. No async/await — the mapper runs inside useMemo at render. It transforms already-loaded data; it never fetches.
  • Success only. The mapper doesn't run during loading or on error, so its inputs are always resolved.
  • Keep it pure. The result is memoized on its inputs (location, props, the previous data, and each queries[n].data). Closing over a value outside those and the memo can go stale — derive everything from the argument.
  • Guard empty pages. With an infinite query, data.pages[0] can be missing on an empty first page — read it as data.pages[0]?.field ?? fallback, as the news example does.
  • .mapper chains. Declare it more than once and each mapper feeds the previous one's output as its data, so the last .mapper produces the final shape. queries stays the raw per-query results throughout — only data advances from one mapper to the next.

Security

The mapper is server-ssr-and-client (see the strip note at the top): it isn't guaranteed to run on the server, and it always ships to the browser. Don't put a server-only secret or gate in a mapper — gate access in a .with wrapper instead.

Назад.withДалееTransformer

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