Context

Share state across your component tree without passing props through every level.

The Prop Drilling Problem

Imagine you have a theme that many components need:

// Without context, you pass theme through every level
function App() {
  const theme = signal('dark');
  return <Layout theme={theme} />;
}

function Layout({ theme }) {
  return <Sidebar theme={theme} />;  // just passing it through
}

function Sidebar({ theme }) {
  return <Menu theme={theme} />;  // still just passing
}

function Menu({ theme }) {
  return <MenuItem theme={theme} />;  // ugh, more passing
}

function MenuItem({ theme }) {
  // Finally! We actually use it
  return <span className={theme}>...</span>;
}

This is called prop drilling. Context solves it.

Creating Context

Create a context with a default value. Give the default the same shape as the value your providers pass, because a component rendered outside any provider gets the default and will use it the same way:

import { createContext, signal } from 'what-framework';

// Providers below pass a signal, so the default is a signal too
const ThemeContext = createContext(signal('light'));

If the default were the bare string 'light', the consumer below would call theme() on a string and throw the moment it rendered outside a provider.

Providing Context

Wrap your tree with the context provider:

import { signal } from 'what-framework';

function App() {
  const theme = signal('dark');

  return (
    <ThemeContext.Provider value={theme}>
      <Layout />
    </ThemeContext.Provider>
  );
}

Consuming Context

Access the value from any component in the tree:

import { useContext } from 'what-framework';

function MenuItem() {
  const theme = useContext(ThemeContext);

  return (
    <span className={() => theme()}>
      Menu Item
    </span>
  );
}

No more prop drilling! Components in between don't need to know about theme at all.

Call useContext during render

useContext resolves the nearest provider by walking up from the component being rendered. Call it from an event handler, an effect, or any other deferred callback and there is no component to walk up from: it silently returns the context default and logs a dev warning. Read it once in the component body and close over the result.

This is also why a guard like the useAuth hook below cannot catch the mistake unless the default is null. A non-null default looks like a successful read.

Complete Example

src/context/theme.js
import { createContext, signal } from 'what-framework';

// Create context with default
export const ThemeContext = createContext(null);

// Provider component with state and actions
export function ThemeProvider({ children }) {
  const theme = signal('light');

  const value = {
    theme,
    toggle: () => theme.set(t => t === 'light' ? 'dark' : 'light'),
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
src/components/ThemeToggle.jsx
import { useContext } from 'what-framework';
import { ThemeContext } from '../context/theme';

export function ThemeToggle() {
  const { theme, toggle } = useContext(ThemeContext);

  return (
    <button onClick={toggle}>
      Current: {theme}
    </button>
  );
}
src/app.jsx
import { ThemeProvider } from './context/theme';
import { ThemeToggle } from './components/ThemeToggle';

function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  );
}

function Header() {
  return (
    <header>
      <ThemeToggle />
    </header>
  );
}

Nested Providers

You can nest providers to override values in subtrees. Every provider must carry the shape consumers expect, which here is a signal:

const light = signal('light');
const dark = signal('dark');

<ThemeContext.Provider value={light}>
  <Header />  {/* uses 'light' */}

  <ThemeContext.Provider value={dark}>
    <Sidebar />  {/* uses 'dark' */}
  </ThemeContext.Provider>

  <Footer />  {/* uses 'light' */}
</ThemeContext.Provider>

Common Patterns

Auth Context

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const user = signal(null);
  const isLoggedIn = computed(() => user() !== null);

  const login = async (credentials) => {
    const userData = await api.login(credentials);
    user.set(userData);
  };

  const logout = () => user.set(null);

  return (
    <AuthContext.Provider value={{ user, isLoggedIn, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

Custom Hook Pattern

// Create a custom hook for cleaner usage
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Usage
function Profile() {
  const { user, logout } = useAuth();
  // ...
}

Alternative: Stores

For global state that doesn't need the component tree hierarchy, createStore provides a simpler pattern. Use derived() to mark computed properties (this distinguishes them from actions):

import { createStore, derived } from 'what-framework';

const useCounter = createStore({
  // State (plain values become signals)
  count: 0,

  // Computed (must use derived marker)
  doubled: derived(state => state.count * 2),

  // Actions (plain functions, use `this`)
  increment() { this.count++; },
  decrement() { this.count--; },
});

// Use anywhere in your app, no Provider needed
function Counter() {
  const store = useCounter();
  return (
    <div>
      <p>Count: {() => store.count} / Doubled: {() => store.doubled}</p>
      <button onClick={store.increment}>+</button>
    </div>
  );
}

Keep the store object and read through it inside a thunk. State and derived keys on the returned object are plain getters over the underlying signals, not accessors, so count() throws and const { count } = useCounter() snapshots the value at that instant and never updates. Actions are ordinary properties and can be destructured safely. See Stores.

Why derived()?

The derived() marker is required so that createStore can distinguish computed properties from actions. Without it, a function like state => state.count * 2 looks the same as an action like item => { ... }. The marker removes this ambiguity.

Best Practices

  • Don't overuse context. Props are simpler for 1-2 levels of nesting.
  • Split contexts by concern. Don't put unrelated state in one context.
  • Provide signals, not values. Pass signals so consumers can subscribe to updates.
  • Create custom hooks. Hide context access behind a hook for better DX.