Skip to content

Passing Data Deeply with Context

Overview

Passing data through components that only hand it on is tedious and easy to get wrong. Context lets a component read a value that an ancestor provides, skipping the middle of the tree. In ReactPy it is purely a plumbing tool — it carries values down and nothing else — and there are three behaviours worth knowing before you rely on it.

You will learn

  • How to recognise prop drilling worth replacing
  • The three steps: create, use, provide
  • Why a component cannot read the context it provides
  • Why removing a provider throws away the subtree's state

The problem with passing props

Here is a list of places, and each row is coloured by the current theme. Only the deepest component cares about the theme, but the prop has to travel through everyone:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PLACES = ["Almaty", "Astana", "Shymkent", "Karaganda", "Turkestan"]


@component
def place_label(place, theme):
    color = "coral" if theme == "dark" else "black"
    return html.li({"style": {"color": color}}, place)


@component
def place_list(places, theme):
    return html.ul([place_label(place, theme) for place in places])


@component
def list_of_places(places, theme):
    # Renders its children inside the list box.
    return html.div(
        {"style": {"border": "1px solid #ccc", "padding": "8px"}},
        place_list(places, theme),
    )


@component
def section(places, theme):
    return html.section(html.h3("Places"), list_of_places(places, theme))


@component
def app():
    theme, set_theme = use_state("dark")

    def toggle(event):
        set_theme("light" if theme == "dark" else "dark")

    return html.div(
        html.button(
            {"onClick": toggle}, f"Switch to {'light' if theme == 'dark' else 'dark'}"
        ),
        html.p({"id": "theme"}, f"theme={theme}"),
        html.div({"id": "places"}, section(PLACES, theme)),
    )
1
# TODO

app holds the theme. section receives it, list_of_places receives it, place_list receives it, and only place_label reads it. Three components mention a value they never use, and adding a row to list_of_places means threading theme through one more signature.

That is the shape worth replacing: a prop that a component only forwards. If a component would use a prop itself, pass it as a prop — that is easier to trace.

Context: an alternative to passing props

Three steps, and the order matters: create the context, read it where you need it, and provide it somewhere above.

Step 1: Create the context

A context is an object you create once, usually at module level:

from reactpy import create_context

ThemeContext = create_context("light")

The argument is the default — what use_context returns when no provider sits above the reader. Pick something that renders sensibly on its own, because a component will occasionally render without a wrapper.

The object is matched by identity, so create it once and import it everywhere. Two create_context calls are two unrelated contexts even with identical defaults.

Step 2: Use the context

In the component that needs the value:

from reactpy import use_context

theme = use_context(ThemeContext)

No parameter, no forwarding. The components in between never learn the value exists:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ThemeContext = create_context("light")


@component
def place_label(place):
    # Reads the theme directly: no prop had to travel here.
    theme = use_context(ThemeContext)
    color = "coral" if theme == "dark" else "black"
    return html.li({"style": {"color": color}}, place)


@component
def place_list(places):
    # No theme parameter, and none to forward.
    return html.ul([place_label(place) for place in places])


@component
def list_of_places(places):
    return html.div(
        {"style": {"border": "1px solid #ccc", "padding": "8px"}},
        place_list(places),
    )


@component
def section(places):
    return html.section(html.h3("Places"), list_of_places(places))


@component
def app():
    theme, set_theme = use_state("dark")

    def toggle(event):
        set_theme("light" if theme == "dark" else "dark")

    return html.div(
        html.button(
            {"onClick": toggle}, f"Switch to {'light' if theme == 'dark' else 'dark'}"
        ),
        html.p({"id": "theme"}, f"theme={theme}"),
        # The provider is a component: it renders its children with no wrapper.
        ThemeContext(html.div({"id": "places"}, section(PLACES)), value=theme),
    )
1
# TODO

place_label calls use_context, and place_list, list_of_places, and section no longer mention theme at all. Verified: this version and the prop-drilling version above render the same five rows with the same colours, and both respond to the toggle identically.

Without any provider at all, every reader gets the default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ThemeContext = create_context("light")


@component
def label(text):
    theme = use_context(ThemeContext)
    return html.p({"id": f"label-{text}"}, f"{text}: theme={theme}")


@component
def app():
    # Nothing wraps these in ThemeContext, so use_context falls back to the
    # value passed to create_context.
    return html.div(html.h3("No provider"), label("a"), label("b"))
1
# TODO

