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:

import { createContext } from 'what-framework';

// Create the context
const ThemeContext = createContext('light');

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 anywhere 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.

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:

<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 { count, doubled, increment } = useCounter();
  return (
    <div>
      <p>Count: {count()} / Doubled: {doubled()}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

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.