Web Context

The WebContext is a set of utilities provided by Brisa to facilitate the development of web components. It encompasses various functionalities such as managing state, handling context, performing effects, and more. This context allows developers to create reactive web components in Brisa applications.

import type { WebContext } from "brisa";

export default function WebComponent(props, webContext: WebContext) {
  const {
    // Shared data across web components
    store,
    useContext,

    // Internal state of this web component
    state,
    derived,

    // Manage web component effects
    effect,
    cleanup,
    onMount,
    reset,

    // Generate unique identifiers (server/client)
    useId,

    // Add reactive styles
    css,

    // Consume translations and control internationalization
    i18n,

    // Access to the name, pathname, params, and query of the current
    // page route (also available in SSR)
    route,

    // Access to the web component DOM element
    self,
  } = webContext;
  // ... Web component implementation ...
}

In contrast to other frameworks that necessitate state imports, our methodology incorporates all state properties directly within each web component. This distinctive design empowers enhanced control over the web component's lifecycle, facilitating the expansion of the web context through precise core management using plugins.

store

store: ReactiveMap

The store property is a reactive map where values can be stored and shared among all web components. It serves as a global state accessible by all components. Values can be set and retrieved using the store.set, store.delete, store.get and store.has methods.

Example setting a value:

store.set("count", 0);

Example getting a value:

<div>{store.get("count")}</div>

For more details, refer to the store documentation.

setOptimistic

useContext

useContext: <T>(context: BrisaContext<T>) => { value: T }

The useContext method is used to consume a context value. It takes a BrisaContext as a parameter and returns a signal containing the context value. The context value is often used for passing data between a provider and multiple consumers within a component tree.

Example:

const foo = useContext(context);
return <div>{foo.value}</div>;

For more details, refer to the context documentation.

When referring to useContext, it is essential to note that this term should not be confused with the broader concept of WebContext mentioned earlier. The useContext is a Brisa Hook for consuming context value, that is piece of data that can be shared across multiple Brisa components. The WebContext denotes the overall environment and configuration specific to each web component, offering a unique and more comprehensive control mechanism. Understanding this distinction is crucial for a clear comprehension of our framework's architecture.

state

state<T>(initialValue?: T): Signal<T>

The state method is used to declare reactive state variables. It returns a Signal that represents the state, and any changes to the state trigger reactivity updates in the associated components.

Example declaration:

const count = state<number>(0);

Example usage:

<div>{count.value}</div>

Example mutation:

count.value += 1;

For more details, refer to the state documentation.

derived

derived<T>(fn: () => T): Signal<T>

The derived method is useful for creating signals derived from other signals, such as state or props. It allows developers to compute values based on existing signals.

Example of declaration:

const doubleCount = derived(() => count.value * 2);

Example of usage:

<div>{doubleCount.value}</div>

For more details, refer to the derived documentation.

effect

effect(fn: Effect): void

The effect method is used to define functions that will be executed when the component is mounted and every time a registered signal within the effect changes. It helps manage side effects such as data fetching or DOM manipulation.

Example:

effect(() => {
  // This log is executed every time someSignal.value changes
  console.log(`Hello ${someSignal.value}`);
});

For more details, refer to the effect documentation.

cleanup

cleanup(fn: Cleanup): void

The cleanup method defines functions that will be executed when the component is unmounted or to clean up an effect. It helps prevent memory leaks and ensures proper cleanup.

Example:

effect(() => {
  window.addEventListener("storage", handleOnStorage);
  cleanup(() => window.removeEventListener("storage", handleOnStorage));
});

cleanup(() => console.log("Web Component unmounted!"));

For more details, refer to the cleanup documentation.

onMount

onMount(fn: Effect): void

The onMount method is triggered only once when the component is mounted. It is useful for handling actions that should occur during the initial mount, such as setting up document events or accessing rendered DOM elements.

While the effect are executed on the fly, the onMount waits until the entire web component has been rendered.

Example:

onMount(() => {
  console.log("Yeah! Component has been mounted");
});

For more details, refer to the onMount documentation.

indicate

indicate(actionName: string): IndicatorSignal

The indicate method is used to add it in the indicator HTML extended attribute. This indicator automatically set the brisa-request class while the indicated server action is pending.

