You are reading release documentation. Read the latest development docs.

Migration Guide: v0.9 to v0.10

Warning: v0.10 is still currently in active development. Any content below is a work in progress and not finalised yet.

Components and component props

This release contains several changes to how components and component props are used and defined.

Implicit props by default

Every component now uses inline props by default. Support for struct props has been removed. To migrate, either move all fields from your props struct to function arguments, or pass the entire struct as a single prop instead.

// Old
#[derive(Props)]
struct FooProps {
    bar: i32
}
#[component]
fn Foo(props: FooProps) - View { ... }

// Old, with inline props
#[component(inline_props)]
fn Foo(bar: i32) - View { ... }

// New
#[component]
fn Foo(bar: i32) - View { ... }

New #[prop] attributes

The behavior of #[prop] attributes on component props has changed. The previous behavior was not well documented at all and was an ad-hoc modification of the typed-builder crate.

The new behavior is intentionally kept simple and adapted to Sycamore’s use case. For more details, see the doc page on the #[component] proc-macro.

Here is a quick example of the new syntax

#[component]
fn Foo(
    // Set the default value of the prop if no explicit value is passed.
    #[prop(default = 42)] x: i32,
    // Accept anything that can be converted to a String.
    #[prop(into)] y: String,
    // Option types are optional by default.
    z: Optioni32
    // This behavior can also be disabled.
    #[prop(required)]
    w: Optioni32
) - View { ... }

Components automatically return builders

The #[component] function now transforms component functions to return a fluent-style builder instead of just the raw component function. For users of the view! macro, this shouldn’t change anything but for builder API users, this makes components much nicer to use.

// Old
#[component(implicit_props)]
fn Foo(bar: i32) - View { ... }

div().children(
    Foo(FooProps::builder().bar(123).build())
)
// New
#[component]
fn Foo(bar: i32) - View { ... }

div().children(
    Foo().bar(i32)
)

This unifies the syntax between components and built-in elements.

New syntax for attribute passthrough

The way attribute passthrough is defined for a component has changed to be more flexible for alternative rendering backends.

// Old
#[component(implicit_props)]
fn MyButton(#[prop(attributes(html, button))] attributes: Attributes) - View {
    ...
}

// New
#[component(extend_element!(html:button))]
fn MyButton(attributes: Attributes) - View {
    ...
}

Interpolated text strings

String literals used as text nodes in view! now support reactive interpolation:

view! {
    p { "Hello, {name}!" }
}

Literal braces in text nodes must now be escaped as {{ and }}, just like in format!. For example, change "{}" to "{{}}". This also applies to raw string literals, including inline CSS in style elements. String literals used as attributes or component props are unchanged.

Explicit reactive attributes and properties

The view! macro no longer automatically wraps element attribute or DOM property expressions in closures. Like component props, expressions are now passed directly. Use an explicit closure when an expression should update reactively:

// Old
view! {
    button(disabled=is_editing.get())
    progress(prop:value=progress.get()[0])
}
// New
view! {
    button(disabled=move || is_editing.get())
    progress(prop:value=move || progress.get()[0])
}

This also applies to hyphenated and quoted attributes and prop: properties passed through components, for example MyInput(data-count=move || count.get().to_string()). Without a closure, a signal read such as disabled=is_editing.get() is evaluated once and will not update, even though it still compiles. Passing a signal directly where supported (for example disabled=is_editing) continues to work.

Child expressions and the DSL’s if/match expressions still automatically track reactive dependencies. Event handlers, bindings, refs, and attribute spreads continue to be passed directly.

Event handlers are now normal methods

Instead of the bespoke on:event=... syntax for event handlers, all events should be attached using on_event=... syntax. For example:

// Old
view! {
    div(on:click=|| { ... })
}
// New
view! {
    div(on_click=|| { ... })
}

This change also makes the builder API slightly simpler to use.

// Old
div().on(event::click, || { ... })
// New
div().on_click(|| { ... })