Skip to content

Latest commit

 

History

History
351 lines (267 loc) · 8.29 KB

File metadata and controls

351 lines (267 loc) · 8.29 KB

Lifecycle Hooks

Lifecycle hooks let you run code at specific points in a component’s life: when it mounts, updates, or unmounts.

1 Overview

HookWhen It Runs
:on-mountAfter first render
:on-updateAfter re-renders (not first)
:on-unmountBefore component is removed

2 Mount - :on-mount

Runs once after the component is first rendered:

(vui-defcomponent my-component ()
  :on-mount
  (message "Component mounted!")

  :render
  (vui-text "Hello"))

2.1 Use Cases

  • Fetching initial data
  • Setting up subscriptions
  • Logging/analytics

2.2 Cleanup Return Value

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.

3 Update - :on-update

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)))))

3.1 Arguments

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 props
  • state - Current state
  • prev-props - Previous props
  • prev-state - Previous state

3.2 Comparing Changes

(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)))

4 Unmount - :on-unmount

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"))

4.1 Use Cases

  • Canceling timers
  • Removing event listeners
  • Saving draft data
  • Cleanup of external resources

4.2 Example: Timer Cleanup

(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)))

5 Execution Order

When components nest, hooks run in specific orders.

5.1 Mount Order (Bottom-Up)

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.

5.2 Update Order (Bottom-Up)

Similarly, children update before parents:

Child A updated
Child B updated
Parent updated

5.3 Unmount Order (Bottom-Up)

Children unmount before parents:

Child A unmounting
Child B unmounting
Parent unmounting

6 Lifecycle vs vui-use-effect

Both can achieve similar goals. Here’s when to use each:

ScenarioUse
Simple setup on mount:on-mount
Setup that depends on props/statevui-vui-use-effect
Simple cleanup on unmount:on-unmount
Cleanup tied to specific valuesvui-vui-use-effect
Compare previous/current values:on-update
React to specific dep changesvui-vui-use-effect

6.1 Lifecycle Hooks

;; Simpler, declarative
(vui-defcomponent with-lifecycle ()
  :on-mount (message "mounted")
  :on-unmount (message "unmounting")
  :render ...)

6.2 vui-use-effect

;; More flexible, dependency-aware
(vui-defcomponent with-effect ()
  :render
  (progn
    (vui-use-effect ()
      (message "mounted")
      (lambda () (message "unmounting")))
    ...))

7 Async Operations in Lifecycle

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.

7.1 Timer in on-mount

(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...")))

7.2 Fetch with Callback

(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))))

8 Error Handling in Lifecycle

Lifecycle hooks are wrapped in error handling. If an error occurs:

  1. The error is caught
  2. vui-last-error is set with details
  3. The error handler (vui-lifecycle-error-handler) is called
  4. The component tree continues to function

See Error Handling for details.

9 Complete Example

(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))))))

10 Try It Yourself

Exercise: Create a stopwatch component that:

  1. Displays elapsed time in seconds
  2. Starts counting on mount
  3. Stops and shows final time on unmount
  4. Uses :on-update to log each second

Hint: Use vui-with-async-context for the timer callback and return a cleanup function from :on-mount.

11 What’s Next?

  • Error Handling - Error boundaries and handlers
  • Hooks - vui-use-effect for more complex scenarios