Both labels read theme=light — the value from create_context, not None. This is what makes it safe to reuse a component in a page that never sets the theme up.

Step 3: Provide the context

Wrap the subtree you want to see the value:

ThemeContext(children, value=theme)

The context object is a component. Calling it produces a provider element whose children render in place, adding no wrapper of its own — verified: the element with id="places" contains the whole tree with nothing extra between it and the list.

A provider only covers its own children. A sibling rendered after it sees the default, and a provider nested inside another one shadows it for its own subtree:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@component
def deep_label(tag):
    # Three levels down, still no prop in sight.
    return html.p({"id": tag}, f"{tag} sees: {use_context(ThemeContext)}")


@component
def middle():
    return html.div(html.h4("middle"), deep_label("deep"))


@component
def wrapper():
    # An intermediate component that neither receives nor forwards a theme.
    return html.div(html.h4("wrapper"), middle())


@component
def inner_scope():
    return html.div(
        deep_label("inner"),
        # A provider nested inside another one shadows it for its subtree.
        ThemeContext(deep_label("deepest"), value="innermost"),
    )


@component
def app():
    return html.div(
        # Same context, several providers, several scopes.
        deep_label("outside"),
        ThemeContext(wrapper(), value="outer"),
        ThemeContext(inner_scope(), value="middle-provider"),
        deep_label("after"),
    )
1
# TODO

Verified readouts: outside and after see light (the default), deep — three levels below outer — sees outer, inner sees middle-provider, and deepest, inside the nested provider, sees innermost. The nearest provider wins; the search walks up and stops.

Pitfall

use_context belongs in the component that uses the value, not in the parent that places it. Write ThemeContext(deep_label(), value="x"), not ThemeContext(html.p(f"{use_context(ThemeContext)}"), value="x"). The second one calls the hook while the parent renders, so it reads the parent's context — the provider has not been entered yet. This is easy to hit by accident: a label formatted inline above a provider reported the default light while its own provider supplied middle-provider.

Using and providing context from the same component

A component cannot read the context it provides. The provider applies to its children, and a component is not its own child:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@component
def label(text):
    return html.p({"id": f"label-{text}"}, f"{text} sees: {use_context(ThemeContext)}")


@component
def section():
    theme, set_theme = use_state("dark")

    def toggle(event):
        set_theme("light" if theme == "dark" else "dark")

    return html.div(
        html.button({"onClick": toggle}, "toggle"),
        # Reads the context the way any *consumer* does...
        html.p({"id": "self"}, f"section itself sees: {use_context(ThemeContext)}"),
        # ...and provides it for everything below, which is not itself.
        ThemeContext(label("child"), value=theme),
    )


@component
def app():
    return html.div(html.h3("Self-provider"), section(), label("outside"))
1
# TODO

The child reads child sees: dark while the component that rendered the provider reads section itself sees: light — the default, before and after the toggle. If the component needs the value too, read the state it comes from: theme is right there in section, so use it directly.

Context passes through intermediate components

The value reaches any depth as long as the provider is a genuine ancestor. wrapper and middle in the nesting example above receive no props at all, and deep still resolves the theme.

This is also what makes context the right tool for a component you do not control — a third-party list renderer, a slot you pass children into. Anything rendered inside the provider's subtree can read it, however deep.

Note

A component's state, however, belongs to its position in the tree. Two sibling subtrees that both mount draft() get two independent pieces of state (verified: x#1 and x#2 under two providers of the same context). Context shares what flows down from the provider; it does not merge instances below it.

Removing a provider resets the subtree below it

A provider is a component, so adding or removing one changes the shape of the tree — and ReactPy identifies a component by where it sits. The subtree below is unmounted and rebuilt, with its state. Press change value and the draft you typed survives; press toggle provider and it is gone:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@component
def draft():
    # State owned by the leaf. It is lost if this component is remounted.
    text, set_text = use_state("")
    body = use_ref(0)
    body.current += 1
    return html.div(
        html.input(
            {
                "value": text,
                "onChange": lambda event: set_text(event["target"]["value"]),
                "placeholder": "type something",
            }
        ),
        html.p(
            {"id": "draft"},
            f"draft={text!r} renders={body.current} theme={use_context(ThemeContext)}",
        ),
    )


