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 lib raxol ui components patterns higher_order.ex
Raw

lib/raxol/ui/components/patterns/higher_order.ex

defmodule Raxol.UI.Components.Patterns.HigherOrder do
@moduledoc """
Higher-Order Components (HOCs) for Raxol UI.
Higher-Order Components are a pattern for reusing component logic. A HOC is a function
that takes a component and returns a new component with enhanced functionality.
## Common Use Cases
- Authentication/Authorization
- Loading states
- Error boundaries
- Data fetching
- Styling/Theming
- Logging/Analytics
- Performance optimization
## Usage
# Create a HOC that adds loading state
with_loading = HigherOrder.with_loading_state()
# Enhance a component
enhanced_user_list = with_loading.(UserList)
# Use the enhanced component
%{
type: enhanced_user_list,
attrs: %{users: users, loading: loading}
}
# Chain multiple HOCs
super_enhanced = HigherOrder.chain([
HigherOrder.with_loading_state(),
HigherOrder.with_error_boundary(),
HigherOrder.with_analytics("user_list_component")
]).(UserList)
"""
alias Raxol.Core.Runtime.Log
alias Raxol.UI.State.{Context, Hooks}
@doc """
Creates a HOC that adds loading state management.
## Enhanced Props
- `:loading` - Boolean indicating loading state
- `:set_loading` - Function to update loading state
## Examples
with_loading = HigherOrder.with_loading_state()
LoadingUserList = with_loading.(UserList)
# The enhanced component will have access to loading state
"""
def with_loading_state(options \\ []) do
initial_loading = Keyword.get(options, :initial_loading, false)
loading_component =
Keyword.get(options, :loading_component, &default_loading_component/1)
fn component_module ->
fn props, context ->
# Add loading state using hooks
{loading, set_loading} = Hooks.use_state(initial_loading)
# Enhance props with loading utilities
enhanced_props =
Map.merge(props, %{
loading: loading,
set_loading: set_loading
})
# Show loading component if loading
render_with_loading_state(
loading,
loading_component,
enhanced_props,
component_module,
context
)
end
end
end
@doc """
Creates a HOC that adds error boundary functionality.
## Enhanced Props
- `:error` - Current error (nil if no error)
- `:clear_error` - Function to clear the error
- `:on_error` - Function to report errors
## Examples
with_error_boundary = HigherOrder.with_error_boundary()
SafeComponent = with_error_boundary.(RiskyComponent)
"""
def with_error_boundary(options \\ []) do
error_component =
Keyword.get(options, :error_component, &default_error_component/1)
on_error_callback = Keyword.get(options, :on_error)
fn component_module ->
fn props, context ->
{error, set_error} = Hooks.use_state(nil)
clear_error =
Hooks.use_callback(
make_clear_error_fn(set_error),
[set_error]
)
report_error =
Hooks.use_callback(
make_report_error_fn(set_error, on_error_callback),
[set_error, on_error_callback]
)
enhanced_props =
Map.merge(props, %{
error: error,
clear_error: clear_error,
on_error: report_error
})
render_with_error_boundary(
error,
error_component,
enhanced_props,
component_module,
context,
props,
report_error
)
end
end
end
defp make_clear_error_fn(set_error) do
fn ->
set_error.(nil)
end
end
defp make_report_error_fn(set_error, on_error_callback) do
fn error_info ->
set_error.(error_info)
handle_error_callback(on_error_callback, error_info)
end
end
@doc """
Creates a HOC that provides authentication context and guards.
## Enhanced Props
- `:user` - Current authenticated user (nil if not authenticated)
- `:authenticated` - Boolean indicating authentication status
- `:permissions` - List of user permissions
- `:has_permission` - Function to check permissions
## Examples
with_auth = HigherOrder.with_authentication()
ProtectedComponent = with_auth.(AdminPanel)
"""
def with_authentication(options \\ []) do
login_component =
Keyword.get(options, :login_component, &default_login_component/1)
required_permissions = Keyword.get(options, :required_permissions, [])
unauthorized_component =
Keyword.get(
options,
:unauthorized_component,
&default_unauthorized_component/1
)
fn component_module ->
fn props, context ->
# Get user context
user_context_raw = Hooks.use_context(:user_context)
user_context = normalize_user_context(user_context_raw)
has_permission =
Hooks.use_callback(
make_has_permission_fn(user_context),
[user_context]
)
enhanced_props =
Map.merge(props, %{
user: Map.get(user_context, :user),
authenticated: Map.get(user_context, :authenticated, false),
permissions: Map.get(user_context, :permissions, []),
has_permission: has_permission
})
render_based_on_auth_state(
user_context,
required_permissions,
has_permission,
component_module,
enhanced_props,
context,
login_component,
unauthorized_component
)
end
end
end
defp normalize_user_context(user_context_raw) do
case user_context_raw do
nil -> %{authenticated: false}
map when is_map(map) -> Map.put_new(map, :authenticated, false)
_ -> %{authenticated: false}
end
end
defp make_has_permission_fn(user_context) do
fn permission ->
user_permissions = Map.get(user_context, :permissions, [])
Enum.member?(user_permissions, permission)
end
end
@doc """
Creates a HOC that adds data fetching capabilities.
## Enhanced Props
- `:data` - Fetched data (nil while loading)
- `:loading` - Boolean indicating fetch in progress
- `:error` - Fetch error (nil if no error)
- `:refetch` - Function to refetch data
## Examples
with_data = HigherOrder.with_data_fetching(fn props ->
fetch_users(props.organization_id)
end)
DataDrivenComponent = with_data.(UserList)
"""
def with_data_fetching(fetch_fn, options \\ [])
when is_function(fetch_fn, 1) do
loading_component =
Keyword.get(options, :loading_component, &default_loading_component/1)
error_component =
Keyword.get(options, :error_component, &default_error_component/1)
dependencies = Keyword.get(options, :dependencies, [])
fn component_module ->
fn props, context ->
# Extract dependency values from props
deps = Enum.map(dependencies, &Map.get(props, &1))
# Use async hook for data fetching
{data, loading, error, refetch} =
Hooks.use_async(
make_fetch_wrapper_fn(fetch_fn, props),
deps
)
enhanced_props =
Map.merge(props, %{
data: data,
loading: loading,
error: error,
refetch: refetch
})
render_based_on_data_state(
loading,
error,
component_module,
enhanced_props,
context,
loading_component,
error_component
)
end
end
end
defp make_fetch_wrapper_fn(fetch_fn, props) do
fn ->
fetch_fn.(props)
end
end
@doc """
Creates a HOC that adds theming capabilities.
## Enhanced Props
- `:theme` - Current theme object
- `:set_theme` - Function to change theme
- `:css_vars` - CSS-like variables for styling
## Examples
with_theme = HigherOrder.with_theme()
ThemedButton = with_theme.(Button)
"""
def with_theme(options \\ []) do
theme_context_name = Keyword.get(options, :context_name, :theme_context)
fn component_module ->
fn props, context ->
theme = Hooks.use_context(theme_context_name)
set_theme =
Hooks.use_callback(
make_set_theme_fn(theme_context_name),
[]
)
# Generate CSS-like variables from theme
css_vars = generate_css_variables(theme)
enhanced_props =
Map.merge(props, %{
theme: theme,
set_theme: set_theme,
css_vars: css_vars
})
component_module.render(enhanced_props, context)
end
end
end
defp make_set_theme_fn(theme_context_name) do
fn new_theme ->
Context.update_context_value(theme_context_name, new_theme)
end
end
@doc """
Creates a HOC that adds analytics tracking.
## Enhanced Props
- `:track_event` - Function to track analytics events
- `:component_id` - Unique identifier for analytics
## Examples
with_analytics = HigherOrder.with_analytics("user_profile")
TrackedComponent = with_analytics.(UserProfile)
"""
def with_analytics(component_name, options \\ []) do
analytics_provider =
Keyword.get(options, :provider, &default_analytics_provider/2)
auto_track_mount = Keyword.get(options, :auto_track_mount, true)
fn component_module ->
fn props, context ->
component_id = System.unique_integer([:positive, :monotonic])
track_event =
Hooks.use_callback(
make_track_event_fn(
component_name,
component_id,
analytics_provider
),
[component_id]
)
# Auto-track component mount
Hooks.use_effect(
make_analytics_effect_fn(auto_track_mount, track_event),
[]
)
enhanced_props =
Map.merge(props, %{
track_event: track_event,
component_id: component_id
})
component_module.render(enhanced_props, context)
end
end
end
defp make_track_event_fn(component_name, component_id, analytics_provider) do
fn event_name, event_data ->
final_event_data =
case event_data do
nil ->
%{
component: component_name,
component_id: component_id,
timestamp: System.monotonic_time(:millisecond)
}
data when is_map(data) ->
Map.merge(data, %{
component: component_name,
component_id: component_id,
timestamp: System.monotonic_time(:millisecond)
})
end
analytics_provider.(event_name, final_event_data)
end
end
defp make_analytics_effect_fn(auto_track_mount, track_event) do
fn ->
track_mount_if_enabled(auto_track_mount, track_event)
# Track unmount
fn ->
track_event.("component_unmount")
end
end
end
@doc """
Creates a HOC that adds performance monitoring.
## Enhanced Props
- `:performance` - Performance metrics
- `:start_timer` - Function to start timing operations
- `:end_timer` - Function to end timing operations
## Examples
with_perf = HigherOrder.with_performance_monitoring()
MonitoredComponent = with_perf.(ExpensiveComponent)
"""
def with_performance_monitoring(options \\ []) do
# 60fps
report_threshold_ms = Keyword.get(options, :report_threshold_ms, 16)
fn component_module ->
fn props, context ->
{performance_data, set_performance_data} =
Hooks.use_state(%{
render_times: [],
average_render_time: 0,
slow_renders: 0
})
start_timer =
Hooks.use_callback(
make_start_timer_fn(props),
[]
)
end_timer =
Hooks.use_callback(
make_end_timer_fn(props, set_performance_data, report_threshold_ms),
[set_performance_data, report_threshold_ms]
)
enhanced_props =
Map.merge(props, %{
performance: performance_data,
start_timer: start_timer,
end_timer: end_timer
})
# Time the render
render_start = System.monotonic_time(:microsecond)
result = component_module.render(enhanced_props, context)
render_end = System.monotonic_time(:microsecond)
render_time_ms = (render_end - render_start) / 1000
# Update render time (async to avoid affecting current render)
_ =
Task.start(
make_log_render_task_fn(
render_time_ms,
report_threshold_ms,
component_module
)
)
result
end
end
end
defp make_start_timer_fn(props) do
fn timer_name ->
start_time = System.monotonic_time(:microsecond)
Map.put(props, :"#{timer_name}_start_time", start_time)
end
end
defp make_end_timer_fn(props, set_performance_data, report_threshold_ms) do
fn timer_name ->
end_time = System.monotonic_time(:microsecond)
start_time = Map.get(props, :"#{timer_name}_start_time")
calculate_timing_if_available(
start_time,
end_time,
set_performance_data,
report_threshold_ms
)
end
end
defp make_log_render_task_fn(
render_time_ms,
report_threshold_ms,
component_module
) do
fn ->
log_slow_render_if_needed(
render_time_ms,
report_threshold_ms,
component_module
)
end
end
@doc """
Creates a HOC that adds memoization to prevent unnecessary re-renders.
## Examples
with_memo = HigherOrder.with_memoization(fn props -> [props.data, props.config] end)
MemoizedComponent = with_memo.(ExpensiveComponent)
"""
def with_memoization(deps_fn) when is_function(deps_fn, 1) do
fn component_module ->
fn props, context ->
deps = deps_fn.(props)
# Memoize the render result
Hooks.use_memo(
make_memoized_render_fn(component_module, props, context),
deps
)
end
end
end
defp make_memoized_render_fn(component_module, props, context) do
fn ->
component_module.render(props, context)
end
end
@doc """
Chains multiple HOCs together.
## Examples
super_hoc = HigherOrder.chain([
HigherOrder.with_loading_state(),
HigherOrder.with_error_boundary(),
HigherOrder.with_authentication(required_permissions: [:admin]),
HigherOrder.with_analytics("admin_panel")
])
SuperAdminPanel = super_hoc.(AdminPanel)
"""
def chain(hocs) when is_list(hocs) do
fn component_module ->
Enum.reduce(Enum.reverse(hocs), component_module, fn hoc, acc_component ->
hoc.(acc_component)
end)
end
end
@doc """
Creates a conditional HOC that only applies enhancement based on a condition.
## Examples
conditional_auth = HigherOrder.when_condition(
fn props -> props.require_auth end,
HigherOrder.with_authentication()
)
"""
def when_condition(condition_fn, hoc) when is_function(condition_fn, 1) do
fn component_module ->
fn props, context ->
apply_hoc_conditionally(
condition_fn.(props),
hoc,
component_module,
props,
context
)
end
end
end
# Default component implementations
defp default_loading_component(_props) do
%{
type: :text,
attrs: %{
content: "Loading...",
style: %{color: :secondary}
}
}
end
defp default_error_component(props) do
error_message =
case Map.get(props, :error) do
%{reason: reason} -> "Error: #{inspect(reason)}"
error when is_binary(error) -> "Error: #{error}"
_ -> "An error occurred"
end
%{
type: :column,
attrs: %{gap: 5},
children: [
%{
type: :text,
attrs: %{
content: error_message,
style: %{color: :error}
}
},
%{
type: :button,
attrs: %{
label: "Retry",
on_click: Map.get(props, :clear_error, fn -> :ok end)
}
}
]
}
end
defp default_login_component(_props) do
%{
type: :column,
attrs: %{gap: 10, align: :center},
children: [
%{type: :text, attrs: %{content: "Please log in to continue"}},
%{type: :button, attrs: %{label: "Login"}}
]
}
end
defp default_unauthorized_component(_props) do
%{
type: :text,
attrs: %{
content: "You don't have permission to view this content",
style: %{color: :warning}
}
}
end
defp generate_css_variables(theme) do
flatten_theme(theme, "", %{})
end
defp flatten_theme(%{} = map, prefix, acc) do
Enum.reduce(map, acc, fn {key, value}, inner_acc ->
var_name = if prefix == "", do: "--#{key}", else: "#{prefix}-#{key}"
case value do
%{} = nested_map ->
flatten_theme(nested_map, var_name, inner_acc)
_ ->
Map.put(inner_acc, var_name, value)
end
end)
end
defp flatten_theme(value, prefix, acc) do
Map.put(acc, prefix, value)
end
defp default_analytics_provider(event_name, event_data) do
Log.info("Analytics: #{event_name} - #{inspect(event_data)}")
end
defp render_based_on_auth_state(
user_context,
required_permissions,
has_permission,
component_module,
enhanced_props,
context,
login_component,
unauthorized_component
)
defp render_based_on_auth_state(
%{authenticated: false},
_required_permissions,
_has_permission,
_component_module,
enhanced_props,
_context,
login_component,
_unauthorized_component
) do
login_component.(enhanced_props)
end
defp render_based_on_auth_state(
%{authenticated: true},
required_permissions,
has_permission,
_component_module,
enhanced_props,
_context,
_login_component,
unauthorized_component
)
when required_permissions != [] and
not is_function(has_permission) do
unauthorized_component.(enhanced_props)
end
defp render_based_on_auth_state(
%{authenticated: true},
required_permissions,
has_permission,
component_module,
enhanced_props,
context,
_login_component,
unauthorized_component
)
when required_permissions != [] do
render_based_on_permissions(
required_permissions,
has_permission,
component_module,
enhanced_props,
context,
unauthorized_component
)
end
defp render_based_on_auth_state(
%{authenticated: true},
_required_permissions,
_has_permission,
component_module,
enhanced_props,
context,
_login_component,
_unauthorized_component
) do
component_module.render(enhanced_props, context)
end
defp render_based_on_data_state(
false,
error,
_component_module,
enhanced_props,
_context,
_loading_component,
error_component
)
when not is_nil(error) do
error_component.(enhanced_props)
end
defp render_based_on_data_state(
false,
_error,
component_module,
enhanced_props,
context,
_loading_component,
_error_component
) do
component_module.render(enhanced_props, context)
end
## Pattern matching helper functions for if statement elimination
defp render_with_loading_state(
true,
loading_component,
enhanced_props,
_component_module,
_context
) do
loading_component.(enhanced_props)
end
defp render_with_loading_state(
false,
_loading_component,
enhanced_props,
component_module,
context
) do
component_module.render(enhanced_props, context)
end
defp handle_error_callback(nil, _error_info), do: :ok
defp handle_error_callback(callback, error_info), do: callback.(error_info)
defp render_with_error_boundary(
nil,
error_component,
enhanced_props,
component_module,
context,
props,
report_error
) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
component_module.render(enhanced_props, context)
end) do
{:ok, result} ->
result
{:error, {reason, stacktrace}} ->
error_info = %{
kind: :error,
reason: reason,
stacktrace: stacktrace,
component: component_module,
props: props,
timestamp: System.monotonic_time(:millisecond)
}
report_error.(error_info)
error_component.(enhanced_props)
end
end
defp render_with_error_boundary(
error,
error_component,
enhanced_props,
_component_module,
_context,
_props,
_report_error
)
when not is_nil(error) do
error_component.(enhanced_props)
end
defp track_mount_if_enabled(false, _track_event), do: :ok
defp track_mount_if_enabled(true, track_event),
do: track_event.("component_mount")
defp calculate_timing_if_available(
nil,
_end_time,
_set_performance_data,
_threshold
),
do: :ok
defp calculate_timing_if_available(
start_time,
end_time,
set_performance_data,
report_threshold_ms
) do
duration_ms = (end_time - start_time) / 1000
# Update performance data
set_performance_data.(fn perf ->
# Keep last 20
new_render_times = [
duration_ms | Enum.take(perf.render_times, 19)
]
avg_time =
Enum.sum(new_render_times) / length(new_render_times)
slow_count =
increment_slow_count_if_needed(
duration_ms,
report_threshold_ms,
perf.slow_renders
)
%{
render_times: new_render_times,
average_render_time: avg_time,
slow_renders: slow_count
}
end)
end
defp increment_slow_count_if_needed(duration_ms, threshold, current_count)
when duration_ms > threshold do
current_count + 1
end
defp increment_slow_count_if_needed(_duration_ms, _threshold, current_count),
do: current_count
defp log_slow_render_if_needed(render_time_ms, threshold, component_module)
when render_time_ms > threshold do
Log.warning(
"Slow render detected: #{component_module} took #{Float.round(render_time_ms, 2)}ms"
)
end
defp log_slow_render_if_needed(
_render_time_ms,
_threshold,
_component_module
),
do: :ok
defp apply_hoc_conditionally(true, hoc, component_module, props, context) do
enhanced_component = hoc.(component_module)
enhanced_component.(props, context)
end
defp apply_hoc_conditionally(false, _hoc, component_module, props, context) do
component_module.render(props, context)
end
defp render_based_on_permissions(
required_permissions,
has_permission,
component_module,
enhanced_props,
context,
unauthorized_component
) do
case Enum.all?(required_permissions, has_permission) do
true -> component_module.render(enhanced_props, context)
false -> unauthorized_component.(enhanced_props)
end
end
end