const pending = indicate('some-server-action-name');
// ...
css`
 span { display: none }
 span.brisa-request { display: inline }
`
// ...
<span indicator={pending}>Pending...</span>

You can also consume it as a signal to know if the server action is pending and to have more control inside the web component.

const  = indicate('some-server-action-name');
// ...
{pending.value && <span>Pending...</span>}

Parameters:

  • string - Indicator name. It can refer to the server action. The idea is that you can use the same indicator in other components (both server and web) using the same name to relate it to the same server action.

For more details, take a look to:

  • indicate in server components, similar method but from RequestContext.
  • indicate[Event] HTML extended attribute to use it in server components to register the server action indicator.
  • indicator HTML extended attribute to use it in any element of server/web components.

reset

reset(): void

The reset method is used to invoke all cleanup functions and clear all effects and cleanups from the memory of the web component. It is primarily intended for internal use and is exposed but may have limited applicability in many cases.

Example:

reset();

The reset method is a powerful tool that should be used judiciously. It clears all effects and cleanups, potentially affecting the web component's behavior. Ensure that its usage aligns with the desired functionality and doesn't compromise the integrity of the web component.

useId

useId(): string

The useId method generates a unique identifier for the web component. It is useful for creating unique keys for elements in lists or for other purposes that require unique identifiers. The generated ID is unique across all web components and the generation is the same on the server and client.

Example:

const passwordHintId = useId();

return (
  <>
    <input type="password" aria-describedby={passwordHintId} />
    <p id={passwordHintId}>
  </>
)

css

css(strings: TemplateStringsArray, ...values: string[]): void

The css template literal is used to inject reactive CSS into the DOM. It allows developers to define styles directly within web components using a template literal.

The styles are encapsulated within the Shadow DOM, ensuring that they do not interfere with each other across web components.

Example:

css`
  div {
    background-color: ${color.value};
  }
`;

For more details, refer to the Template literal css documentation.

i18n

i18n: I18n

The i18n object provides utilities for accessing the locale and consuming translations within components.

Example:

const { t, locale } = i18n;
return <div>{t("hello-world")}</div>;

For more details, refer to the i18n documentation.

route

route: Route

The route object provides access to the current route's name, pathname, params, and query.

Example:

const { name, pathname, params, query } = route;

The route object is available in both server-side rendering (SSR) and client-side rendering (CSR).

self

self: HTMLElement

The self property in the WebContext provides access to the DOM element of the web component. It allows developers to interact directly with the component's rendered output, enabling manipulation and customization.

Example:

self.addEventListener("click", () => {
  console.log("Web component clicked!");
});

The self property has some cases that work during the Server-Side Rendering (SSR):

  • self.attachInternals() - You can use it at the top level that during the SSR ignores the method.
  • self.shadowRoot.adoptedStyleSheets - You can use it to clean global styles in web components and it works during the SSR.
  • self.setAttribute() - You can modify attributes of the web component during the SSR, for example to force a tabindex of an element without having to manually set it when consuming the web component.
  • self.getAttribute() - You can get attributes of the web component during the SSR. Brisa reactivity does not work here, but you can use it to get attribute values passed to the web component.
  • self.addEventListener - During the SSR, the execution will be ignored, but you can use it to register events at the top level of the web component where the subscription will be made on the client.
  • self.removeEventListener: During the SSR, the execution will be ignored, but you can use it to clean events at the top level of the web component where the cleanup will be done on the client.

It is important to exercise caution when directly manipulating the DOM element using the self property. This approach can lead to potential issues, such as conflicts with the reactive nature of Brisa components. Therefore, it is recommended to use this property judiciously and only when necessary.

It is an empty object during SSR to use it in some specific cases like reseting shadowRoot.adoptedStyleSheets. Normally it is better to use it inside an effect to ensure that is executed only in the client-side.

Expanding the WebContext

The WebContext in Brisa is intentionally designed to be extensible, providing developers with the flexibility to enhance its capabilities based on project-specific requirements. This extensibility is achieved through the integration of plugins, which are custom functionalities injected into the core of each web component.

Web Context Plugins

To add plugins, you must add them in the webContextPlugins named export of the /src/web-components/_integrations.(ts|tsx|js|jsx) file.

Params:

Receives the preceding WebContext. Plugins are executed sequentially; if it is the initial plugin, it will contain the original WebContext, whereas for subsequent plugins, it will incorporate the WebContext modified by the preceding plugin.