@component
def panel():
    wrap, set_wrap = use_state(True)
    theme, set_theme = use_state("dark")

    def toggle_wrap(event):
        # Adding or removing the provider changes the shape of the tree, so the
        # subtree below is unmounted and rebuilt: the draft text is lost.
        set_wrap(not wrap)

    def toggle_theme(event):
        # Changing only the value keeps the same tree: the draft survives.
        set_theme("light" if theme == "dark" else "dark")

    return html.div(
        html.button({"onClick": toggle_wrap}, "toggle provider"),
        html.button({"onClick": toggle_theme}, "change value"),
        (ThemeContext(draft(), value=theme) if wrap else draft()),
    )


@component
def app():
    return html.div(html.h3("Provider identity"), panel())
1
# TODO

Verified sequence. Type hello: the readout shows draft='hello'. Press change value: draft='hello' ... theme=light, and the component's own render counter keeps counting, which is how you can tell it is the same instance — a changed value does not disturb the tree. Press toggle provider: the counter resets to renders=1 and draft=''. The leaf was unmounted and a fresh one mounted, so its state is not stale, it is new.

Flipping the conditional back does not restore it either — that is a third instance.

The rule: keep the provider in the tree and vary only its value. Toggling a theme off should be Theme(child, value="light"), not Theme(child, value=...) if dark else child.

Do not reach for key to fix this — it does not. Verified across four variants: a conditional provider loses the state, and adding a stable key to the child beneath it loses the state too. What keeps the state is having the provider element present in both branches, so the tree shape never changes.

The version that works moves the condition inside value, expressing "not provided" as the default rather than as an absent wrapper:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@component
def draft():
    # State owned by the leaf. It survives as long as this component keeps the
    # same position in the tree.
    text, set_text = use_state("")
    body = use_ref(0)
    body.current += 1
    return html.div(
        html.input(
            {
                "value": text,
                "onChange": lambda event: set_text(event["target"]["value"]),
                "placeholder": "type something",
            }
        ),
        html.p(
            {"id": "draft"},
            f"draft={text!r} renders={body.current} theme={use_context(ThemeContext)}",
        ),
    )


@component
def panel():
    provide, set_provide = use_state(True)
    theme, set_theme = use_state("dark")

    def toggle_wrap(event):
        set_provide(not provide)

    def toggle_theme(event):
        set_theme("light" if theme == "dark" else "dark")

    return html.div(
        html.button({"onClick": toggle_wrap}, "toggle provider"),
        html.button({"onClick": toggle_theme}, "change value"),
        # The provider is always in the tree, so nothing below it is unmounted.
        # "Not provided" is expressed as the default value, not as an absent
        # wrapper.
        ThemeContext(draft(), value=theme if provide else "light"),
    )


@component
def app():
    return html.div(html.h3("Provider always present"), panel())
1
# TODO

Type hello, press toggle provider: draft='hello' ... theme=light, and the render counter keeps counting. Same buttons, same user-visible behaviour as the broken version, state intact.

This is a special case of a rule that applies to all components, not just providers — see Preserving and Resetting State. Context makes it easier to hit by accident, because "wrap this in a provider" is a natural thing to put behind an if.

Before you use context

  • Does a component only forward the prop? If it uses the value, pass a prop. If it is one level deep, pass a prop. Context pays off when the value crosses several levels that do not care about it.
  • Is the value genuinely shared? A value used by one subtree is simpler as a prop, and easier to trace when it is wrong.
  • Are you hoping it saves renders? It does not. ReactPy has no render bailout, so when the provider re-renders, everything below it re-renders too, whether or not the value changed. Context is a plumbing feature, never a performance one — see What context does not do.
  • Do you want to change a value from below? Then context is the wrong shape. Put the state in a common ancestor and pass the setter (or a dispatch) down with it — see Scaling Up with Reducer and Context.

Recap

  • Context replaces props that components only forward. It does not replace props a component actually uses.
  • Three steps: create_context(default) once at module level, use_context(Context) in the component that needs it, Context(children, value=...) above that subtree.
  • The default from create_context is what an unprovided reader gets — not None. Verified.
  • The nearest provider wins; a nested provider shadows the outer one, and siblings outside a provider see the default. Verified across four readers.
  • Call use_context in the component that uses the value. Formatting it inside the parent's arguments reads the parent's context.
  • A component cannot read the context it provides — verified reading the default while its own child read the provided value. Read the underlying state instead.
  • Keep the provider element in the tree and vary only its value. Conditionally wrapping in a provider unmounts the subtree and discards its state; a key does not prevent it (verified).
  • Context is plumbing only: no render bailout, no reactivity, no shared state. See Use Context for the parameter-level detail and error messages.
Authors: Archmonger