Data Fetching
Data fetching is a fundamental aspect of any application, influencing its performance and user experience. This documentation outlines how data fetching can be accomplished in a Brisa application, emphasizing best practices and efficient strategies.
Fetching Data with fetch
Brisa recommend to use the native fetch
Web API.
We don't make any modifications to the native implementation to handle caching, revalidation, or anything magical, the fetch
works as fetch
, because it is the native one. We believe that adding cache and extending the native fetch is a sign of an incorrect design of how to fetch data. So to fix this we have improved the way to share this data in your application.
As all components (server/web) can be async/await
and are rendered only once, you can do this without problems:
export default async function ServerComponent() {
const res = await fetch(/* */);
// Your server component logic
}
or
export default async function WebComponent() {
const res = await fetch(/* */);
// Your web component logic
}
In the same way, you can fetch data in the middleware
, layout
, responseHeaders
, Head
, suspense
phase, etc, and share the data with the rest of the application.
Suspense phase
Each component (server-component and web-component) allows an extension to add a suspense
component to it, which is the fallback that will be displayed while the component loads.
SomeComponent.suspense = ({}, { i18n }) => {
return <div>{i18n.t('loading-message')...}</div>
}
Suspense is useful during HTML streaming, while the server loads the data the suspense content is displayed, and once the server loads the data, during streaming the suspense is changed to the real content, all this without the client having to make any request to the server.
Async generators
async generators are also supported if you want to stream every item in a list for example:
async function* List() {
yield <li>{await foo()}</li>;
yield <li>{await bar()}</li>;
yield <li>{await baz()}</li>;
}
This can be used as a server component:
return <List />;
And the HTML is resolved via streaming.
Async generators can make sense if used in conjunction with database queries and streaming the results, example with SQLite:
import { Database } from "bun:sqlite";
const db = new Database("db.sqlite");
export default function MovieList() {
return (
<ul>
<MovieItems />
</ul>
);
}
// Streaming HTML from SQLite query
async function* MovieItems() {
for (const movie of db.query("SELECT title, year FROM movies")) {
yield (
<li>
{movie.title} ({movie.year})
</li>
);
}
}
Share server-server data between components
To share data across all parts of the server (middleware
, layout
, responseHeaders
, Head
, suspense
phase, etc) there are two ways:
- Request
store
- Context API
Example using store:
import { type RequestContext } from "brisa";
type Props = {};
export async function Main({}: Props, request: RequestContext) {
const res = await fetch(/* */);
const user = await res.json();
// Set key-value data to request store
request.store.set("user", user);
return <UserInfo />;
}
Main.suspense = ({}: Props, request: RequestContext) => (
<div>Loading user...</div>
);
export function UserInfo({}: Props, request: RequestContext) {
const user = request.store.get("user");
return <div>Hello {user.name}</div>;
}
Example using Context API:
import { type RequestContext, createStore } from "brisa";
type Props = {};
const UserCtx = createStore();
export async function Main({}: Props, request: RequestContext) {
const res = await fetch(/* */);
const user = await res.json();
// Use serverOnly inside context-provider to avoid to create a web
// component for the provider and share the data only with the
// server part
return (
<context-provider serverOnly context={UserCtx} value={user}>
<UserInfo />
</context-provider>
);
}
Main.suspense = ({}: Props, request: RequestContext) => (
<div>Loading user...</div>
);
export function UserInfo({}: Props, request: RequestContext) {
const user = request.useContext(UserCtx);
return <div>Hello {user.value.name}</div>;
}
We recommend that whenever possible you add the data to the store
inside the request. And use the Context API only in specific cases where you only want to share this data with a piece of the component tree.
The reason is that the Context API is more expensive and if you don't put the serverOnly
attribute it creates a DOM element (context-provider
) and shares the data with the rest of the web-components that are in the same component tree.
In both cases the data lives within the lifetime of the request, it is not global data, and one of the benefits is that all server-components receive the RequestContext
as a second parameter, and you can access easly to that data.
The RequestContext
is an extension of the Request, where apart from the Request API you have some extra things, such as the store.
If your data is utilized in multiple locations, and you wish to display the suspense at the lowest-level component while making only one request, we recommend passing down the promise and resolving it in all child components that utilize this data.
Share web-web data between components
To share data across all web components there are also the same two ways:
- Web Context
store
- Context API
Example using store:
src/web-components/main-app.ts
import { type WebContext } from "brisa";
type Props = {};
export default async function MainApp({}: Props, { store }: WebContext) {
const res = await fetch(/* */);
const user = await res.json();
// Set key-value data to request store
store.set("user", user);
return <user-info />;
}
MainApp.suspense = ({}: Props, webContext: WebContext) => (
<div>Loading user...</div>
);
src/web-components/user-info.ts
export default function UserInfo({}: Props, { store, derived }: WebContext) {
const username = derived(() => store.get("user").name);
return <div>Hello {username}</div>;
}
Example using Context API:
src/web-components/main-app.ts
import { type WebContext, createStore } from "brisa";
type Props = {};
const UserCtx = createStore();
export default async function Main({}: Props, request: WebContext) {
const res = await fetch(/* */);
const user = await res.json();
// Use context-provider to share the data with the
// web part
return (
<context-provider context={UserCtx} value={user}>
<user-info />
</context-provider>
);
}
Main.suspense = ({}: Props, webContext: WebContext) => (
<div>Loading user...</div>
);
src/web-components/user-info.ts
export default function UserInfo({}: Props, { useContext }: WebContext) {
const user = useContext(UserCtx);
return <div>Hello {user.value.name}</div>;
}
We recommend that whenever possible you add the data to the store
. And use the Context API only in specific cases where you only want to share this data with a piece of the component tree.
The reason is that the Context API is more expensive and it creates a DOM element (context-provider
).
Re-fetch data in web components
Web-components are reactive, and although they are only rendered once when the component is mounted, an effect
can be used to do a re-fetch
whenever a signal (prop, state, store, context...) changes.
export default async function WebComponent(
{ foo }: Props,
{ store, store }: WebContext,
) {
await effect(async () => {
if (foo === "bar") {
const res = await fetch(/* */);
const user = await res.json();
// Set key-value data to request store
store.set("user", user);
}
});
return <user-info />;
}
An effect
can be async/await
without any problems.
In this example, every time the foo
prop signal inside the effect
changes, the effect will be executed again. When updating the store, all web-components that reactively consumed this store entry will be updated with the new data.
Share server-web data between components
To share data across all parts of the server and web there are two ways:
- Request
store
usingtransferToClient
method - Context API (without
serverOnly
prop)
Example using store:
src/components/server-component.tsx
import { type RequestContext } from "brisa";
type Props = {};
export async function ServerComponent({}: Props, { store }: RequestContext) {
const res = await fetch(/* */);
const user = await res.json();
// Set key-value data to request store
store.set("user", user);
// Transfer "user" key-value to WebContext store
store.transferToClient(["user"]);
return <user-info />;
}
Main.suspense = ({}: Props, request: RequestContext) => (
<div>Loading user...</div>
);
src/web-components/user-info.tsx
import { type WebContext } from "brisa";
export function UserInfo({}: Props, { store }: WebContext) {
// Consuming "user" store value in a web-component:
return <div>Hello {store.get("user").name}</div>;
}
By default the RequestContext store
is for sharing data only during the lifetime of the request and therefore only with server components. However, the store
has the transferToClient
method to transmit keys from the dictionary to the WebContext store
.
Example using Context API:
src/context/user.ts
import { createStore } from "brisa";
export const UserCtx = createStore();
src/components/server-component.tsx
import { type RequestContext } from "brisa";
import { UserCtx } from "@/context/user";
type Props = {};
export async function Main({}: Props, request: RequestContext) {
const res = await fetch(/* */);
const user = await res.json();
// Use context-provider to share the data with the
// rest of server tree and also the web tree
return (
<context-provider context={UserCtx} value={user}>
<user-info />
</context-provider>
);
}
Main.suspense = ({}: Props, request: RequestContext) => (
<div>Loading user...</div>
);
src/web-components/user-info.tsx
import { type WebContext } from "brisa";
import { UserCtx } from "@/context/user";
export function UserInfo({}: Props, { useContext }: WebContext) {
const user = useContext(UserCtx);
return <div>Hello {user.value.name}</div>;
}
The Context API by default shares server-web data unless we pass the serverOnly
attribute to make it server-server only.
All data transferred between server-web must be serializable.
You can encrypt store data if you want to transfer sensitive data to the server actions so that it cannot be accessed from the client.