Reactivity & Context

Coralite provides a fine-grained, signal-backed reactivity system combined with tree-scoped dependency injection. This guide covers how component state updates, synchronous and asynchronous derived getters, side-effect observation, DOM render synchronization, and cross-component context injection operate across the framework.

1. State #

The state object in Coralite is a reactive Proxy backed by framework signals. When state properties are mutated, Coralite automatically tracks dirty dependencies and schedules a DOM update.

Synchronous state writes are batched into a single microtask flush. Mutating multiple state keys within the same event handler or synchronous function causes a single re-render pass, preventing unnecessary layout recalculations or intermediate state renders.

html
Code copied!
  <template id="counter-btn">
    <button ref="btn">Clicked  times</button>
  </template>
  
  <script type="module">
    import { defineComponent } from 'coralite'
  
    export default defineComponent({
      attributes: {
        count: {
          type: Number,
          default: 0
        }
      },
      client: ({ state, refs, signal }) => {
        const button = refs('btn')
  
        button.addEventListener('click', () => {
          // Mutating reactive state schedules a batched DOM flush
          state.count++
        }, { signal })
      }
    })
  </script>

2. Getters #

Getters compute derived state synchronously from reactive state values. A getter function receives a context parameter (which can be destructured as ({ state }) => ... or accessed as a whole object (context) => context.state...) and must be pure and synchronous, returning a calculated value without causing side effects or mutating state.

In component templates, getters are referenced using standard flat template tokens (e.g. ).

javascript
Code copied!
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
      price: { type: Number, default: 0 },
      taxRate: { type: Number, default: 0.1 }
    },
    getters: {
      // Pure synchronous derived state function with destructured context
      totalPrice: ({ state }) => (state.price * (1 + state.taxRate)).toFixed(2),
      isDiscounted: ({ state }) => state.price > 100
    }
  })
No Side Effects in Getters

Getters are executed automatically during reactive evaluations. Do not perform network fetches, DOM manipulations, or state mutations inside getters.

3. Async Getters #

Async getters allow computing derived state using asynchronous operations (such as API calls or async dynamic computations). Async getters execute strictly in the client runtime and are skipped on the server during SSR.

The Sync Prologue Rule: Reactive dependency tracking relies on synchronous call stacks. In an async getter, you must read all required state dependencies synchronously before the first await statement. Any state reads occurring after an await statement will not be registered as active dependencies.

Because async getters are skipped on the server, you should supply a sibling synchronous getter or server property as a fallback value for initial server-rendered output.

javascript
Code copied!
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
      userId: String
    },
    server: () => ({
      // Server-rendered initial value
      userProfile: { name: 'Loading...' }
    }),
    getters: {
      // Fallback sync getter used during SSR and initial render
      formattedName: ({ state }) => state.userProfile?.name ?? 'Anonymous',
  
      // Client-only async getter
      async userDetails ({ state }) {
        // SYNC PROLOGUE: Read reactive dependencies BEFORE any await
        const currentId = state.userId
        if (!currentId) return null
  
        // Async operation after dependencies are recorded
        const res = await fetch(`/api/users/${currentId}`)
        return await res.json()
      }
    }
  })

4. observe(key, callback) #

The observe function in the client() block registers a reactive subscriber for a specific state key. When that key's value changes during state updates, the callback is invoked with (newValue, oldValue).

Use observe exclusively for external side effects, such as updating third-party charts, syncing state to localStorage, or logging metrics. Never mutate component state directly inside an observe callback, as doing so can trigger recursive reactive loops or unexpected cascade cycles.

javascript
Code copied!
  client: ({ state, observe, signal }) => {
    // Sync state changes to localStorage or external services
    observe('theme', (newTheme, oldTheme) => {
      localStorage.setItem('app-theme', newTheme)
      document.documentElement.setAttribute('data-theme', newTheme)
    })
  }

5. updateComplete #

updateComplete is a Promise exposed in the client() context object that resolves when the current microtask DOM update batch and observer flush complete.

Do not await updateComplete at the top of the client() function. When client() executes during hydration, the initial DOM is already rendered and ready for binding. Await updateComplete only after mutating state when you need to measure or interact with the updated DOM immediately afterward.

javascript
Code copied!
  client: async ({ state, refs, updateComplete, signal }) => {
    const container = refs('box')
  
    refs('expandBtn').addEventListener('click', async () => {
      // 1. Mutate state
      state.expanded = true
  
      // 2. Await DOM flush to guarantee updated layout
      await updateComplete
  
      // 3. Measure DOM element after render
      const height = container.getBoundingClientRect().height
      console.log('New container height:', height)
    }, { signal })
  }

6. Context (provide / consume) #

Coralite provides a tree-scoped dependency injection mechanism via provide and consume declarations. This allows ancestor components (providers) to pass data down to any deeply nested descendant components (consumers) without explicit prop drilling.

Context keys are generated using createContext('keyName'), which returns a uniquely identified string symbol for context registration.

Defining Context Key & Provider #

In a provider component, define provide using computed context functions. Context functions receive destructured parameters (such as ({ state }) => ...):

javascript
Code copied!
  import { defineComponent, createContext } from 'coralite'
  
  // Export shared context key
  export const ThemeContext = createContext('app:theme')
  
  export default defineComponent({
    attributes: {
      theme: { type: String, default: 'light' }
    },
    provide: {
      [ThemeContext]: ({ state }) => ({
        current: state.theme,
        isDark: state.theme === 'dark'
      })
    }
  })

Consuming Context #

In descendant consumer components, declare consume to map provider keys into local state properties:

javascript
Code copied!
  import { defineComponent } from 'coralite'
  import { ThemeContext } from './theme-provider.js'
  
  export default defineComponent({
    consume: {
      theme: ThemeContext
    },
    getters: {
      themeClass: ({ state }) => state.theme?.isDark ? 'dark-mode' : 'light-mode'
    }
  })

Context holds a single value. When passing multiple values or controls down the tree, group related properties into a single object in the provide function.


Next: Explore Styling & Slots to learn about Light DOM scoping, custom properties, and slot projections.

Start Building with Coralite!

Use the scaffolding script to get jump started into your next project with Coralite

Copied commandline!