.with
- Category: Methods
.with is the point builder's real swiss-army knife. With it you 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 point
— root, base, plugin, page, layout,
component, and provider — 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, and you see the loading state until all
of them have loaded. As for the error, you see the first one that comes up. When
more than one query is used and you want to bring the data into a normal 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.
But there's much more to .with, and that's what this page is about.
- What do we do if we need to use data from one query as the input to another query?
- Or if we need to get the data for a query's input from some external hook altogether?
- Or we want to react to the status of each individual query up until the moment they load successfully?
This is exactly where the rest of .with comes in handy.
.with as a query injector
Let's look at injecting a single query into a page. We've already seen this, and it's convenient to do it exactly this way:
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>,
)But the same thing can be written by passing a function — each .with mimics a
component wrapper around the component below it, so you can freely 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>
</>
))Now the first way of using the data of one query as the input to another:
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>
))These tricks with enabled are kind of clunky, so there's another way I like
more — covered in the resolve sections below.
.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 you set 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 even carry an HTTP status, which is honored
during the SSR render — return new ErrorPoint0('Failed', { status: 500 }).
Returning any string other than 'loading', or an array from a plain function,
is a type error: those shapes are reserved.
.with as a props injector
Suppose in one .with hook we got some computation result and want to use it in
another .with hook, or on the page itself. Return a plain object and 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
Now let's combine everything to see how else .with lets us use the data of one
query in the input of another. This is what I arrived at while building a real
project on the framework, and it becomes convenient once you're writing not your
first page:
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,
requested in a previous point — 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:
| Key | What |
|---|---|
status / loading / error / data | the accumulated state so far |
props | props contributed by earlier .with calls |
queries | injected queries — in an indeterminate state here (may be loading or errored) |
params / search / input | present when the matching schema exists |
location | present on pages and layouts |
resolve | the resolve helper |
children | the rendered remainder of the chain |
LoadingComponent / ErrorComponent | the components you set with .loading / .error |
There is no ctx here — ctx is server-only and lives in .ctx and
loaders. If you need a ctx value at render, merge it into props in a
plugin first. (.ctx is server-only — cut from the client bundle: its
body and the imports it uses are removed, so it never ships to the browser;
that's the opposite category from .with.)
What you can return
| Return | Effect |
|---|---|
| a query result, or an array of them | appended to queries (data = the first) |
'loading' | render the loading component |
an Error | render the error component (status honored in SSR) |
a RedirectTask, or an Error with .redirect | redirect |
a React element (with children) | wrap the rest of the chain |
| a React element (standalone) | render it and stop the chain |
| a plain object | shallow-merge into props |
undefined / nothing | proceed to the next method |
a non-'loading' string, or an array from a plain fn | type error — those shapes are reserved |
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 propsThe 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
.withbody — and the imports it pulls in — are cut whenssr: false, or after a.clientOnly()earlier in the chain (which makes the rest of the point client-only). Nothing from that argument lands in the server bundle then. It's kept in the server build only when SSR is on, so the point can render server-side. - From the client build, nothing is cut —
.withalways ships and runs at render in the browser. (Server-only methods around it,.ctx, server.loader, …, are the ones cut from the client bundle;.withitself 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 (not throw) from .with is normalized through that error
class and rendered by the error component, the same as a thrown one — see
How a thrown error reaches the error component.
Enjoying Point0?
Star Point0
Start0
YouTube
Discord
Telegram
of the Lord Jesus Christ ☦️
With love for developers
of all backgrounds around the world ❤️