Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol docs tutorials 02_component_deep_dive.md
Raw

docs/tutorials/02_component_deep_dive.md

# Component Deep Dive
---
id: component_deep_dive
title: Deep Dive into Raxol Components
difficulty: intermediate
estimated_time: 25
tags: [components, lifecycle, state, props]
prerequisites: [getting_started]
---
## Understanding Raxol's Component System
This tutorial provides an in-depth exploration of Raxol's component architecture, lifecycle, and best practices.
### Step 1: Component Lifecycle
---
step_id: component_lifecycle
title: Understanding Component Lifecycle
---
Raxol components follow a predictable lifecycle with hooks for initialization, updates, and cleanup.
#### Lifecycle Phases
1. **Initialization**: Component is created and mounted
2. **Rendering**: Component generates UI
3. **Updates**: State or props change trigger re-renders
4. **Cleanup**: Component is unmounted
#### Example Code
```elixir
defmodule LifecycleDemo do
use Raxol.Component
@impl true
def init(props) do
IO.puts("Component initializing with props: #{inspect(props)}")
# Setup resources
timer_ref = :timer.send_interval(1000, self(), :tick)
{:ok, %{
timer_ref: timer_ref,
counter: 0,
mounted_at: System.system_time(:second)
}}
end
@impl true
def update(state, prev_props, new_props) do
IO.puts("Props changed from #{inspect(prev_props)} to #{inspect(new_props)}")
# Update state based on prop changes
if new_props.reset && !prev_props.reset do
{:ok, %{state | counter: 0}}
else
{:ok, state}
end
end
@impl true
def render(state, props) do
uptime = System.system_time(:second) - state.mounted_at
Raxol.UI.box(title: props.title || "Lifecycle Demo") do
Raxol.UI.text("Counter: #{state.counter}")
Raxol.UI.text("Uptime: #{uptime}s")
Raxol.UI.text("Props: #{inspect(props)}")
end
end
@impl true
def handle_info(:tick, state) do
{:ok, %{state | counter: state.counter + 1}}
end
@impl true
def terminate(_reason, state) do
IO.puts("Component terminating")
:timer.cancel(state.timer_ref)
:ok
end
end
```
#### Exercise
Create a component that tracks its own lifecycle events and displays them in a log.
#### Hints
- Store lifecycle events in state as a list
- Add timestamps to each event
- Display events in reverse chronological order
### Step 2: State Management Patterns
---
step_id: state_management
title: State Management Patterns
---
Learn different patterns for managing component state effectively.
#### Local State vs. Shared State
- **Local State**: Component-specific data
- **Shared State**: Data shared between components
- **Derived State**: Computed from props or other state
#### Example Code
```elixir
defmodule StatePatterns do
use Raxol.Component
# Define state structure
defstruct [
:local_value,
:computed_value,
:history,
:undo_stack,
:redo_stack
]
def init(props) do
initial_state = %__MODULE__{
local_value: props.initial_value || "",
computed_value: nil,
history: [],
undo_stack: [],
redo_stack: []
}
{:ok, compute_derived_state(initial_state)}
end
def render(state, props) do
Raxol.UI.box do
# Local state input
Raxol.UI.text_input(
value: state.local_value,
on_change: {:update_value, :value}
)
# Computed state display
Raxol.UI.text("Computed: #{state.computed_value}")
# History tracking
Raxol.UI.text("History (#{length(state.history)} entries)")
# Undo/Redo controls
Raxol.UI.flex(direction: :horizontal) do
Raxol.UI.button(
"Undo",
on_click: :undo,
disabled: Enum.empty?(state.undo_stack)
)
Raxol.UI.button(
"Redo",
on_click: :redo,
disabled: Enum.empty?(state.redo_stack)
)
end
# Shared state from parent
if props.shared_data do
Raxol.UI.text("Shared: #{props.shared_data}")
end
end
end
def handle_event({:update_value, value}, state) do
# Save current state to undo stack
new_undo = [state.local_value | state.undo_stack]
new_state = %{state |
local_value: value,
history: [value | Enum.take(state.history, 9)],
undo_stack: new_undo,
redo_stack: [] # Clear redo on new change
}
{:ok, compute_derived_state(new_state)}
end
def handle_event(:undo, state) do
case state.undo_stack do
[] -> {:ok, state}
[prev | rest] ->
new_state = %{state |
local_value: prev,
undo_stack: rest,
redo_stack: [state.local_value | state.redo_stack]
}
{:ok, compute_derived_state(new_state)}
end
end
def handle_event(:redo, state) do
case state.redo_stack do
[] -> {:ok, state}
[next | rest] ->
new_state = %{state |
local_value: next,
undo_stack: [state.local_value | state.undo_stack],
redo_stack: rest
}
{:ok, compute_derived_state(new_state)}
end
end
defp compute_derived_state(state) do
# Compute derived values
computed = String.upcase(state.local_value)
%{state | computed_value: computed}
end
end
```
#### Exercise
Implement a form component with validation that maintains error state and history.
#### Hints
- Validate on change and on submit
- Store errors in state
- Keep history of valid submissions
### Step 3: Component Composition
---
step_id: component_composition
title: Component Composition
---
Learn how to build complex UIs by composing smaller components.
#### Composition Patterns
- **Container/Presentational**: Separate logic from presentation
- **Higher-Order Components**: Enhance components with additional functionality
- **Render Props**: Share code between components using a prop
#### Example Code
```elixir
# Presentational component
defmodule UserCard do
use Raxol.Component
def init(_props), do: {:ok, %{}}
def render(_state, props) do
Raxol.UI.box(border: :single, padding: 1) do
Raxol.UI.text(props.user.name, style: [bold: true])
Raxol.UI.text(props.user.email, style: [color: :gray])
if props.on_click do
Raxol.UI.button("Select", on_click: props.on_click)
end
end
end
end
# Container component
defmodule UserList do
use Raxol.Component
def init(_props) do
{:ok, %{
users: [],
selected_user: nil,
loading: true
}}
end
def mount(state) do
# Fetch users on mount
send(self(), :load_users)
{:ok, state}
end
def render(state, _props) do
Raxol.UI.box do
Raxol.UI.heading("Users")
cond do
state.loading ->
Raxol.UI.spinner(text: "Loading users...")
Enum.empty?(state.users) ->
Raxol.UI.text("No users found", style: [color: :gray])
true ->
Raxol.UI.scroll_view do
for user <- state.users do
UserCard.render(%{}, %{
user: user,
on_click: {:select_user, user.id}
})
end
end
end
if state.selected_user do
Raxol.UI.box(title: "Selected User") do
Raxol.UI.text("ID: #{state.selected_user.id}")
Raxol.UI.text("Name: #{state.selected_user.name}")
end
end
end
end
def handle_info(:load_users, state) do
# Simulate API call
Process.send_after(self(), {:users_loaded, fetch_users()}, 1000)
{:ok, state}
end
def handle_info({:users_loaded, users}, state) do
{:ok, %{state | users: users, loading: false}}
end
def handle_event({:select_user, user_id}, state) do
user = Enum.find(state.users, &(&1.id == user_id))
{:ok, %{state | selected_user: user}}
end
defp fetch_users do
[
%{id: 1, name: "Alice", email: "alice@example.com"},
%{id: 2, name: "Bob", email: "bob@example.com"},
%{id: 3, name: "Charlie", email: "charlie@example.com"}
]
end
end
# Higher-Order Component for adding loading state
defmodule WithLoading do
defmacro __using__(opts) do
quote do
use Raxol.Component
def render_with_loading(state, props, render_fn) do
if state.loading do
Raxol.UI.box do
Raxol.UI.spinner(text: unquote(opts[:message] || "Loading..."))
end
else
render_fn.(state, props)
end
end
end
end
end
```
#### Exercise
Create a reusable data table component that supports sorting, filtering, and pagination.
#### Hints
- Separate table logic from presentation
- Use composition for column definitions
- Make sorting and filtering pluggable
### Step 4: Performance Optimization
---
step_id: performance_optimization
title: Performance Optimization
---
Learn techniques to optimize component rendering and state updates.
#### Optimization Techniques
- **Memoization**: Cache computed values
- **Virtual Rendering**: Render only visible items
- **Batch Updates**: Group state changes
- **Lazy Loading**: Load components on demand
#### Example Code
```elixir
defmodule OptimizedList do
use Raxol.Component
@viewport_size 20
def init(props) do
{:ok, %{
items: props.items || [],
filtered_items: nil,
filter: "",
scroll_offset: 0,
memo_cache: %{}
}}
end
def render(state, _props) do
# Use memoized filtered items
items = get_filtered_items(state)
# Virtual scrolling - only render visible items
visible_items = get_visible_items(items, state.scroll_offset)
Raxol.UI.box do
# Filter input
Raxol.UI.text_input(
value: state.filter,
placeholder: "Filter items...",
on_change: {:update_filter, :value}
)
# Item count
Raxol.UI.text("Showing #{length(visible_items)} of #{length(items)} items")
# Virtual list
Raxol.UI.virtual_list(
height: @viewport_size,
item_count: length(items),
scroll_offset: state.scroll_offset,
on_scroll: {:update_scroll, :offset}
) do
for item <- visible_items do
render_item(item, state)
end
end
end
end
# Memoized filtering
defp get_filtered_items(state) do
cache_key = {state.filter, hash(state.items)}
case Map.get(state.memo_cache, cache_key) do
nil ->
filtered = filter_items(state.items, state.filter)
# Store in cache (would need to update state properly)
filtered
cached ->
cached
end
end
defp filter_items(items, "") do
items
end
defp filter_items(items, filter) do
pattern = String.downcase(filter)
Enum.filter(items, fn item ->
String.contains?(
String.downcase(item.text),
pattern
)
end)
end
# Virtual scrolling
defp get_visible_items(items, offset) do
items
|> Enum.drop(offset)
|> Enum.take(@viewport_size)
end
# Optimized item rendering with keys
defp render_item(item, _state) do
Raxol.UI.box(key: item.id, padding: 0) do
Raxol.UI.text(item.text)
end
end
# Batch state updates
def handle_event({:update_filter, value}, state) do
# Debounce filter updates
Process.cancel_timer(state[:filter_timer])
timer = Process.send_after(self(), {:apply_filter, value}, 300)
{:ok, %{state | filter: value, filter_timer: timer}}
end
def handle_info({:apply_filter, value}, state) do
# Apply filter after debounce
filtered = filter_items(state.items, value)
new_cache = Map.put(
state.memo_cache,
{value, hash(state.items)},
filtered
)
{:ok, %{state |
filtered_items: filtered,
memo_cache: new_cache,
scroll_offset: 0
}}
end
defp hash(items) do
:erlang.phash2(items)
end
end
```
#### Exercise
Build a virtualized grid component that efficiently renders thousands of cells.
#### Hints
- Calculate visible viewport based on scroll position
- Use cell recycling for better performance
- Implement smooth scrolling with momentum
### Step 5: Custom Hooks
---
step_id: custom_hooks
title: Creating Custom Hooks
---
Learn how to create reusable logic with custom hooks.
#### Custom Hook Patterns
- **State Hooks**: Reusable state logic
- **Effect Hooks**: Side effects management
- **Ref Hooks**: DOM/Terminal references
- **Context Hooks**: Shared context access
#### Example Code
```elixir
defmodule Hooks do
@moduledoc """
Collection of reusable hooks for Raxol components.
"""
# useLocalStorage hook
defmodule UseLocalStorage do
def init(key, default_value) do
stored = read_storage(key)
value = stored || default_value
%{
value: value,
key: key,
set_value: fn new_value ->
write_storage(key, new_value)
new_value
end
}
end
defp read_storage(key) do
case File.read(".raxol_storage/#{key}") do
{:ok, content} -> :erlang.binary_to_term(content)
_ -> nil
end
end
defp write_storage(key, value) do
File.mkdir_p!(".raxol_storage")
File.write!(".raxol_storage/#{key}", :erlang.term_to_binary(value))
end
end
# useDebounce hook
defmodule UseDebounce do
def init(value, delay) do
%{
value: value,
debounced_value: value,
delay: delay,
timer: nil
}
end
def update(state, new_value) do
if state.timer do
Process.cancel_timer(state.timer)
end
timer = Process.send_after(self(), {:debounce_complete, new_value}, state.delay)
%{state | value: new_value, timer: timer}
end
def handle_info({:debounce_complete, value}, state) do
%{state | debounced_value: value, timer: nil}
end
end
# useKeyboard hook
defmodule UseKeyboard do
def init(key_map) do
%{
key_map: key_map,
pressed_keys: MapSet.new()
}
end
def handle_key_down(state, key) do
new_pressed = MapSet.put(state.pressed_keys, key)
# Check for key combinations
action = find_matching_action(new_pressed, state.key_map)
{%{state | pressed_keys: new_pressed}, action}
end
def handle_key_up(state, key) do
new_pressed = MapSet.delete(state.pressed_keys, key)
{%{state | pressed_keys: new_pressed}, nil}
end
defp find_matching_action(pressed_keys, key_map) do
Enum.find_value(key_map, fn {keys, action} ->
if MapSet.subset?(MapSet.new(keys), pressed_keys) do
action
end
end)
end
end
end
# Component using custom hooks
defmodule ComponentWithHooks do
use Raxol.Component
def init(_props) do
storage = Hooks.UseLocalStorage.init("user_prefs", %{theme: "dark"})
debounce = Hooks.UseDebounce.init("", 500)
keyboard = Hooks.UseKeyboard.init(%{
[:ctrl, :s] => :save,
[:ctrl, :z] => :undo,
[:ctrl, :shift, :z] => :redo
})
{:ok, %{
storage: storage,
debounce: debounce,
keyboard: keyboard,
content: ""
}}
end
def render(state, _props) do
Raxol.UI.box do
Raxol.UI.text("Theme: #{state.storage.value.theme}")
Raxol.UI.text_input(
value: state.debounce.value,
on_change: {:update_search, :value}
)
Raxol.UI.text("Debounced: #{state.debounce.debounced_value}")
Raxol.UI.text_area(
value: state.content,
on_key_down: {:handle_shortcut, :key}
)
end
end
def handle_event({:update_search, value}, state) do
new_debounce = Hooks.UseDebounce.update(state.debounce, value)
{:ok, %{state | debounce: new_debounce}}
end
def handle_event({:handle_shortcut, key}, state) do
{new_keyboard, action} = Hooks.UseKeyboard.handle_key_down(state.keyboard, key)
state = %{state | keyboard: new_keyboard}
case action do
:save -> save_content(state)
:undo -> undo_change(state)
:redo -> redo_change(state)
_ -> {:ok, state}
end
end
defp save_content(state) do
# Save logic
{:ok, state}
end
defp undo_change(state) do
# Undo logic
{:ok, state}
end
defp redo_change(state) do
# Redo logic
{:ok, state}
end
end
```
#### Exercise
Create a custom hook for managing form state with validation.
#### Hints
- Track field values and errors
- Validate on change or on submit
- Provide helper functions for common operations
### Congratulations!
You've mastered Raxol's component system! You now understand:
- ✓ Component lifecycle and hooks
- ✓ State management patterns
- ✓ Component composition
- ✓ Performance optimization
- ✓ Custom hooks
## Next Steps
- Explore [Terminal Emulation](terminal_emulation.md)
- Learn about [Advanced Input Handling](advanced_input.md)
- Build [Production Applications](production_apps.md)