Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project.
Freya is a cross-platform, native, declarative GUI library for Rust.
General rules:
Start by asking the user what they would like to do:
#[derive(PartialEq)]
struct Counter {
initial: i32,
}
impl Component for Counter {
fn render(&self) -> impl IntoElement {
let mut count = use_state(|| self.initial);
label()
.on_mouse_up(move |_| *count.write() += 1)
.text(format!("Count: {}", count.read()))
}
}
#[derive(PartialEq)] is required - Freya uses it to skip re-rendering unchanged subtrees.KeyExt and ChildrenExt when the component can be keyed or accept children.fn app() -> impl IntoElement {
rect().child("Hello, World!")
}
Pass data from main via the App trait:
struct MyApp { number: u8 }
impl App for MyApp {
fn render(&self) -> impl IntoElement {
label().text(self.number.to_string())
}
}
fn colored_label(color: Color, text: &str) -> impl IntoElement {
label().color(color).text(text.to_string())
}
Use plain functions when you only need to reuse a chunk of UI with no internal state. Use a Component when you need hooks or render optimization.
Elements use a fluent builder API. Never store an element in a variable to modify it later - chain all methods directly or use .when / .map.
// Good
rect()
.background((255, 0, 0))
.width(Size::fill())
.height(Size::px(100.))
.center() // centers children both axes
.expanded() // fills available space in parent's main axis
.maybe(is_active, |r| r.child("Active"))
.map(some_value, |r, v| r.child(v.to_string()))
// Bad - storing to modify later
let mut element = rect();
Common layout shorthands: .center() centers children on both axes; .expanded() makes the element fill all remaining space along the parent's main axis (equivalent to flex: 1 in CSS).
rect()
.maybe(show_badge, |r| r.child("New")) // bool condition
.map(large_size, |r, size| r.height(size)) // Option<T>, passes value
.maybe_child(optional_element) // Option<impl IntoElement>
.maybe(bool, |el| el) - applies the callback when the condition is true.map(Option<T>, |el, val| el) - applies the callback when the Option is Some, passing the inner value.maybe_child(Option<impl IntoElement>) - appends a child only when Some&str and String implement Into<Label>, so prefer passing them directly instead of constructing a label():
rect().child("Hello") // preferred
rect().child(label().text("Hello")) // unnecessary
Hooks are prefixed with use_ (e.g. use_state, use_animation). Follow these rules:
render - never inside conditionals, loops, or closures.render and capture the values in move closures instead.render method or a function component.spawn callbacks are async and cannot call hooks; capture state before spawning. Some hooks have non-hook counterparts that are safe to call in async contexts (e.g. use_consume → consume_context()).Capture hook values in move closures for event handlers:
let mut state = use_state(|| false);
let on_click = move |_| state.set(true); // capture, not call inside handler
rect().on_mouse_up(on_click)
let mut count = use_state(|| 0);
*count.write() += 1; // write
let n = *count.read(); // read
count.set(5); // convenience setter
use_state returns a Copy type (State<T>). No .clone() needed when passing it around.
Pass local state to child components:
#[derive(PartialEq)]
struct Child(State<i32>);
Use Freya Radio for large or deeply nested app state where you need surgical, fine-grained updates - only the components subscribed to a specific channel re-render when that channel changes. This makes it well-suited for complex UIs (e.g. a tab system where each tab has independent state, or a big data model where different parts of the UI subscribe to different slices).
Define your state and a channel enum that maps to the parts of the state that can change independently:
#[derive(Default, Clone)]
struct AppState {
count: i32,
name: String,
}
#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum AppChannel {
Count,
Name,
}
impl RadioChannel<AppState> for AppChannel {}
Initialize once in the root component, then subscribe from any descendant:
// Root
use_init_radio_station::<AppState, AppChannel>(AppState::default);
// Any component - only re-renders when AppChannel::Count changes
let mut radio = use_radio(AppChannel::Count);
radio.read().count;
radio.write().count += 1;
For channels where a write to one should also notify subscribers of another, override derive_channel:
impl RadioChannel<AppState> for AppChannel {
fn derive_channel(self, _state: &AppState) -> Vec<Self> {
match self {
// Writing to Count also notifies Name subscribers
AppChannel::Count => vec![self, AppChannel::Name],
AppChannel::Name => vec![self],
}
}
}
For complex state transitions, implement the reducer pattern with DataReducer:
impl DataReducer for AppState {
type Channel = AppChannel;
type Action = AppAction;
fn reduce(&mut self, action: AppAction) -> ChannelSelection<AppChannel> {
match action {
AppAction::Increment => { self.count += 1; }
AppAction::SetName(n) => { self.name = n; }
}
ChannelSelection::Current
}
}
// Then in a component:
radio.apply(AppAction::Increment);
Use Readable<T> / Writable<T> as component props when the component should accept state from any source:
#[derive(PartialEq)]
struct NameInput { name: Writable<String> }
// Caller passes either local state or radio slice:
NameInput { name: local_name.into_writable() }
NameInput { name: name_slice.into_writable() }
Use context to make a value available to any descendant component without threading it through every prop. Prefer this over static variables, thread_local!, or global singletons - context is scoped to the component tree and plays well with Freya's reactivity.
// Provider: stores the value and makes it available to all descendants
fn app() -> impl IntoElement {
use_provide_context(|| AppConfig { theme: Theme::Dark });
rect().child(DeepChild {})
}
// Consumer: retrieve by type, walks up the tree until found
#[derive(PartialEq)]
struct DeepChild;
impl Component for DeepChild {
fn render(&self) -> impl IntoElement {
let config = use_consume::<AppConfig>();
format!("Theme: {:?}", config.theme)
}
}
Use use_try_consume::<T>() when the context may not be present. If context is not found, use_consume panics.
Context values are identified by type, so each distinct type gets its own slot. Providing the same type again in a deeper component shadows the ancestor's value for that subtree.
Context is the right tool for dependency injection (e.g. passing a DB client, config, or theme down the tree). For reactive shared state use Freya Radio; for passing state between a parent and immediate children, plain props or State<T> are simpler.
use_state - component-local stateReadable/Writable - reusable components that don't care about backing storageFor simple derived values, compute them directly in render - no hook needed:
let doubled = *count.read() * 2;
For expensive computations that should only re-run when their dependencies change, use use_memo. It subscribes to any State read inside the callback and caches the result:
let expensive = use_memo(move || {
let n = *count.read(); // subscribed - reruns when count changes
compute_something(n)
});
let value = expensive.read();
For side effects that should re-run when state changes (e.g. logging, triggering external systems), use use_side_effect. Do not use it to sync one state into another - derive values directly or use use_memo instead:
use_side_effect(move || {
let value = *count.read(); // subscribed
println!("count changed: {value}");
});
Use Freya's spawn() (not tokio::spawn) for async work that updates the UI. Tasks spawned with spawn() are tied to Freya's reactivity system and can safely write to component state:
let mut data = use_state(|| None);
use_hook(move || {
spawn(async move {
let result = fetch_something().await;
data.set(Some(result));
});
});
use_hook runs once on mount, making it the right place for one-shot side effects. spawn returns a TaskHandle you can cancel if needed.
Components and hooks are synchronous - you cannot await inside render. Prefer use_future for typical async work (see below). Only reach for use_hook + spawn when you need fine-grained control over the task lifecycle:
// Only when you need manual control
use_hook(move || {
spawn(async move {
let s = some_async_fn().await;
result.set(s);
});
});
use_future wraps this pattern: it starts an async task on mount and exposes its state as FutureState<D> (Pending, Loading, Fulfilled(D)):
let task = use_future(|| async {
fetch_user(42).await
});
match &*task.state() {
FutureState::Pending | FutureState::Loading => "Loading...",
FutureState::Fulfilled(user) => user.name.as_str(),
}
Call task.start() to restart and task.cancel() to stop it.
For data that should be cached, deduplicated, and automatically refetched, use freya-query (features = ["query"]):
// Define the query
#[derive(Clone, PartialEq, Hash, Eq)]
struct FetchUser;
impl QueryCapability for FetchUser {
type Ok = String;
type Err = String;
type Keys = u32;
async fn run(&self, user_id: &u32) -> Result<String, String> {
Ok(format!("User {user_id}"))
}
}
// Use it in a component
impl Component for UserProfile {
fn render(&self) -> impl IntoElement {
let query = use_query(Query::new(self.0, FetchUser));
match &*query.read().state() {
QueryStateData::Pending => "Loading...",
QueryStateData::Settled { res, .. } => res.as_deref().unwrap_or("Error"),
QueryStateData::Loading { .. } => "Refreshing...",
}
}
}
Multiple components using the same (capability, keys) pair share one cache entry. Invalidate with query.invalidate() or QueriesStorage::<FetchUser>::invalidate_all().await.
For write operations, use use_mutation + MutationCapability. The on_settled callback is the right place to invalidate related queries after a mutation.
Prefer freya-query over manual use_future + state when you need caching, background refetch, or deduplication.
Freya has its own async runtime. To use Tokio-ecosystem crates (reqwest, sqlx, etc.), enter a Tokio runtime context in main before launching:
fn main() {
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
let _guard = rt.enter(); // keep alive for the whole program
launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}
Use Freya's spawn() for UI updates. tokio::spawn runs on the Tokio runtime and cannot update component state.
Use .key(id) on elements in dynamic lists to ensure correct reconciliation on reorders:
VirtualScrollView::new(|i, _| {
rect()
.key(i)
.child(format!("Item {i}"))
.into()
})
.length(items.len())
Missing .key() in dynamic lists causes element misidentification during reorders.
Enable with features = ["i18n"]. Uses Fluent (.ftl files) for translations.
1. Define .ftl files:
# en-US.ftl
hello_world = Hello, World!
hello = Hello, { $name }!
2. Initialize once in the root component:
use freya::i18n::*;
let mut i18n = use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
.with_locale(Locale::new_static(langid!("en-US"), include_str!("../i18n/en-US.ftl")))
.with_locale(Locale::new_static(langid!("es-ES"), include_str!("../i18n/es-ES.ftl")))
.with_fallback(langid!("en-US"))
});
3. Translate in any descendant component:
// t! panics if key missing, te! returns Result, tid! falls back to the key string
t!("hello_world") // "Hello, World!"
t!("hello", name: {"Alice"}) // "Hello, Alice!"
te!("hello_world") // Ok("Hello, World!")
tid!("missing-key") // "message-id: missing-key should be translated"
4. Switch language at runtime:
let mut i18n = I18n::get(); // retrieve from any descendant
i18n.set_language(langid!("es-ES"));
For multi-window apps, create with I18n::create_global in main and share with use_share_i18n.
Use use_animation for manual control and use_animation_transition to animate between two values reactively:
// Manual: call .start() / .reverse() yourself
let mut anim = use_animation(|_| AnimColor::new((240, 240, 240), (200, 80, 80)).time(400));
rect().background(&*anim.read()).on_press(move |_| anim.start())
// Transition: re-runs automatically when the tracked value changes
let color = use_animation_transition(is_active, |from, to| AnimColor::new(from, to).time(300));
rect().background(&*color.read())
Animate colors (AnimColor), sizes, positions, and other numeric properties. Easing functions and sequencing are supported.
Enable with features = ["router"]. Define routes with #[derive(Routable)], render them with router::<Route>(), place the current page with outlet::<Route>(), and navigate with Link or RouterContext::get().replace(...):
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[route("/")]
Home,
#[route("/settings")]
Settings,
}
fn app() -> impl IntoElement {
router::<Route>(|| RouterConfig::default())
}
freya-testing lets you test components without a window. Use TestingRunner to mount a component, simulate interactions, and assert on state:
use freya_testing::prelude::*;
let (mut runner, state) = TestingRunner::new(app, (300., 300.).into(), |r| {
r.provide_root_context(|| State::create(0))
}, 1.);
runner.sync_and_update();
runner.click_cursor((15., 15.));
assert_eq!(*state.peek(), 1);
Call runner.render_to_file("out.png") to snapshot the current UI.
Enable with features = ["icons"]. Uses Lucide icons rendered as SVGs:
use freya::icons;
svg(icons::lucide::antenna()).color((120, 50, 255)).expanded()
Use use_editable to manage a text editor with cursor, selection, keyboard shortcuts, and virtualization. Wire it to a paragraph() element's event handlers and feed EditableEvents from mouse/keyboard events. See examples/ for full wiring.
Enable with features = ["code-editor"]. CodeEditorData holds a Rope-backed buffer with tree-sitter syntax highlighting. Pass it to the CodeEditor component:
let editor = use_state(|| {
let mut e = CodeEditorData::new(Rope::from_str(src), LanguageId::Rust);
e.parse();
e.measure(14., "Jetbrains Mono");
e
});
CodeEditor::new(editor, focus.a11y_id())
Enable with features = ["plot"]. Use the plot() element with a RenderCallback and draw into it using the Plotters API via PlotSkiaBackend:
plot(RenderCallback::new(|ctx| {
let backend = PlotSkiaBackend::new(ctx.canvas, ctx.font_collection, size).into_drawing_area();
// ... Plotters drawing code
})).expanded()
Enable with features = ["material-design"]. Adds style modifiers like .ripple() to built-in components:
use freya::material_design::*;
Button::new().ripple().child("Click me")
Enable with features = ["webview"]. Embeds a browser view into your UI:
use freya::webview::*;
WebView::new("https://example.com").expanded()
Enable with features = ["terminal"]. Spawns a PTY process and renders it as a terminal:
use freya::terminal::*;
let mut cmd = CommandBuilder::new("bash");
cmd.env("TERM", "xterm-256color");
let handle = TerminalHandle::new(TerminalId::new(), cmd, None).ok();
// Render with Terminal::new(handle) and forward keyboard events via handle.write_key()
Enable with features = ["devtools"]. Adds a real-time component tree inspector. Run the devtools app alongside your app to examine layout, props, and state.
Add to your Cargo.toml as needed:
freya = { version = "...", features = ["router", "radio"] }
| Feature | What it enables |
|---|---|
router | Page routing (freya-router) |
i18n | Internationalization via Fluent (freya-i18n) |
remote-asset | Load images/assets from remote URLs |
radio | Global state management (freya-radio) |
query | Async data fetching with caching (freya-query) |
sdk | Generic utility APIs (freya-sdk) |
plot | Chart/plotting via Plotters (freya-plotters-backend) |
gif | Animated GIF support in GifViewer |
calendar | Calendar date-picker component |
markdown | Markdown renderer component |
icons | SVG icon library via Lucide (freya-icons) |
material-design | Material Design theme (freya-material-design) |
webview | Embed a WebView (freya-webview) |
terminal | Terminal emulator (freya-terminal) |
code-editor | Code editing APIs (freya-code-editor) |
tray | System tray support |
titlebar | Custom window titlebar component |
devtools | Developer tools overlay |
performance | Performance monitoring plugin |
hotpath | Hot-path optimization |
all | All of the above (except devtools/performance/hotpath) |
AGENTS.md (also symlinked as CLAUDE.md) in the repo root - authoritative dev workflow and Rust conventions for working on Freya itself.crates/freya/src/_docs/ - in-source documentation for hooks, state management, components, routing, animations, and more.examples/ - 150+ working examples covering every feature.