Blade provides the following React hooks:
useLocation
Mimics document.location and thereby exposes a URL
object containing the URL of the current page.
Unlike in document.location
, however, the URL is not populated, so dynamic path segments of pages will not be populated with their respective value.
import { useLocation } from 'blade/hooks'; const location = useLocation();
useParams
Exposes the keys and values of all parameters (dynamic path segments) present in the URL.
For example, if the URL being accessed is /elaine
and the page is named [handle].tsx
, the returned object would be { handle: 'elaine' }
.
import { useParams } from 'blade/hooks'; const params = useParams();
useRedirect
Used to transition to a different page.
import { useRedirect } from 'blade/hooks'; const redirect = useRedirect(); redirect('/pathname');
The following options are available:
redirect('/pathname', { // If the pathname provided to `redirect()` contains dynamic segments, such as // `/[handle]`, you can provide a value for those segments here. extraParams: { handle: 'elaine' }, // If a part of your app relies on React state that is also reflected in the URL // (e.g. this often happens with `?search` text), you can avoid keeping React state // by using this option and simply reading the output of `useLocation()`. // // However note that this option is only available on the client. immediatelyUpdateQueryParams: true });
usePopulatePathname
As mentioned in the docs for useLocation()
, dynamic path segments (such as /[handle]
) are not populated in the URL
object exposed by the hook.
To populate them, you can pass the pathname to the usePopulatePathname()
hook.
import { usePopulatePathname } from 'blade/hooks'; const populatePathname = usePopulatePathname(); // Assuming that the URL being accessed is `/elaine` and the page is called // `[handle].tsx`, this would output `/elaine`. populatePathname('/[handle]');
The following options are available:
populatePathname('/[handle]', { // May be used to provide values for params that are present in the pathname that was // provided to `populatePathname()`, but are not present in the current URL. This is // essentially a clean way of using `populatePathname(`/${params.handle}`)`. extraParams: { handle: 'elaine' } });
useNavigator
Mimics window.navigator and thereby exposes an object containing details about the current user agent.
import { useNavigator } from 'blade/hooks'; const navigator = useNavigator(); navigator.userAgent; // See MDN docs navigator.languages; // See MDN docs navigator.geoLocation; // See MDN docs (currently under construction)
useCookie
Allows for reading and writing cookies. On the server, cookies are attached to the headers of the resulting HTTP request before the JSX stream of React elements begins. On the client, they modify the cookies stored in the browser directly.
import { useCookie } from 'blade/hooks'; const Page = () => { const [tokenCookie, setTokenCookie] = useCookie('token'); if (!tokenCookie) setTokenCooke('a-value'); return <div>I am a page</div>; };
The following options are available:
setCookie('a-value', { // By default, cookies are set as HTTP-only. To let JavaScript access them in the // browser, you can set this option to `true`. client: false, // By default, cookies receive a path value of "/". You can customize it using this // option if needed. path: '/' });
The max age of cookies currently defaults to 365 days, which is not yet customizable.
useMutation
(Client)Allows for performing data mutations and updates all use
queries accordingly.
The function exposes all write query types available in Blade, but any other data source may be used as well.
import { useMutation } from 'blade/client/hooks'; const { set, add, remove, batch } = useMutation(); await set.account({ with: { handle: 'elaine' }, to: { handle: 'mia' } }); await add.account.with.handle('elaine'); await remove.account.with.handle('elaine'); // If you need to run multiple mutation queries serially, use a single transaction to // avoid unnecessary hops to the database. await batch(() => [ set.account({ with: { handle: 'elaine' }, to: { handle: 'mia' } }), add.account.with.handle('elaine'), remove.account.with.handle('elaine') ]);
When a mutation happens, its queries are combined with any potential read queries that might be necessary to update the use
hooks on the current path and sent to the database as a single transaction.
The following options are available for all query types:
await add.account.with.handle('elaine', { // Render a different page after the mutation has been completed. redirect: '/accounts/elaine', // For the above, you may also decide to access the result of the mutation, such that // the pathname of the destination page depends on the mutation result. // // The curly braces represent a segment that will be replaced with the result of the // mutation query. The number (0) indicates the item in the list of results (when // using `batch`, you might have multiple queries) and the string ("handle") // indicates the slug of the field that should be used. redirect: '/accounts/{0.handle}' });
usePagination
(Client)When retrieving multiple records (such as using get.accounts()
), a maximum of 20 records are returned at once.
This limit ensures that memory overflows are avoided by default. This means there cannot be a scenario where your Queries retrieve so many records that the memory available to your Blade application (which is determined by your cloud provider) fills up, and the application crashes.
Furthermore, always returning the same amount of records within your application (rather than returning more records if there are more available) ensures consistent response times, as the response time does not depend on how many records are being provided. Like this, you will be able to track and improve your app’s performance much more easily.
To retrieve more than 20 records, making use of “pagination” is advised, which means additional records are loaded on demand. If your application provides a UI, this may, for example, manifest in the following two ways:
Which of these implementations (or any other) you may choose is up to you. The usePagination
hook provides a function that you can
call to easily load the next page without having to modify your query.
For example, you might use the following read query in your page (on the server):
import { use } from 'blade/server/hooks'; import SomeComponent from './components/test.client'; const Page = () => { const accounts = use.accounts(); return <SomeComponent records={accounts} recordsNextPage={accounts.moreAfter}>; }
Within SomeComponent
(which must be a client component, identified through .client
in its name), you could then paginate the list of records you've retrieved like this:
import { usePagination } from 'blade/client/hooks'; export const SomeComponent = ({ records, recordsNextPage }) => { const { paginate, resetPagination } = usePagination(recordsNextPage); return ( <div> <button onClick={() => paginate()}>Show next page</button> <button onClick={() => resetPagination()}>Show initial page</button> </div>; ); };
The following options are available:
usePagination(moreAfter, { // By default, a `?page` parameter will be added to the URL, which allows for sharing // the current pagination status of the page with other people. Setting this argument // to `false` will avoid that. // // For example, if you're paginating a large list of records in the middle of your // page, you should likely use the default value, such that users can share a link // to a particular page of records with other people. // // If you're paginating a list of records within an overlay, however, for example, // you should set this to `false`, since people don't need to be able to link to a // specific page of records within an overlay that's nested deeply into your UI. updateAddressBar: true });
usePaginationBuffer
(Client)Concatenates arrays based on pagination. Whenever the current paginated page changes, the provided items will be concatenated with the previously provided list of items.
When implementing pagination in an application, one of the key considerations is how to efficiently manage the complete array of records, since your backend should only ever return a single page at once, and never the full list, as the latter would degrade performance and affect memory usage.
When displaying a list of records, usePaginationBuffer
automates this for you. You simply provide it with the result of a read query, and it intelligently concatenates the result of the query for you, allowing you to operate on the full list of records if needed.
For example, you might use the following read query in your page (on the server):
import { use } from 'blade/server/hooks'; import SomeComponent from './components/test.client'; const Page = () => { const accounts = use.accounts(); return ( <SomeComponent moreBefore={accounts.moreBefore} moreAfter={accounts.moreAfter}> {accounts.map(account => <Row record={account} />)} </SomeComponent>; ); }
Within SomeComponent
(which must be a client component, identified through .client
in its name), you could then concatenate the list of records like so:
import { usePagination, usePaginationBuffer } from 'blade/client/hooks'; export const SomeComponent = ({ children: defaultChildren, moreAfter, moreBefore }) => { const { paginate, resetPagination } = usePagination(moreAfter); const [children] = usePaginationBuffer(defaultChildren, { moreBefore }); return <div>{children}</div>; };
This allows for shipping the least amount of code to the client, because the entire layout (including Row
, with the only exception being SomeComponent
) will be rendered only on the server, so no unnecessary code will leave the server, ensuring that no unnecessary JavaScript must be downloaded.
The following options are available:
usePaginationBuffer(list, { // The pagination cursor for the previous page of your record list. Must be provided. moreBefore: '...', // By default, any changes to `list` will be reflected in the concatenated list // returned by `usePaginationBuffer()`. This default is useful because that means // automatically revalidated read queries will see their results reflected. // // However, if you want to force a list to untouched by revalidation, you can set // this option to `false`, which will result in the returned list staying static, // even if the provided `list` is modified. // // For example, if you are paginating a list of records within an overlay, you might // want the list of records within the overlay to never automatically update. allowUpdates: true });
You may also decide to pass a TypeScript generic with the type of provided array item:
usePaginationBuffer<ReactElement>();
useLinkEvents
(Client)In the majority of cases, you should use Blade's <Link>
component to display links that should automatically result in a page transition (links pointing to external pages should just use anchor elements).
In the rare scenario that you need to capture the click event of a link element yourself for other purposes, however, you can use useLinkEvents
to manually trigger the same event handlers that would normally be triggered for a <Link>
component instance.
For example, if a drag-and-drop system is used, it might want to use those event handlers for different purposes (detecting whether an element is being dragged or dropped), so it could fire the ones provided by Blade at a different time.
import { useLinkEvents } from 'blade/client/hooks'; const eventHandlers = useLinkEvents('/pathname'); <button {...eventHandlers}>I am a link</button>
useQueryState
(Client)In React, ephemeral state is managed with the useState
hook. For example, if your interface contains a dialog that should open when a button is clicked, you would typically store the status of the dialog in useState
. This state does not persist across sessions, meaning if a user closes the browser tab, the state will be lost.
To persist state, it should be stored with the client-side useMutation
hook, which has the ability to commit changes to your database. This state, since it is stored in your database, can then also be accessed by other users, if needed.
For maximum flexibility, Blade offers a third kind of state: In addition to ephemeral and persistent state (which we've covered above), there is also URL state. This kind of state can be re-used across different browser sessions, as long as the same URL is used to access the page.
For example, if your application contains a list of items within the interface, you might decide to add a ?search
query parameter as part of the URL, in order to store search keywords in the URL. Like that, the state does not need to be stored in your database, but it also wouldn't get lost when the page gets reloaded, and it would apply whenever the URL is shared with other people.
In those kinds of scenarios, you can add the useQueryState
hook, which essentially auto-generates a useState
hook that depends on a query parameter in the URL. Reading the value of the hook therefore reads the value of the query parameter in the URL, and setting the value of the hook therefore also updates the value of the query parameter in the URL — all without you needing to write extra code.
import { useQueryState } from 'blade/client/hooks'; const ClientComponent = () => { const [hello, setHello] = useQueryState('hello'); return ( <> <input onChange={(e) => setHello(e.target.value)} value={hello} /> <p>Hello, {hello || 'world'}!</p> </> ); };
use
(Server)Usually, in React, the use hook is used to consume a Promise or context.
In Blade, however, since Blade purposefully does not support Suspense and also does not support asynchronous components, the hook is used to load data instead.
Specifically, the hook reflects the full capabilities of the RONIN query syntax (which is as powerful as SQL) and thereby allows for easily querying records on your database.
By default, if you provide a RONIN_TOKEN
environment variable that contains a RONIN app token, these
queries will simply target your RONIN database. However, if the environment variable is not provided,
any other data source can be defined using triggers instead.
import { use } from 'blade/server/hooks'; const Page = () => { // "accounts" would be the name of your model. const records = use.accounts(); return <>{records.map(record => <span>{record.name}</span>)}</>; };
The queries you provide can be as complex as you would like them to be (see the query syntax docs for more details on what you can do):
import { use } from 'blade/server/hooks'; const records = use.members({ with: { team: { notBeing: null }, email: { endingWith: '@site.co' } }, orderedBy: { descending: ['joinedAt'] } });
Note that, in order to avoid data waterfalls, data can only be read in server components. On the client, data can only be modified.
The queries of your page and its surrounding layouts are executed as a single database transaction, in order to ensure instant page renders.
If the same read query is used in different layouts surrounding a page (or the page itself), Blade will automatically de-deduplicates the queries, such that every query is only run a single time, and all other instances of that same query simply receive its results.
Because of that, if you want to re-use the exact same query in multiple places across your application, it is recommended to add a utility file containing a React hook that returns the query that you would like to reuse. For example:
import { useCookie } from 'blade/hooks'; import { use } from 'blade/server/hooks'; export const useCurrentSession = () => { const [token] = useCookie('token'); if (!token) return null; return use.session.with({ token }); };
The React hook above parses the value of a token
cookie and then uses that value to query
a particular record of the session
model. As you can see, since we are using a React hook,
we can compose several different other hooks together to create a reusable utility hook that
performs multiple tasks and then uses their result to query the data we are interested in.
In fact, even the reusable utility hook can, itself, be composed further together with other query hooks, if we want to, to enable the leanest code possible:
import { use, useBatch } from 'blade/server/hooks'; import { useCurrentSession } from '@/utils/hooks'; const [session, team] = useBatch(() => [ useCurrentSession(), use.team.with.handle('engineering') ]);
If you don't need to reuse entire queries or would like to store queries directly in your database like SQL views, you should consider using query presets instead.
useBatch
(Server)To run multiple queries within a single layout or page, the useBatch
hook can be used:
import { useBatch } from 'blade/server/hooks'; const [accounts, posts] = useBatch(() => [ use.accounts(), use.posts() ]);
useCountOf
(Server)To count records, the useCountOf
hook can be used with the same syntax as the use
hook.
The hook always returns a single integer, which reflects the amount of records that were found that are matching the provided conditions.
useMetadata
(Server)Allows for defining the metadata of a layout or page and thereby populates the head element of the rendered page with metadata elements.
import { useMetadata } from 'blade/server/hooks'; const Page = () => { useMetadata({ title: 'Page Title' }); return <div>I am a page</div>; };
The following options are available:
useMetadata({ // The browser tab title of the page. Titles defined in surrounding layouts are // joined together with the title of the page using an em dash separator. title: 'Page Title', // Additional metadata for browsers. themeColor: '#4285f4', colorScheme: 'light', description: 'This is a description', icon: 'https://example.co/icon.png', // Additional metadata for Open Graph (OG). openGraph: { title: 'Page Title', description: 'This is a description', siteName: 'Site', images: [{ url: 'https://example.co/banner.png'; width: 1280; height: 720 }], }, // Additional metadata for X (formerly Twitter). x: { title: 'Page Title', description: 'This is a description', card: 'summary_large_image', site: '@nytimes', creator: '@jk_rowling', images: ['https://example.co/banner.png']; }, // Since Blade does not allow for replacing the `<html>` or `<body>` elements, // these options can be used to attach classes to them. htmlClassName: 'lg:overflow-hidden', bodyClassName: 'text-gray-800' });