This is a release made from #2607, now that the 0.7 branch has reached a relatively stable state, and all of the examples are in working condition.
0.7 is a nearly-complete rewrite of the internals of the framework, with the following goals:
- maintain backwards compatibility for as much user application code as possible
- improve the async story and fix Suspense edge cases and limitations
- reduce WASM binary size
- reduce HTML size
- faster HTML rendering
- allow signals to be sent across threads
- enhance the ergonomics of things like prop spreading and accessing the HTML shell of your application
- build the foundation for future work
- reactive stores to make nested reactivity more pleasant
- client-side routing with islands and state preservation
- integrating with native UI toolkits to create desktop applications
My goal between now and the end of July is to fill in the remaining missing pieces and release a beta version.
Getting Started
0.7 works with the current cargo-leptos
version. If you want to start exploring, I've created starter templates for Axum and Actix. Each template is only three files. They show some of the boilerplate differences; for more details, see below.
Axum: cargo leptos new --git https://github.com/leptos-rs/start-axum-0.7
(repo)
Actix: cargo leptos new --git https://github.com/leptos-rs/start-actix-0.7
(repo)
Please use the PR (#2607) or the #preview
channel in our Discord for feedback or questions.
Generally speaking, the leptos_0.7
branch will include the latest bug fixes, but any given commit may be broken (sorry!)
New Features
.await
on resources and async
in <Suspense/>
Currently, create_resource
allows you to synchronously access the value of some async data as either None
or Some(_)
. However, it requires that you always access it this way. This has some drawbacks:
- requires that you null-check every piece of data
- makes it difficult for one resource to wait for another resource to load
Now, you can .await
a resource, and you can use async
blocks within a <Suspense/>
via the Suspend
wrapper, which makes it easier to chain two resources:
let user = Resource::new(|| (), |_| user_id());
let posts = Resource::new(
// resources still manually track dependencies (necessary for hydration)
move || user.track(),
move |_| async move {
// but you can .await a resource inside another
let user = user.await?;
get_posts(user).await
},
);
view! {
<Suspense>
// you can `.await` resources to avoid dealing with the `None` state
<p>"User ID: " {move || Suspend(async move {
match user.await {
// ...
}
})}</p>
// or you can still use .get() to access resources in things like component props
<For
each=move || posts.get().and_then(Result::ok).unwrap_or_default()
key=|post| post.id
let:post
>
// ...
</For>
</Suspense>
}
Reference-counted signal types
One of the awkward edge cases of current Leptos is that our Copy
arena for signals makes it possible to leak memory if you have a collection of nested signals and do not dispose them. (See current example.) 0.7 exposes ArcRwSignal
, ArcReadSignal
, etc., which are Clone
but not Copy
and manage their memory via reference counting, but can easily be converted into the copyable RwSignal
etc. This makes working with nested signal correctly much easier, without sacrificing ergonomics meaningfully. See the 0.7 counters
example for more.
.read()
and .write()
on signals
You can now use .read()
and .write()
to get immutable and mutable guards for the value of a signal, which will track/update appropriately: these work like .with()
and .update()
but without the extra closure, or like .get()
but without cloning.
let long_vec = RwSignal::new(vec![42; 1000]);
let short_vec = RwSignal::new(vec![13; 2]);
// bad: clones both Vecs
let bad_len = move || long_vec.get().len() + short_vec.get().len();
// ugly: awkward nested syntax (or a macro)
let ugly_len = move || long_vec.with(|long| short_vec.with(|short| long.len() + short.len()));
// readable but doesn't clone
let good_len = move || long_vec.read().len() + short_vec.read().len();
These should always be used for short periods of time, not stored somewhere for longer-term use, just like any guard or lock, or you can cause deadlocks or panics.
Custom HTML shell
The HTML document "shell" for server rendering is currently hardcoded as part of the server integrations, limiting your ability to customize it. Now you simply include it as part of your application, which also means that you can customize things like teh <title>
without needing to use leptos_meta
.
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
Enhanced attribute spreading
Any valid attribute can now be spread onto any component, allowing you to extend the UI created by a component however you want. This works through multiple components: for example, if you spread attributes onto a Suspense
they will be passed through to whatever it returns.
// attributes that are spread onto a component will be applied to *all* elements returned as part of
// the component's view. to apply attributes to a subset of the component, pass them via a component prop
<ComponentThatTakesSpread
// the class:, style:, prop:, on: syntaxes work just as they do on elements
class:foo=true
style:font-weight="bold"
prop:cool=42
on:click=move |_| alert("clicked ComponentThatTakesSpread")
// props are passed as they usually are on components
some_prop=13
// to pass a plain HTML attribute, prefix it with attr:
attr:id="foo"
// or, if you want to include multiple attributes, rather than prefixing each with
// attr:, you can separate them from component props with the spread {..}
{..} // everything after this is treated as an HTML attribute
title="ooh, a title!"
{..spread_onto_component}
/>
Improved <ProtectedRoute/>
The current ProtectedRoute
component is not great: it checks the condition once, synchronously, on navigation, and so it doesn't respond to changes and can't easily be used with async data. The new ProtectedRoute
is reactive and uses Suspense so you can use resources or reactive data. There are examples of this now in router
and ssr_modes_axum
.
Known missing or not-yet-implemented APIs
- local resources (resources that only run on the client, not server)
-
<ProtectedRoute/>
- Render individual
Suspend(_)
without a<Suspense/>
- Nested router
set_is_routing
- Attribute spreading for attributes on
<Body/>
etc. instead ofVec<AnyAttribute>
- Equivalent to a user implementing
IntoView
on an arbitrary struct - Figuring out the interaction between
Lazy
routes and hydration (what do you do while hydrating if the code for the route hasn't been lazy-loaded yet?) -
path!()
macro to make route definitions easier in router - Reactive channel implementation for single-use signals
- blocking resources
- partially-blocked SSR
- Thread-local arena that yields
Copy
but!Send
for!Send
data (like browser things) - global class support
- hot reloading (and therefore view markers)
- static routes
Breaking Changes
Imports
I'm reorganizing the module structure to improve docs and discoverability. We will still have a prelude that can be used for glob imports of almost everything that's currently exported from the root.
- use leptos::*;
+ use leptos::prelude::*;
Likewise, the router exposes things via leptos_router::components
and leptos_router::hooks
. rust-analyzer can help fix imports fairly well.
I'm hoping for feedback on the new module structure, whether it makes sense, and any improvements. I have not done too much work to sort through the reexports, look at how docs look, etc. yet.
Naming
We're migrating away from create_
naming toward more idiomatic Rust naming patterns:
create_signal
tosignal
(likechannel
)create_rw_signal
toRwSignal::new()
- etc.
I've left some of the current functions in, marked deprecated; others may have been missed, but should be easy to find via docs.rs.
Type erasure and view types
One of the major changes in this release is replacing the View
enum with statically-typed views, which is where most of the binary size savings come from. If you need to branch and return one of several types, you can either use one of the Either
enums in leptos::either
, or you can use .into_any()
to erase the type. Generally speaking the compiler can do its job better if you maintain more type information so the Either
types should be preferred, but AnyView
is not bad to use when needed.
// Either
if some_condition {
Either::Left(view! { <p>"Foo"</p> })
} else {
Either::Right("Bar")
}
// .into_any()
if some_condition {
view! { <p>"Foo"</p> }.into_any()
} else {
"Bar".into_any()
}
Boilerplate
There have been changes to the SSR and hydration boilerplate, which include (but aren't limited to)
get_configuration
is sync (remove the.await
)- you provide the app shell
.leptos_routes
no longer takesLeptosOptions
as an argument- use
leptos::mount::hydrate_body
(hydration) instead ofleptos::mount::mount_to_body
(which is now CSR-specific) - ... and probably more
Check the starter templates for a good setup.
Route definitions
The patterns for route definition have changed in several ways.
fallback
is now a required prop on<Routes/>
, rather than an optional prop on<Router/>
- If you do not need nested routes, there is now a
<FlatRoutes/>
component that optimizes for this case - If you use nested routes, any routes with children should be
<ParentRoute/>
- Route paths are defined with static types, rather than strings:
path="foo"
becomespath=StaticSegment("foo")
, and there arepath=":id"
becomespath=ParamSegment("id")
,path="posts/:id"
becomespath=(StaticSegment("posts"), ParamSegment("id"))
, and so on. I plan to create apath!()
macro that will do this for you, and just haven't done it yet.
See the router
and hackernews
examples.
Send
/Sync
signals
The data held in reactive primitives (signals, memos, effects) must be safe to send across threads. Threading the needle of the type system in order to ensure thread safety on the server while allowing flexibility in the browser (where you generally don't have multiple threads and where most types are !Send
) is complicated. If you have browser-only types that need to be stored in signals, you can use the send_wrapper
crate to wrap them. We also have _unsync
versions of several APIs that allow you to use !Send
types: see example.
Miscellaneous
I'm sure there are a bunch of small and larger changes I have not mentioned above. By the time of final release, help compiling a total list of breaking changes/migration guide would be much appreciated. At present, the starter templates and the examples
directory in the PR can provide a pretty comprehensive set of changes.
On storing views in signals...
There's a pattern I've seen many use that I do not particularly like, but accidentally enabled through the way APIs happened to be (or needed to be) designed in Leptos 0.1-0.6, in which a user stores some view in a signal and then reads it somewhere else. This was possible because View
needed to be Clone
for internal reasons. Some users used this to create custom control flow: for example, you could create a global "header view" signal, and then update it from leaf components by storing a new view in it.
I'd consider this a bit of an antipattern, for a couple reasons:
- Ideally the application is designed so that data flows through the reactive graph, and the view is defined declaratively at the "leaves" of the application by components that take that reactive data
- More practically, DOM elements are
Clone
but in a surprising way: you can clone the reference to a DOM node, but that is a shallow, not a deep clone, and if you use it in multiple places by.get()
ing the signal more than once, it will only appear in the last location
In the statically-typed view tree, views are not necessarily cloneable (including the AnyView
type), so they can't easily be stored in a signal.
However, it is possible to achieve a similar goal by using a "reactive channel" pattern instead:
let count = RwSignal::new(0);
let trigger = ArcTrigger::new();
let (tx, rx) = std::sync::mpsc::channel();
let on_click = {
let trigger = trigger.clone();
move |_| {
leptos::logging::log!("clicked");
*count.write() += 1;
tx.send(if *count.read() % 2 == 0 {
view! { <p>"An even paragraph"</p> }.into_any()
} else {
view! { <span>"An odd span"</span> }.into_any()
})
.unwrap();
trigger.trigger();
}
};
view! {
<div>
<button on:click=on_click>"Update view"</button>
{move || {
trigger.track();
rx.try_recv().unwrap_or_else(|_| view! {
<p>"Click the button once to begin."</p>
}.into_any())
}}
</div>
}
Send the views through a channel means they do not need to be cloned, and won't be used in more than once place (avoiding the edge cases of 2 above.) Each time you send a view through the channel, simply trigger the trigger.