Return:

The output will be the WebContext extended by the functionalities implemented in your plugin.

It is imperative to consistently return the remaining context properties to prevent potential disruptions in web-component functionality.

Note that the WebContext is utilized in server-side rendering (SSR) as well. Take this into consideration, as certain extensions may not be suitable for server-side usage. Therefore, it is recommended to employ typeof window === 'undefined' to determine if the code is running on the server.

Example: Tab Synchronization

src/web-components/_integrations.tsx:

import type { WebContextPlugin } from "brisa";

export const webContextPlugins: WebContextPlugin[] = [
  (ctx) => {
    ctx.store.sync = (
      key: string,
      storage: "localStorage" | "sessionStorage" = "localStorage",
    ) => {
      // Skip execution on server side (SSR)
      if (typeof window === "undefined") return;

      // Sync from storage to store
      const sync = (event?: StorageEvent) => {
        if (event && event.key !== key) return;
        const storageValue = window[storage].getItem(key);
        if (storageValue != null) ctx.store.set(key, JSON.parse(storageValue));
      };

      // Add and remove "storage" event listener
      ctx.effect(() => {
        window.addEventListener("storage", sync);
        ctx.cleanup(() => window.removeEventListener("storage", sync));
      });

      // Update storage when store changes
      ctx.effect(() => {
        const val = ctx.store.get(key);
        if (val != null) window[storage].setItem(key, JSON.stringify(val));
      });

      sync();
    };

    // The ctx now has a new method "sync" that can be used to sync a
    // store key with localStorage
    return ctx;
  },
];

In this example, the behavior of the store property is modified, enabling any web component to possess the store with an additional sync method. This method facilitates reactive synchronization of a store entry with the localStorage. If another tab modifies the value, the update will be reflected reactively in the DOM.

In any web component:

export default async function WebComponent({ }, { store }: WebContext) {
  store.sync("count", 'localStorage');

  // This value will change reactively if the localStorage
  // "count" item is updated.
  return store.get('count');

The approach to synchronizing tabs can be implemented in various ways: using web sockets, monitoring tab focus, or utilizing storage events, as demonstrated in this example. From Brisa's perspective, implementing specific signals for such scenarios might be too project-specific. Therefore, we offer the flexibility to extend these signals and access web component core extras for greater control.

Example: Reactive URL Params

This is another example to have params of the url reactive, working with SPA navigation, for example for filtering a list of items using the URL query parameters as state:

src/web-components/_integrations.tsx

import type { WebContext, WebContextPlugin } from "brisa";

function paramsPlugin(ctx: WebContext) {
  Object.assign(ctx, {
    get params() {
      let params = ctx.state<{ [k: string]: string }>();

      ctx.effect(() => {
        params.value = Object.fromEntries(
          new URLSearchParams(window.location.search).entries(),
        );

        const navigate = (e: any) => {
          params.value = Object.fromEntries(
            new URL(e.destination.url).searchParams.entries(),
          );
        };

        window.navigation?.addEventListener("navigate", navigate);
        ctx.cleanup(() =>
          window.navigation?.removeEventListener("navigate", navigate),
        );
      });

      return params;
    },
  });

  return ctx;
}

export const webContextPlugins: WebContextPlugin[] = [paramsPlugin];

Usage:

import type { WebContext } from "brisa";

export default function SearchResult({}, { params }: WebContext) {
  return <div>{params.value?.q}</div>;
}

A web component in the layout is not unmounted after SPA navigation. Therefore, we need the params signal to be reactive and update the component when the URL changes.

Ultimately, we believe that the JavaScript community will contribute more refined signals than these examples. We encourage developers to share their signals with the community to enhance the Brisa ecosystem.

TypeScript

To extend the WebContext interface in TypeScript, create a file in the root and define the typings for your extensions:

web-context.d.ts:

import "brisa";

declare module "brisa" {
  interface WebContext {
    /**
     * Augmented store with sync method
     */
    store: BaseWebContext["store"] & {
      sync: (key: string, storage?: "localStorage" | "sessionStorage") => void;
    };

    /**
     * Reactive URL params
     */
    params: Signal<{ [k: string]: string }>;
  }
}

Modify the WebContext accordingly, and if you wish to utilize elements like the current store, you can leverage the BaseWebContext.