Injections
An injection is a query the server runs before the page is sent, whose result arrives inside the HTML document. It is what makes a QLive view complete when it paints.
import {useInjection} from "@qlivedev/qlive-ts";import {Q_Foo} from "./Q_Foo";
export default function Home() {
const foos = useInjection(Q_Foo);
return <pre>{JSON.stringify(foos, null, 4)}</pre>;}Nothing is declared twice. useInjection(Q_Foo, ...) in the view is the
only place the query is named; the build’s analysis carries it to the
server, which resolves the identifier the same way the bundler does – in
the calling module and in the modules it imports directly.
Signature
Section titled “Signature”function useInjection<T>(query: GraphQLQuery<T>, params?: InjectParams): TIt returns T, the value of the query’s single top-level selection. Where
that value is a query document, what you get is a snapshot of it and
your component is subscribed: an update() re-renders it. See
Query documents.
Rules of hooks apply – call it at the top level of a view, unconditionally.
The view below injects a query document and hands it to a grid. Sorting,
filtering and paging are update() calls on that document, and each one
re-renders the view with a new snapshot. The page has no server: the
document holds the rows it was recorded with.
import { DataGrid, useInjection } from "@qlivedev/qlive-ts";import { Q_FooList, Q_FooListResult } from "./Q_FooList";
/** * The least a grid takes: a query document and the fields to show. * * Everything else is derived from the schema. The headings are the i18n() of `Foo.name` and so on, each value is * formatted as its type, a header click sorts by the column, the filter row offers what fits the type -- text * contains, a yes/no select, a number, a date range -- and the pager pages. The owner is a relation and shows its * name field, the login. * * None of it is the grid's own state. Sort, filter and page are the document's config, and the grid changes them * through update(), so anything else in the view holding the same document sees the same rows. */export default function Basic(){ const foos: Q_FooListResult = useInjection(Q_FooList, {config: {pageSize: 5}});
return ( <div className="grid-example"> <h1>A plain grid</h1> <DataGrid doc={ foos } columns={ ["name", "num", "flag", "owner", "created"] }/> </div> );}The rules
Section titled “The rules”These are not style advice. Each one is a thing the mechanism cannot do.
Only views may inject
Section titled “Only views may inject”A component that calls useInjection() is refused. In production the build
fails at startup; in dev it is reported and the server keeps serving so you
can move the call.
The reason is that a request identifies exactly one module: the view being served. A component would be invisible to the server, so its data would not be in the page, and the failure would surface in the browser as a missing injection rather than where the mistake is. It would also mean a component quietly costing a query in every view that imports it, whether or not it renders.
A component that needs data gets it from the view that injected it.
Only the view’s own calls run
Section titled “Only the view’s own calls run”Not the calls in the modules it imports. What a view imports is not what it renders.
Parameters have to be readable at build time
Section titled “Parameters have to be readable at build time”// works -- the analysis can see thisconst foos = useInjection(Q_Foo, {config: {pageSize: 5}});
// works too -- the analysis runs the FilterDSL's buildersconst foos = useInjection(Q_Foo, {config: {sortFields: [field("num").mod(value(10)).desc()]}});
// does not -- there is nothing to read at build timeconst foos = useInjection(Q_Foo, {config: {pageSize: userPreference}});Readable are literals – strings, numbers, booleans, null, and arrays and
objects of them – and the FilterDSL’s builders: field(), value(),
and(), or() and the rest, with the methods of the nodes they build. The
analysis makes those calls itself and records the node, which is the same
data the browser would have sent. A variable is not readable, not even a
constant one: the analysis does not follow it to its value.
A call with a parameter it cannot read is left out of the analysis, and the dev server says so, naming the file, the line and the part that is not readable.
The parameters are the query’s GraphQL variables, and the server reads them out of the analysis to run the query before the page exists. Anything it cannot see at build time is not there when the query runs.
For values only known at runtime, inject a starting page and move on with
update() or execute().
The query has to be a declared constant
Section titled “The query has to be a declared constant”new GraphQLQuery(...) at module scope, with a literal source. A query
assembled at runtime is not there to be read when the build reads it, so
there is nothing to carry to the server and nothing to generate a result
type from.
The full statement of that rule, and the two accepted ways of naming a query, are Generated artifacts.
Injection ids and __id
Section titled “Injection ids and __id”The result goes out under an injection id: the route of the view, a
slash, and the GraphQL operation name. useInjection(Q_FooList) in
app/grid/Sorting.tsx is grid/sorting/Q_FooList. The server takes the
route from the view module it serves, the browser from its location, so
neither side needs telling, and views that inject the same query stay apart
when one page runs several of them – as the
Filters demo does, with grid/Filters and
grid/Sorting both injecting Q_FooList.
A view injecting the same query twice needs to disambiguate at least one of
them. __id takes the place of the operation name:
const first = useInjection(Q_Foo, {__id: "first", config: {pageSize: 5}});const second = useInjection(Q_Foo, {__id: "second", config: {offset: 10}});__id is not a variable of the query – it is stripped before execution,
and a query does not declare it. Two calls reading the same id is an error,
reported at build time: one of them would get data it did not ask for, and
only you can say which.
Query config parameters
Section titled “Query config parameters”Naming no config at all is the normal case:
useInjection(Q_Foo);The injection still runs with a complete QueryConfig. It is assembled on
the server, most general first:
- the defaults a fresh
QueryConfighas, - the delta the row type of the queried document declares, if it declares one – see Per-type query config below,
- whatever the call itself names.
So the page size, sort order and standing condition of a type are said once, on the server, next to the type – and every view querying those rows gets them without repeating itself.
Where a view does want something else, it gives a delta: the fields it cares about, and no others.
useInjection(Q_BazList, {config: {pageSize: 50}});The rest still comes from the two layers underneath, the same way
QueryConfigDelta spreads over a document’s current config on the client.
That is what strong defaults are for – to be broken, and broken only where something local wants something different. The example above is a picker that needs its options all at once, which is a fact about that one form and not about the type, so it is said at the call site. A page size every view of those rows should have is the opposite kind of fact and belongs on the type, where saying it once covers all of them.
Per-type query config
Section titled “Per-type query config”What a type declares is written by a QueryConfigMetadataProvider bean on
the Java side:
@Beanpublic MetadataProvider queryConfigMetadata(){ return QueryConfigMetadataProvider.newProvider() .forAllTypes() .pageSize(20) .andForType(Foo.class) .sortFields("name") .maxPageSize(100) .build();}forAllTypes() is the house rule for every row type the domain has a query
document for; forType(Class) and forTypes(Class...) are the departures
from it. Chain statements with andForType(), andForTypes() and
andForAllTypes(), and close the chain with build().
A type may only be named once – say it with forAllTypes() and depart from
it per type rather than declaring the same type twice.
Reading the raw injection
Section titled “Reading the raw injection”import {data} from "@qlivedev/qlive-ts";
const {value, type, meta} = data("grid/sorting/Q_Foo");useInjection() is the normal way in. data() is for the cases that want
the GraphQL type or the meta alongside the value, or the id of an injection
no view claimed. It takes the full injection id, route included. It gives
you the value in whatever state it is in – raw
as received until the first read converts it – and subscribes to nothing.
Both, and the query and parameter types they are spelled with, are Injection in the API reference.
When something goes wrong
Section titled “When something goes wrong”A view reads its injection unconditionally, so a page served without one fails in the browser where the reason is no longer visible. QLive therefore treats a failing injection as fatal for the page rather than leaving it out, and reports the mistakes it can see – a call outside a view, two calls on one id, an identifier resolving to no query – off the analysis instead: at startup for a production build, and at every push in dev.