Skip to content

Commit

Permalink
porting book to 0.7 (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
gbj authored Nov 30, 2024
1 parent dacf468 commit 96b169a
Show file tree
Hide file tree
Showing 48 changed files with 1,328 additions and 1,849 deletions.
2 changes: 1 addition & 1 deletion src/01_introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), an
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
understand Leptos.

You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/0.7.0-gamma3/leptos/).

> The source code for the book is available [here](https://github.com/leptos-rs/book). PRs for typos or clarification are always welcome.
320 changes: 22 additions & 298 deletions src/15_global_state.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The three best approaches to global state are

1. Using the router to drive global state via the URL
2. Passing signals through context
3. Creating a global state struct and creating lenses into it with `create_slice`
3. Creating a global state struct using stores

## Option #1: URL as Global State

Expand All @@ -32,7 +32,7 @@ all its children and descendants using `provide_context`.
fn App() -> impl IntoView {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(0);
let (count, set_count) = signal(0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(count);
Expand Down Expand Up @@ -62,7 +62,7 @@ fn FancyMath() -> impl IntoView {
let count = use_context::<ReadSignal<u32>>()
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count() & 1 == 0;
let is_even = move || count.get() & 1 == 0;

view! {
<div class="consumer blue">
Expand All @@ -79,334 +79,58 @@ fn FancyMath() -> impl IntoView {
}
```

Note that this same pattern can be applied to more complex state. If you have multiple fields you want to update independently, you can do that by providing some struct of signals:
## Option #3: Create a Global State Store

```rust
#[derive(Copy, Clone, Debug)]
struct GlobalState {
count: RwSignal<i32>,
name: RwSignal<String>
}

impl GlobalState {
pub fn new() -> Self {
Self {
count: create_rw_signal(0),
name: create_rw_signal("Bob".to_string())
}
}
}

#[component]
fn App() -> impl IntoView {
provide_context(GlobalState::new());
Stores are a new reactive primitive, available in Leptos 0.7 through the accompanying `reactive_stores` crate. (This crate is shipped separately for now so we can continue to develop it without requiring a version change to the whole framework.)

// etc.
}
```
Stores allow you to wrap an entire struct, and reactively read from and update individual fields without tracking changes to other fields.

## Option #3: Create a Global State Struct and Slices

You may find it cumbersome to wrap each field of a structure in a separate signal like this. In some cases, it can be useful to create a plain struct with non-reactive fields, and then wrap that in a signal.
They are used by adding `#[derive(Store)]` onto a struct. (You can `use reactive_stores::Store;` to import the macro.) This creates an extension trait with a getter for each field of the struct, when the struct is wrapped in a `Store<_>`.

```rust
#[derive(Copy, Clone, Debug, Default)]
#[derive(Clone, Debug, Default, Store)]
struct GlobalState {
count: i32,
name: String
name: String,
}
```

This creates a trait named `GlobalStateStoreFields` which adds with methods `count` and `name` to a `Store<GlobalState>`. Each method returns a reactive store *field*.

```rust
#[component]
fn App() -> impl IntoView {
provide_context(create_rw_signal(GlobalState::default()));
provide_context(Store::new(GlobalState::default()));

// etc.
}
```

But there’s a problem: because our whole state is wrapped in one signal, updating the value of one field will cause reactive updates in parts of the UI that only depend on the other.

```rust
let state = expect_context::<RwSignal<GlobalState>>();
view! {
<button on:click=move |_| state.update(|state| state.count += 1)>"+1"</button>
<p>{move || state.with(|state| state.name.clone())}</p>
}
```

In this example, clicking the button will cause the text inside `<p>` to be updated, cloning `state.name` again! Because signals are the atomic unit of reactivity, updating any field of the signal triggers updates to everything that depends on the signal.

There’s a better way. You can take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates.

Here, instead of reading from the state signal directly, we create “slices” of that state with fine-grained updates via `create_slice`. Each slice signal only updates when the particular piece of the larger struct it accesses updates. This means you can create a single root signal, and then take independent, fine-grained slices of it in different components, each of which can update without notifying the others of changes.

```rust
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter() -> impl IntoView {
let state = expect_context::<RwSignal<GlobalState>>();
let state = expect_context::<Store<GlobalState>>();

// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(

// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
|state| state.count,
// our setter describes how to mutate that slice, given a new value
|state, n| state.count = n,
);
// this gives us reactive access to the `count` field only
let count = state.count();

view! {
<div class="consumer blue">
<button
on:click=move |_| {
set_count(count() + 1);
*count.write() += 1;
}
>
"Increment Global Count"
</button>
<br/>
<span>"Count is: " {count}</span>
<span>"Count is: " {move || count.get()}</span>
</div>
}
}
```

Clicking this button only updates `state.count`, so if we create another slice
somewhere else that only takes `state.name`, clicking the button won’t cause
that other slice to update. This allows you to combine the benefits of a top-down
Clicking this button only updates `state.count`. If we read from `state.name` somewhere else,
click the button won’t notify it. This allows you to combine the benefits of a top-down
data flow and of fine-grained reactive updates.

> **Note**: There are some significant drawbacks to this approach. Both signals and memos need to own their values, so a memo will need to clone the field’s value on every change. The most natural way to manage state in a framework like Leptos is always to provide signals that are as locally-scoped and fine-grained as they can be, not to hoist everything up into global state. But when you _do_ need some kind of global state, `create_slice` can be a useful tool.
```admonish sandbox title="Live example" collapsible=true
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2)
<noscript>
Please enable JavaScript to view examples.
</noscript>
<template>
<iframe src="https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
</template>
```

<details>
<summary>CodeSandbox Source</summary>

```rust
use leptos::*;

// So far, we've only been working with local state in components
// We've only seen how to communicate between parent and child components
// But there are also more general ways to manage global state
//
// The three best approaches to global state are
// 1. Using the router to drive global state via the URL
// 2. Passing signals through context
// 3. Creating a global state struct and creating lenses into it with `create_slice`
//
// Option #1: URL as Global State
// The next few sections of the tutorial will be about the router.
// So for now, we'll just look at options #2 and #3.

// Option #2: Pass Signals through Context
//
// In virtual DOM libraries like React, using the Context API to manage global
// state is a bad idea: because the entire app exists in a tree, changing
// some value provided high up in the tree can cause the whole app to render.
//
// In fine-grained reactive libraries like Leptos, this is simply not the case.
// You can create a signal in the root of your app and pass it down to other
// components using provide_context(). Changing it will only cause rerendering
// in the specific places it is actually used, not the whole app.
#[component]
fn Option2() -> impl IntoView {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(count);

view! {
<h1>"Option 2: Passing Signals"</h1>
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// These consumers can only read from it
// But we could give them write access by passing `set_count` if we wanted
<div style="display: flex">
<FancyMath/>
<ListItems/>
</div>
}
}

/// A button that increments our global counter.
#[component]
fn SetterButton(set_count: WriteSignal<u32>) -> impl IntoView {
view! {
<div class="provider red">
<button on:click=move |_| set_count.update(|count| *count += 1)>
"Increment Global Count"
</button>
</div>
}
}

/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath() -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>()
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count() & 1 == 0;

view! {
<div class="consumer blue">
"The number "
<strong>{count}</strong>
{move || if is_even() {
" is"
} else {
" is not"
}}
" even."
</div>
}
}

/// A component that shows a list of items generated from the global count.
#[component]
fn ListItems() -> impl IntoView {
// again, consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>().expect("there to be a `count` signal provided");

let squares = move || {
(0..count())
.map(|n| view! { <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
.collect::<Vec<_>>()
};

view! {
<div class="consumer green">
<ul>{squares}</ul>
</div>
}
}

// Option #3: Create a Global State Struct
//
// You can use this approach to build a single global data structure
// that holds the state for your whole app, and then access it by
// taking fine-grained slices using `create_slice` or `create_memo`,
// so that changing one part of the state doesn't cause parts of your
// app that depend on other parts of the state to change.

#[derive(Default, Clone, Debug)]
struct GlobalState {
count: u32,
name: String,
}

#[component]
fn Option3() -> impl IntoView {
// we'll provide a single signal that holds the whole state
// each component will be responsible for creating its own "lens" into it
let state = create_rw_signal(GlobalState::default());
provide_context(state);

view! {
<h1>"Option 3: Passing Signals"</h1>
<div class="red consumer" style="width: 100%">
<h2>"Current Global State"</h2>
<pre>
{move || {
format!("{:#?}", state.get())
}}
</pre>
</div>
<div style="display: flex">
<GlobalStateCounter/>
<GlobalStateInput/>
</div>
}
}

/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter() -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");

// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(

// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
|state| state.count,
// our setter describes how to mutate that slice, given a new value
|state, n| state.count = n,
);

view! {
<div class="consumer blue">
<button
on:click=move |_| {
set_count(count() + 1);
}
>
"Increment Global Count"
</button>
<br/>
<span>"Count is: " {count}</span>
</div>
}
}

/// A component that updates the count in the global state.
#[component]
fn GlobalStateInput() -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");

// this slice is completely independent of the `count` slice
// that we created in the other component
// neither of them will cause the other to rerun
let (name, set_name) = create_slice(
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
|state| state.name.clone(),
// our setter describes how to mutate that slice, given a new value
|state, n| state.name = n,
);

view! {
<div class="consumer green">
<input
type="text"
prop:value=name
on:input=move |ev| {
set_name(event_target_value(&ev));
}
/>
<br/>
<span>"Name is: " {name}</span>
</div>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
}
```

</details>
</preview>
Check out the [`stores` example](https://github.com/leptos-rs/leptos/blob/main/examples/stores/src/lib.rs) in the repo for a more extensive example.
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- [No Macros: The View Builder Syntax](./view/builder.md)
- [Reactivity](./reactivity/README.md)
- [Working with Signals](./reactivity/working_with_signals.md)
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
- [Responding to Changes with Effects](./reactivity/14_create_effect.md)
- [Interlude: Reactivity and Functions](./reactivity/interlude_functions.md)
- [Testing](./testing.md)
- [Async](./async/README.md)
Expand Down
Loading

0 comments on commit 96b169a

Please sign in to comment.