Context provides a way to pass data through the component tree without having to pass props down manually at every level. It’s useful for “global” data like themes, user authentication, or localization.
Consider an app where many nested components need access to the current user:
;; Without context: passing user through every level (prop drilling)
(vui-defcomponent app ()
:state ((user '(:name "Alice" :role "admin")))
:render
(vui-component 'layout :user user))
(vui-defcomponent layout (user)
:render
(vui-component 'sidebar :user user))
(vui-defcomponent sidebar (user)
:render
(vui-component 'user-info :user user))
(vui-defcomponent user-info (user)
:render
(vui-text (format "Logged in as: %s" (plist-get user :name))))This is tedious and makes components harder to reuse. Context solves this.
(vui-defcontext user nil
"The currently logged-in user.")This creates:
- A context variable named
user-context - A provider component
user-provider - A hook
use-userto consume the context
(vui-defcontext name default-value &optional docstring)Wrap your component tree with the provider:
(vui-defcomponent app ()
:state ((user '(:name "Alice" :role "admin")))
:render
(user-provider user ; Provide the context value
(vui-component 'layout)))Everything inside the provider can access the context.
Use the auto-generated hook to access context:
(vui-defcomponent user-info ()
:render
(let ((user (use-user))) ; Get context value
(vui-text (format "Logged in as: %s"
(plist-get user :name)))))No more prop drilling! The component gets the user directly from context.
;; 1. Define the context
(vui-defcontext theme 'light
"Current UI theme (light or dark).")
;; 2. Create components that use it
(vui-defcomponent themed-text (content)
:render
(let ((theme (use-theme)))
(vui-text content
:face (if (eq theme 'dark)
'(:foreground "white" :background "black")
'(:foreground "black" :background "white")))))
(vui-defcomponent theme-toggle ()
:render
(let ((theme (use-theme)))
(vui-button (format "Theme: %s" theme)
:on-click (lambda ()
;; Note: This won't work! See next section
(message "Can't change context from here")))))
;; 3. Provide the context
(vui-defcomponent app ()
:state ((current-theme 'light))
:render
(theme-provider current-theme
(vui-fragment
(vui-component 'themed-text :content "Hello!")
(vui-newline)
(vui-button "Toggle theme"
:on-click (lambda ()
(vui-set-state
:current-theme
(if (eq current-theme 'light)
'dark
'light)))))))Context values are read-only from consumers. To update context, you have two options:
(vui-defcontext user nil)
(vui-defcomponent app ()
:state ((user '(:name "Alice")))
:render
(let ((context-value (list :user user
:set-user (lambda (new-user)
(vui-set-state :user new-user)))))
(user-provider context-value
(vui-component 'user-editor))))
(vui-defcomponent user-editor ()
:render
(let* ((ctx (use-user))
(user (plist-get ctx :user))
(set-user (plist-get ctx :set-user)))
(vui-fragment
(vui-text (format "Name: %s" (plist-get user :name)))
(vui-newline)
(vui-button "Change name"
:on-click (lambda ()
(funcall set-user '(:name "Bob")))))));; Theme context with setter pattern
(vui-defcontext theme-context nil)
(vui-defcomponent theme-provider-wrapper ()
:state ((theme 'light))
:render
(theme-context-provider
(list :theme theme
:toggle (lambda ()
(vui-set-state :theme
(if (eq theme 'light) 'dark 'light))))
children))
(vui-defcomponent theme-consumer ()
:render
(let* ((ctx (use-theme-context))
(theme (plist-get ctx :theme))
(toggle (plist-get ctx :toggle)))
(vui-button (format "Theme: %s (click to toggle)" theme)
:on-click toggle)))Providers can be nested - inner providers override outer ones:
(vui-defcontext level 0)
(vui-defcomponent nested-example ()
:render
(level-provider 1
(vui-fragment
(vui-component 'show-level) ; Shows 1
(vui-newline)
(level-provider 2
(vui-component 'show-level))))) ; Shows 2
(vui-defcomponent show-level ()
:render
(vui-text (format "Level: %d" (use-level))))You can define and use multiple contexts:
(vui-defcontext user nil)
(vui-defcontext theme 'light)
(vui-defcontext locale "en")
(vui-defcomponent app ()
:state ((user '(:name "Alice"))
(theme 'dark)
(locale "fr"))
:render
(user-provider user
(theme-provider theme
(locale-provider locale
(vui-component 'main-view)))))
(vui-defcomponent main-view ()
:render
(let ((user (use-user))
(theme (use-theme))
(locale (use-locale)))
(vui-text (format "User: %s, Theme: %s, Locale: %s"
(plist-get user :name)
theme
locale))))When no provider is found, context returns the default value:
(vui-defcontext optional-data "default-value")
(vui-defcomponent uses-optional ()
:render
(let ((data (use-optional-data)))
;; Returns "default-value" if no provider above
(vui-text (format "Data: %s" data))))- Theme (dark/light mode)
- Current user/authentication
- Locale/internationalization
- Feature flags
- UI configuration (font size, density)
- Data that changes frequently (use props or state instead)
- Data only needed by one component (just use props)
- Complex state management (consider other patterns)
| API | Purpose |
|---|---|
vui-defcontext | Define a new context |
NAME-provider | Provide context value to tree |
use-NAME | Consume context value |
Exercise: Create a notification system using context:
- Define a
notificationscontext- Create a
notification-providerthat manages a list of notifications- Provide
add-notificationandclear-notificationsfunctions- Create a
notification-listcomponent that displays notifications- Create an
add-notification-buttoncomponent
(vui-defcontext notifications nil)
(vui-defcomponent notification-app ()
:state ((notifications '()))
:render
;; Your implementation
)