Visualizing CompositionLocal in the Composition Tree

Published: (February 5, 2026 at 12:11 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Visualizing Data Flow Through the Compose Tree

When Compose runs your composables, it builds an internal tree structure:

Your Code:                          Composition Tree:
─────────────                       ─────────────────
@Composable                         
fun App() {                              [App]
    Screen()          ───────►            │
}                                      [Screen]

@Composable                            [Card]
fun Screen() {                            │
    Card()                             [Text]
}

Locals Map on Each Node

Each node can have an optional locals map attached.

Without CompositionLocalProvider

─────────────────────────────────
     [App]                    ← locals: { }

    [Screen]                  ← locals: { }

     [Card]                   ← locals: { }

     [Text]                   ← locals: { }

With CompositionLocalProvider

──────────────────────────────
     [App]                    ← locals: { }

  ┌────────────────────────────────────────────┐
  │ CompositionLocalProvider(LocalTheme → Dark)│
  └────────────────────────────────────────────┘

    [Screen]                  ← locals: { LocalTheme → Dark }  ✓ ATTACHED HERE

     [Card]                   ← locals: { }  (inherits from parent)

     [Text]                   ← locals: { }  (inherits from parent)

When you call LocalTheme.current, Compose walks up the tree until it finds a provider:

@Composable
fun Text() {
    val theme = LocalTheme.current  // How does this work?
}

Lookup Walk‑through

LOOKUP: LocalTheme.current from [Card]
──────────────────────────────────────

     [App]          ← 4. Still not found? Use default or throw

    [Screen]        ← 3. FOUND! LocalTheme → Dark ✓ RETURN THIS
       │                  (Provider attached here)
     [Card]         ← 2. Not here, look at parent...

     [Text]         ← 1. Start here: "I need LocalTheme"

Shadowing Example

@Composable
fun App() {
    CompositionLocalProvider(LocalTheme provides Dark) {
        Screen()
        CompositionLocalProvider(LocalTheme provides Light) {
            AnotherScreen()
        }
    }
}
TREE WITH SHADOWING:
────────────────────

                    [App]

    ┌─────────────────────────────────────┐
    │  Provider(LocalTheme → Dark)        │
    └─────────────────────────────────────┘

         ┌────────────┴────────────┐
         │                         │
     [Screen]                 ┌─────────────────────────────┐
         │                    │ Provider(LocalTheme → Light)│ ← SHADOWS parent
     [Card]                   └─────────────────────────────┘
         │                              │
     [Text]                      [AnotherScreen]

    LocalTheme.current               [Card]
         = Dark                         │
                                    [Text]

                                 LocalTheme.current
                                      = Light ← SHADOWED VALUE

The Slot Table (Simplified)

Think of the slot table as a flat array that mirrors the tree:

IndexNodeLocals MapParent Index
0App{ }null
1Provider{ LocalTheme → Dark }0
2Screen{ }1
3Card{ }2
4Text{ }3
5Provider{ LocalTheme → Light }1
6AnotherScr{ }5
7Card{ }6
8Text{ }7

The slot table is a deep topic—don’t feel pressured to master it right away.

Lookup Algorithm (pseudo‑code)

function lookup(key, nodeIndex):
    current = nodeIndex
    while (current != null):
        if (current.locals.contains(key)):
            return current.locals[key]
        current = current.parentIndex
    return key.defaultValue   // or throw if no default

Providing Multiple Locals

CompositionLocalProvider(
    LocalTheme provides Dark,
    LocalUser provides currentUser,
    LocalAnalytics provides analyticsService
) {
    Content()
}
                    [App]

    ┌─────────────────────────────────────────────────────┐
    │  Provider                                            │
    │  locals: {                                           │
    │      LocalTheme     → Dark                           │
    │      LocalUser      → User(name="John")              │
    │      LocalAnalytics → AnalyticsServiceImpl           │
    │  }                                                   │
    └─────────────────────────────────────────────────────┘

                  [Content]

              ┌───────┴───────┐
              │               │
          [Header]        [Body]
              │               │
          [Avatar]        [Posts]

   LocalUser.current   → User("John")
   LocalTheme.current  → Dark

When a Value Changes (compositionLocalOf)

Provider(LocalCounter → 1)  ──changes to──►  Provider(LocalCounter → 2)
           │                                            │
       [Screen]  ← NOT recomposed                   [Screen]
           │       (doesn't read it)                    │
       [Counter] ← RECOMPOSED! ✓                   [Counter] ← reads LocalCounter
           │       (reads LocalCounter.current)        │
       [Label]   ← NOT recomposed                  [Label]
               (doesn't read it)

Only nodes that actually READ the value get recomposed!

Summary Diagram

┌──────────────────────────────────────────────────────────────┐
│                     COMPOSITION TREE                         │
└──────────────────────────────────────────────────────────────┘

│   ┌─────┐                                                    │
│   │Node │ ◄─── Each node can have a "locals" attachment      │
│   └──┬──┘                                                    │
│      │      ┌──────────────────────┐                         │
│      │      │ locals: {            │                         │
│      │      │   Key1 → Value1      │ ◄── Provider attaches   │
│      │      │   Key2 → Value2      │     key‑value pairs     │
│      │      │ }                    │                         │
│      │      └──────────────────────┘                         │
│   ┌──┴──┐                                                    │
│   │Child│ ◄─── Children inherit (lookup walks up the tree)   │
│   └─────┘                                                    │
│                                                              │
│   KEY INSIGHT: Data is NOT copied down.                      │
│   It's attached at one node, and lookups walk UP to find it. │
└──────────────────────────────────────────────────────────────┘
Back to Blog

Related posts

Read more »