Context and Scopes

A deeply nested component may need data owned by a higher-level component.

Passing that data through every intermediate component adds props that those components do not use. Retend provides Scopes for sharing data with descendants.

A Scope makes a value available to descendant components. A child can read the value without receiving it through each intermediate component.

Creating and Using Scopes

Create a Scope with createScope():

import { createScope } from 'retend';

// Give it a name to help with debugging
export const ThemeScope = createScope('Theme');

The returned Scope object contains a Provider component. Wrap the relevant layout in the Provider and pass the shared data through its value prop:

import { ThemeScope } from './scopes';

function App() {
  const currentTheme = 'dark';

  return (
    <ThemeScope.Provider value={currentTheme}>
      <MainLayout />
    </ThemeScope.Provider>
  );
}

A component inside MainLayout can read the theme with useScopeContext() without receiving it from intermediate components:

import { useScopeContext } from 'retend';
import { ThemeScope } from './scopes';

function ThemedButton() {
  // Grab the data directly from the Scope
  const theme = useScopeContext(ThemeScope);

  return (
    <button
      type="button"
      class={[
        'btn',
        { 'btn-dark': theme === 'dark', 'btn-light': theme === 'light' },
      ]}
    >
      Click Me
    </button>
  );
}

If no Provider exists above a component that reads a Scope, Retend throws an error.

Passing Reactive Data (Cells)

Shared data can be reactive, such as login state or shopping cart contents. Pass a Cell to a Scope like any other value:

import { Cell, createScope, useScopeContext } from 'retend';

export const UserScope = createScope('User');

function App() {
  // 1. Create a reactive Cell
  const user = Cell.source({ name: 'Alice', role: 'guest' });

  return (
    // 2. Pass the entire Cell into the Provider
    <UserScope.Provider value={user}>
      <Dashboard />
    </UserScope.Provider>
  );
}

Child components can read the Cell from the Scope, use .get() to read its value, and use .set() to update it. Updates are reflected wherever the Cell is used.

function UserProfile() {
  // 3. Grab the Cell from the Scope
  const userCell = useScopeContext(UserScope);

  // 4. Create derived state from it
  const userName = Cell.derived(() => userCell.get().name);

  const upgradeRole = () => {
    // 5. Update the shared Cell
    const current = userCell.get();
    userCell.set({ ...current, role: 'admin' });
  };

  return (
    <div>
      <p>Hello, {userName}</p>
      <button type="button" onClick={upgradeRole}>
        Upgrade to Admin
      </button>
    </div>
  );
}

Scopes and Global Variables

A Cell in a separate module can also be imported wherever it is needed.

Scopes provide two additional properties:

  1. Independent instances: Multiple Providers can exist on the same page, and each has isolated state.
  2. Automatic cleanup: When the part of the component tree containing a Provider is removed, Retend cleans up the scoped data. A module-level global remains available until the page is unloaded.

Scopes associate shared data with a part of the component tree.