Lifecycle hooks let you run code at specific points in a component’s life: when it mounts, updates, or unmounts.
| Hook | When It Runs |
|---|---|
:on-mount | After first render |
:on-update | After re-renders (not first) |
:on-unmount | Before component is removed |
Runs once after the component is first rendered:
(vui-defcomponent my-component ()
:on-mount
(message "Component mounted!")
:render
(vui-text "Hello"))- Fetching initial data
- Setting up subscriptions
- Logging/analytics
If :on-mount returns a function, it will be called during unmount:
(vui-defcomponent with-mount-cleanup ()
:on-mount
(progn
(message "Setting up...")
;; Return cleanup function
(lambda ()
(message "Cleaning up from mount!")))
:render
(vui-text "Hello"))This is useful when setup and cleanup are closely related.
Runs after every re-render (but not the first render):
(vui-defcomponent tracking-updates ()
:state ((count 0))
:on-update
(message "Component updated! Count is now: %d" count)
:render
(vui-button (format "Count: %d" count)
:on-click (lambda ()
(vui-set-state :count (1+ count)))))The update hook receives current and previous values:
(vui-defcomponent tracking-changes (value)
:state ((internal 0))
:on-update
(message "Props: %s -> %s, State: %s -> %s"
prev-props props
prev-state state)
:render
(vui-text (format "%s - %d" value internal)))The available variables are:
props- Current propsstate- Current stateprev-props- Previous propsprev-state- Previous state
(vui-defcomponent smart-updater (data)
:on-update
(let ((old-data (plist-get prev-props :data))
(new-data (plist-get props :data)))
(unless (equal old-data new-data)
(message "Data actually changed!")))
:render
(vui-text (format "Data: %s" data)))Runs before the component is removed from the tree:
(vui-defcomponent cleanup-example ()
:on-unmount
(message "Cleaning up!")
:render
(vui-text "I'm here for now"))- Canceling timers
- Removing event listeners
- Saving draft data
- Cleanup of external resources
(vui-defcomponent auto-refresh ()
:state ((data nil))
:on-mount
;; Return cleanup function that cancels the timer
(let ((timer (run-with-timer 5 5
(vui-with-async-context
(fetch-and-update-data)))))
(lambda ()
(cancel-timer timer)))
:render
(vui-text (format "Data: %s" data)))When components nest, hooks run in specific orders.
Children mount before parents. Given this tree:
(vui-defcomponent parent ()
:on-mount (message "Parent mounted")
:render
(vui-vstack
(vui-component 'child :name "A")
(vui-component 'child :name "B")))
(vui-defcomponent child (name)
:on-mount (message "Child %s mounted" name)
:render (vui-text name))The console shows:
Child A mounted Child B mounted Parent mounted
This ensures a parent’s :on-mount can safely interact with already-mounted children.
Similarly, children update before parents:
Child A updated Child B updated Parent updated
Children unmount before parents:
Child A unmounting Child B unmounting Parent unmounting
Both can achieve similar goals. Here’s when to use each:
| Scenario | Use |
|---|---|
| Simple setup on mount | :on-mount |
| Setup that depends on props/state | vui-vui-use-effect |
| Simple cleanup on unmount | :on-unmount |
| Cleanup tied to specific values | vui-vui-use-effect |
| Compare previous/current values | :on-update |
| React to specific dep changes | vui-vui-use-effect |
;; Simpler, declarative
(vui-defcomponent with-lifecycle ()
:on-mount (message "mounted")
:on-unmount (message "unmounting")
:render ...);; More flexible, dependency-aware
(vui-defcomponent with-effect ()
:render
(progn
(vui-use-effect ()
(message "mounted")
(lambda () (message "unmounting")))
...))When lifecycle hooks start async operations (timers, processes, fetch callbacks), you need to use vui-with-async-context or vui-async-callback to restore the component context when the callback runs.
(vui-defcomponent polling-component ()
:state ((data nil))
:on-mount
(let ((timer (run-with-timer 10 10
(vui-with-async-context
;; Use functional update to avoid stale closure
(vui-set-state :data (fetch-latest-data))))))
;; Return cleanup
(lambda () (cancel-timer timer)))
:render
(vui-text (or data "Loading...")))(vui-defcomponent data-loader (item-id)
:state ((data nil) (loading t))
:on-mount
(fetch-item item-id
(vui-async-callback (result)
(vui-set-state :data result)
(vui-set-state :loading nil)))
:render
(if loading
(vui-text "Loading...")
(vui-text (format "Data: %s" data))))Lifecycle hooks are wrapped in error handling. If an error occurs:
- The error is caught
vui-last-erroris set with details- The error handler (
vui-lifecycle-error-handler) is called - The component tree continues to function
See Error Handling for details.
(vui-defcomponent document-editor (doc-id)
:state ((content nil)
(loading t)
(dirty nil))
:on-mount
(progn
;; Fetch document - use vui-async-callback for the response
(fetch-document doc-id
(vui-async-callback (data)
(vui-set-state :content data)
(vui-set-state :loading nil)))
;; Auto-save timer - use vui-with-async-context
;; Return cleanup function to cancel timer
(let ((save-timer
(run-with-timer 30 30
(vui-with-async-context
;; Note: 'dirty' and 'content' would be stale here
;; In real code, you'd use a ref or functional update
(save-if-dirty doc-id)))))
(lambda ()
(cancel-timer save-timer))))
:on-update
(let ((old-doc-id (plist-get prev-props :doc-id)))
;; Fetch new doc if ID changed
(unless (equal old-doc-id doc-id)
(vui-set-state :loading t)
(fetch-document doc-id
(vui-async-callback (data)
(vui-set-state :content data)
(vui-set-state :loading nil)))))
:on-unmount
;; Save unsaved changes synchronously before unmount
(when dirty
(save-document doc-id content))
:render
(if loading
(vui-text "Loading...")
(vui-fragment
(vui-text (format "Editing: %s%s" doc-id (if dirty " *" "")))
(vui-newline)
(vui-field :value content
:on-change (lambda (v)
(vui-set-state :content v)
(vui-set-state :dirty t))))))Exercise: Create a
stopwatchcomponent that:
- Displays elapsed time in seconds
- Starts counting on mount
- Stops and shows final time on unmount
- Uses
:on-updateto log each second
Hint: Use vui-with-async-context for the timer callback and return a cleanup
function from :on-mount.
- Error Handling - Error boundaries and handlers
- Hooks - vui-use-effect for more complex scenarios