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.
<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. ).
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
}
})
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.
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.
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.
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 }) => ...):
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:
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.