Skip to content

Custom Provider

custom_provider demonstrates another kind of application architecture customization: instead of putting state into globals, it uses ContextProvider to inject configuration into a subtree. In the recording, the outer workspace switches theme and density, while the nested provider on the right locally overrides the workspace without affecting sibling components.

custom provider demo

This recording is generated by docs/tapes/custom-provider.tape and runs examples/advanced/custom_provider.rs.

cargo run --example custom_provider

The root component builds the current workspace configuration as an ordinary Rust value, then passes it to ContextProvider. It affects only its children:

let settings = WorkspaceSettings {
    profile: PROFILES[profile_index.get()],
    density: density.get(),
};

element!(
    ContextProvider(value: Context::owned(settings)) {
        ProviderHeader(status: status_view)
        WorkspaceSummary
        ScopedAuditProvider
        ContextProbe
    }
)

These values are usually themes, permissions, current workspace, route context, service handles, or page-level configuration. They do not always belong in Atom because they naturally have a boundary: only a particular subtree should see them.

Context has three sources:

ConstructorMeaningFits
Context::owned(value)Provider owns the valuePage-level configuration, current workspace, route context
Context::from_ref(&value)Provider offers an immutable borrowRead-only configuration or service references already owned outside
Context::from_mut(&mut value)Provider offers a mutable borrowFramework internals temporarily passing route tables or subtree state to the next layer

Application code should prefer Context::owned. Borrowed contexts live only for the current update subtree and fit low-level components or framework-internal composition. If you want to save a value from from_ref / from_mut for a long time, it should usually become owned configuration, use_state, Atom, or explicit props.

Children use use_context::<T>() to read the type injected by a Provider. Keep the borrow guard in a small block and copy out only the fields needed for rendering:

let (name, mode, accent, density_label) = {
    let settings = hooks.use_context::<WorkspaceSettings>();
    (
        settings.profile.name,
        settings.profile.mode,
        settings.profile.accent,
        settings.density.label(),
    )
};

This avoids accidentally holding an old context borrow when you later call other hooks, render children, or enter a new Provider. The habit is especially important with use_context_mut.

Providers can nest. When an inner Provider injects the same type, child components read the nearest layer:

let scoped = WorkspaceSettings {
    profile: AUDIT_PROFILE,
    density,
};

element!(
    ContextProvider(value: Context::owned(scoped)) {
        ScopedAuditPanel
    }
)

In the recording, ScopedAuditPanel sees nested audit, while the sibling ContextProbe still sees the outer workspace. That is the core value of Provider: scope is clear and overrides do not leak.

If a component can work both inside and outside a Provider, use try_use_context:

let label = hooks
    .try_use_context::<WorkspaceSettings>()
    .map(|settings| settings.profile.name.to_string())
    .unwrap_or_else(|| "<missing>".to_string());

The try_ variant returns None when there is no Provider or the context is currently borrowed. The asserting use_context / use_context_mut variants panic with clearer diagnostics, which fits components where missing Provider means a programming error.

Provider and Atom solve different problems:

ScenarioPrefer
Subtree-level theme, current workspace, page context, local service objectContextProvider
Cross-page, cross-subtree, process-wide shared stateAtom
Component-private state that should be released on unmountuse_state

Do not put page-level state into Atom by default just because “props travel far”. First ask whether it has a natural scope; if it does, Provider gives clearer lifetime and override behavior.

When multiple components need the same environmental data, but that data should not be a global singleton, wrap your own Provider component. RouterProvider in the routing system follows the same pattern: it combines history, route context, and outlet subtree so page components can read current route abilities only through hooks.

Next, see Custom Hook to learn how business logic can become a use_*, or return to Atom Global State to compare process-wide state boundaries.