mirror of
https://github.com/leptos-rs/book.git
synced 2025-12-27 10:01:41 -05:00
585 lines
35 KiB
HTML
585 lines
35 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="light" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>Global State Management</title>
|
||
|
||
|
||
<!-- Custom HTML head -->
|
||
|
||
<meta name="description" content="">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#ffffff">
|
||
|
||
<link rel="icon" href="favicon.svg">
|
||
<link rel="shortcut icon" href="favicon.png">
|
||
<link rel="stylesheet" href="css/variables.css">
|
||
<link rel="stylesheet" href="css/general.css">
|
||
<link rel="stylesheet" href="css/chrome.css">
|
||
<link rel="stylesheet" href="css/print.css" media="print">
|
||
|
||
<!-- Fonts -->
|
||
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||
<link rel="stylesheet" href="fonts/fonts.css">
|
||
|
||
<!-- Highlight.js Stylesheets -->
|
||
<link rel="stylesheet" href="highlight.css">
|
||
<link rel="stylesheet" href="tomorrow-night.css">
|
||
<link rel="stylesheet" href="ayu-highlight.css">
|
||
|
||
<!-- Custom theme stylesheets -->
|
||
<link rel="stylesheet" href="./mdbook-admonish.css">
|
||
|
||
</head>
|
||
<body class="sidebar-visible no-js">
|
||
<div id="body-container">
|
||
<!-- Provide site root to javascript -->
|
||
<script>
|
||
var path_to_root = "";
|
||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||
</script>
|
||
|
||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||
<script>
|
||
try {
|
||
var theme = localStorage.getItem('mdbook-theme');
|
||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||
|
||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||
}
|
||
|
||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||
}
|
||
} catch (e) { }
|
||
</script>
|
||
|
||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||
<script>
|
||
var theme;
|
||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||
var html = document.querySelector('html');
|
||
html.classList.remove('light')
|
||
html.classList.add(theme);
|
||
var body = document.querySelector('body');
|
||
body.classList.remove('no-js')
|
||
body.classList.add('js');
|
||
</script>
|
||
|
||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||
|
||
<!-- Hide / unhide sidebar before it is displayed -->
|
||
<script>
|
||
var body = document.querySelector('body');
|
||
var sidebar = null;
|
||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||
if (document.body.clientWidth >= 1080) {
|
||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||
sidebar = sidebar || 'visible';
|
||
} else {
|
||
sidebar = 'hidden';
|
||
}
|
||
sidebar_toggle.checked = sidebar === 'visible';
|
||
body.classList.remove('sidebar-visible');
|
||
body.classList.add("sidebar-" + sidebar);
|
||
</script>
|
||
|
||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||
<div class="sidebar-scrollbox">
|
||
<ol class="chapter"><li class="chapter-item expanded "><a href="01_introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="getting_started/index.html"><strong aria-hidden="true">2.</strong> Getting Started</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="getting_started/leptos_dx.html"><strong aria-hidden="true">2.1.</strong> Leptos DX</a></li><li class="chapter-item expanded "><a href="getting_started/community_crates.html"><strong aria-hidden="true">2.2.</strong> The Leptos Community and leptos-* Crates</a></li></ol></li><li class="chapter-item expanded "><a href="view/index.html"><strong aria-hidden="true">3.</strong> Part 1: Building User Interfaces</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="view/01_basic_component.html"><strong aria-hidden="true">3.1.</strong> A Basic Component</a></li><li class="chapter-item expanded "><a href="view/02_dynamic_attributes.html"><strong aria-hidden="true">3.2.</strong> Dynamic Attributes</a></li><li class="chapter-item expanded "><a href="view/03_components.html"><strong aria-hidden="true">3.3.</strong> Components and Props</a></li><li class="chapter-item expanded "><a href="view/04_iteration.html"><strong aria-hidden="true">3.4.</strong> Iteration</a></li><li class="chapter-item expanded "><a href="view/04b_iteration.html"><strong aria-hidden="true">3.5.</strong> Iterating over More Complex Data</a></li><li class="chapter-item expanded "><a href="view/05_forms.html"><strong aria-hidden="true">3.6.</strong> Forms and Inputs</a></li><li class="chapter-item expanded "><a href="view/06_control_flow.html"><strong aria-hidden="true">3.7.</strong> Control Flow</a></li><li class="chapter-item expanded "><a href="view/07_errors.html"><strong aria-hidden="true">3.8.</strong> Error Handling</a></li><li class="chapter-item expanded "><a href="view/08_parent_child.html"><strong aria-hidden="true">3.9.</strong> Parent-Child Communication</a></li><li class="chapter-item expanded "><a href="view/09_component_children.html"><strong aria-hidden="true">3.10.</strong> Passing Children to Components</a></li><li class="chapter-item expanded "><a href="view/builder.html"><strong aria-hidden="true">3.11.</strong> No Macros: The View Builder Syntax</a></li></ol></li><li class="chapter-item expanded "><a href="reactivity/index.html"><strong aria-hidden="true">4.</strong> Reactivity</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="reactivity/working_with_signals.html"><strong aria-hidden="true">4.1.</strong> Working with Signals</a></li><li class="chapter-item expanded "><a href="reactivity/14_create_effect.html"><strong aria-hidden="true">4.2.</strong> Responding to Changes with create_effect</a></li><li class="chapter-item expanded "><a href="reactivity/interlude_functions.html"><strong aria-hidden="true">4.3.</strong> Interlude: Reactivity and Functions</a></li></ol></li><li class="chapter-item expanded "><a href="testing.html"><strong aria-hidden="true">5.</strong> Testing</a></li><li class="chapter-item expanded "><a href="async/index.html"><strong aria-hidden="true">6.</strong> Async</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="async/10_resources.html"><strong aria-hidden="true">6.1.</strong> Loading Data with Resources</a></li><li class="chapter-item expanded "><a href="async/11_suspense.html"><strong aria-hidden="true">6.2.</strong> Suspense</a></li><li class="chapter-item expanded "><a href="async/12_transition.html"><strong aria-hidden="true">6.3.</strong> Transition</a></li><li class="chapter-item expanded "><a href="async/13_actions.html"><strong aria-hidden="true">6.4.</strong> Actions</a></li></ol></li><li class="chapter-item expanded "><a href="interlude_projecting_children.html"><strong aria-hidden="true">7.</strong> Interlude: Projecting Children</a></li><li class="chapter-item expanded "><a href="15_global_state.html" class="active"><strong aria-hidden="true">8.</strong> Global State Management</a></li><li class="chapter-item expanded "><a href="router/index.html"><strong aria-hidden="true">9.</strong> Router</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="router/16_routes.html"><strong aria-hidden="true">9.1.</strong> Defining <Routes/></a></li><li class="chapter-item expanded "><a href="router/17_nested_routing.html"><strong aria-hidden="true">9.2.</strong> Nested Routing</a></li><li class="chapter-item expanded "><a href="router/18_params_and_queries.html"><strong aria-hidden="true">9.3.</strong> Params and Queries</a></li><li class="chapter-item expanded "><a href="router/19_a.html"><strong aria-hidden="true">9.4.</strong> <A/></a></li><li class="chapter-item expanded "><a href="router/20_form.html"><strong aria-hidden="true">9.5.</strong> <Form/></a></li></ol></li><li class="chapter-item expanded "><a href="interlude_styling.html"><strong aria-hidden="true">10.</strong> Interlude: Styling</a></li><li class="chapter-item expanded "><a href="metadata.html"><strong aria-hidden="true">11.</strong> Metadata</a></li><li class="chapter-item expanded "><a href="csr_wrapping_up.html"><strong aria-hidden="true">12.</strong> Client-Side Rendering: Wrapping Up</a></li><li class="chapter-item expanded "><a href="ssr/index.html"><strong aria-hidden="true">13.</strong> Part 2: Server Side Rendering</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ssr/21_cargo_leptos.html"><strong aria-hidden="true">13.1.</strong> cargo-leptos</a></li><li class="chapter-item expanded "><a href="ssr/22_life_cycle.html"><strong aria-hidden="true">13.2.</strong> The Life of a Page Load</a></li><li class="chapter-item expanded "><a href="ssr/23_ssr_modes.html"><strong aria-hidden="true">13.3.</strong> Async Rendering and SSR “Modes”</a></li><li class="chapter-item expanded "><a href="ssr/24_hydration_bugs.html"><strong aria-hidden="true">13.4.</strong> Hydration Bugs</a></li></ol></li><li class="chapter-item expanded "><a href="server/index.html"><strong aria-hidden="true">14.</strong> Working with the Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="server/25_server_functions.html"><strong aria-hidden="true">14.1.</strong> Server Functions</a></li><li class="chapter-item expanded "><a href="server/26_extractors.html"><strong aria-hidden="true">14.2.</strong> Extractors</a></li><li class="chapter-item expanded "><a href="server/27_response.html"><strong aria-hidden="true">14.3.</strong> Responses and Redirects</a></li></ol></li><li class="chapter-item expanded "><a href="progressive_enhancement/index.html"><strong aria-hidden="true">15.</strong> Progressive Enhancement and Graceful Degradation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="progressive_enhancement/action_form.html"><strong aria-hidden="true">15.1.</strong> <ActionForm/>s</a></li></ol></li><li class="chapter-item expanded "><a href="deployment/index.html"><strong aria-hidden="true">16.</strong> Deployment</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="deployment/csr.html"><strong aria-hidden="true">16.1.</strong> Deploying CSR Apps</a></li><li class="chapter-item expanded "><a href="deployment/ssr.html"><strong aria-hidden="true">16.2.</strong> Deploying SSR Apps</a></li><li class="chapter-item expanded "><a href="deployment/binary_size.html"><strong aria-hidden="true">16.3.</strong> Optimizing WASM Binary Size</a></li></ol></li><li class="chapter-item expanded "><a href="islands.html"><strong aria-hidden="true">17.</strong> Guide: Islands</a></li><li class="chapter-item expanded "><a href="appendix_reactive_graph.html"><strong aria-hidden="true">18.</strong> Appendix: How Does the Reactive System Work?</a></li></ol>
|
||
</div>
|
||
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||
</nav>
|
||
|
||
<!-- Track and set sidebar scroll position -->
|
||
<script>
|
||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||
sidebarScrollbox.addEventListener('click', function(e) {
|
||
if (e.target.tagName === 'A') {
|
||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||
}
|
||
}, { passive: true });
|
||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||
sessionStorage.removeItem('sidebar-scroll');
|
||
if (sidebarScrollTop) {
|
||
// preserve sidebar scroll position when navigating via links within sidebar
|
||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||
} else {
|
||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||
var activeSection = document.querySelector('#sidebar .active');
|
||
if (activeSection) {
|
||
activeSection.scrollIntoView({ block: 'center' });
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div id="page-wrapper" class="page-wrapper">
|
||
|
||
<div class="page">
|
||
<div id="menu-bar-hover-placeholder"></div>
|
||
<div id="menu-bar" class="menu-bar sticky">
|
||
<div class="left-buttons">
|
||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||
<i class="fa fa-bars"></i>
|
||
</label>
|
||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||
<i class="fa fa-paint-brush"></i>
|
||
</button>
|
||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||
</ul>
|
||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||
<i class="fa fa-search"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<h1 class="menu-title"></h1>
|
||
|
||
<div class="right-buttons">
|
||
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||
<i id="print-button" class="fa fa-print"></i>
|
||
</a>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div id="search-wrapper" class="hidden">
|
||
<form id="searchbar-outer" class="searchbar-outer">
|
||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||
</form>
|
||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||
<div id="searchresults-header" class="searchresults-header"></div>
|
||
<ul id="searchresults">
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||
<script>
|
||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||
});
|
||
</script>
|
||
|
||
<div id="content" class="content">
|
||
<main>
|
||
<h1 id="global-state-management"><a class="header" href="#global-state-management">Global State Management</a></h1>
|
||
<p>So far, we've only been working with local state in components, and we’ve seen how to coordinate state between parent and child components. On occasion, there are times where people look for a more general solution for global state management that can work throughout an application.</p>
|
||
<p>In general, <strong>you do not need this chapter.</strong> The typical pattern is to compose your application out of components, each of which manages its own local state, not to store all state in a global structure. However, there are some cases (like theming, saving user settings, or sharing data between components in different parts of your UI) in which you may want to use some kind of global state management.</p>
|
||
<p>The three best approaches to global state are</p>
|
||
<ol>
|
||
<li>Using the router to drive global state via the URL</li>
|
||
<li>Passing signals through context</li>
|
||
<li>Creating a global state struct and creating lenses into it with <code>create_slice</code></li>
|
||
</ol>
|
||
<h2 id="option-1-url-as-global-state"><a class="header" href="#option-1-url-as-global-state">Option #1: URL as Global State</a></h2>
|
||
<p>In many ways, the URL is actually the best way to store global state. It can be accessed from any component, anywhere in your tree. There are native HTML elements like <code><form></code> and <code><a></code> that exist solely to update the URL. And it persists across page reloads and between devices; you can share a URL with a friend or send it from your phone to your laptop and any state stored in it will be replicated.</p>
|
||
<p>The next few sections of the tutorial will be about the router, and we’ll get much more into these topics.</p>
|
||
<p>But for now, we'll just look at options #2 and #3.</p>
|
||
<h2 id="option-2-passing-signals-through-context"><a class="header" href="#option-2-passing-signals-through-context">Option #2: Passing Signals through Context</a></h2>
|
||
<p>In the section on <a href="view/08_parent_child.html">parent-child communication</a>, we saw that you can use <code>provide_context</code> to pass signal from a parent component to a child, and <code>use_context</code> to read it in the child. But <code>provide_context</code> works across any distance. If you want to create a global signal that holds some piece of state, you can provide it and access it via context anywhere in the descendants of the component where you provide it.</p>
|
||
<p>A signal provided via context only causes reactive updates where it is read, not in any of the components in between, so it maintains the power of fine-grained reactive updates, even at a distance.</p>
|
||
<p>We start by creating a signal in the root of the app and providing it to
|
||
all its children and descendants using <code>provide_context</code>.</p>
|
||
<pre><code class="language-rust">#[component]
|
||
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);
|
||
// we'll pass the setter to specific components,
|
||
// but provide the count itself to the whole app via context
|
||
provide_context(count);
|
||
|
||
view! {
|
||
// 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
|
||
<FancyMath/>
|
||
<ListItems/>
|
||
}
|
||
}</code></pre>
|
||
<p><code><SetterButton/></code> is the kind of counter we’ve written several times now.
|
||
(See the sandbox below if you don’t understand what I mean.)</p>
|
||
<p><code><FancyMath/></code> and <code><ListItems/></code> both consume the signal we’re providing via
|
||
<code>use_context</code> and do something with it.</p>
|
||
<pre><code class="language-rust">/// 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>
|
||
}
|
||
}</code></pre>
|
||
<p>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:</p>
|
||
<pre><code class="language-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());
|
||
|
||
// etc.
|
||
}</code></pre>
|
||
<h2 id="option-3-create-a-global-state-struct-and-slices"><a class="header" href="#option-3-create-a-global-state-struct-and-slices">Option #3: Create a Global State Struct and Slices</a></h2>
|
||
<p>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.</p>
|
||
<pre><code class="language-rust">#[derive(Copy, Clone, Debug, Default)]
|
||
struct GlobalState {
|
||
count: i32,
|
||
name: String
|
||
}
|
||
|
||
#[component]
|
||
fn App() -> impl IntoView {
|
||
provide_context(create_rw_signal(GlobalState::default()));
|
||
|
||
// etc.
|
||
}</code></pre>
|
||
<p>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.</p>
|
||
<pre><code class="language-rust">let state = expect_context::<RwSignal<GlobalState>>();
|
||
view! {
|
||
<button on:click=move |_| state.update(|n| *n += 1)>"+1"</button>
|
||
<p>{move || state.with(|state| state.name.clone())}</p>
|
||
}</code></pre>
|
||
<p>In this example, clicking the button will cause the text inside <code><p></code> to be updated, cloning <code>state.name</code> again! Because signals are the atomic unit of reactivity, updating any field of the signal triggers updates to everything that depends on the signal.</p>
|
||
<p>There’s a better way. You can take fine-grained, reactive slices by using <a href="https://docs.rs/leptos/latest/leptos/fn.create_memo.html"><code>create_memo</code></a> or <a href="https://docs.rs/leptos/latest/leptos/fn.create_slice.html"><code>create_slice</code></a> (which uses <code>create_memo</code> 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.</p>
|
||
<p>Here, instead of reading from the state signal directly, we create “slices” of that state with fine-grained updates via <code>create_slice</code>. 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.</p>
|
||
<pre><code class="language-rust">/// A component that updates the count in the global state.
|
||
#[component]
|
||
fn GlobalStateCounter() -> impl IntoView {
|
||
let state = expect_context::<RwSignal<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,
|
||
);
|
||
|
||
view! {
|
||
<div class="consumer blue">
|
||
<button
|
||
on:click=move |_| {
|
||
set_count(count() + 1);
|
||
}
|
||
>
|
||
"Increment Global Count"
|
||
</button>
|
||
<br/>
|
||
<span>"Count is: " {count}</span>
|
||
</div>
|
||
}
|
||
}</code></pre>
|
||
<p>Clicking this button only updates <code>state.count</code>, so if we create another slice
|
||
somewhere else that only takes <code>state.name</code>, clicking the button won’t cause
|
||
that other slice to update. This allows you to combine the benefits of a top-down
|
||
data flow and of fine-grained reactive updates.</p>
|
||
<blockquote>
|
||
<p><strong>Note</strong>: 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 <em>do</em> need some kind of global state, <code>create_slice</code> can be a useful tool.</p>
|
||
</blockquote>
|
||
<p><a href="https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2">Click to open CodeSandbox.</a></p>
|
||
<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>
|
||
<details>
|
||
<summary>CodeSandbox Source</summary>
|
||
<pre><code class="language-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/> })
|
||
}</code></pre>
|
||
</details>
|
||
</preview>
|
||
|
||
</main>
|
||
|
||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
<!-- Mobile navigation buttons -->
|
||
<a rel="prev" href="interlude_projecting_children.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="router/index.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
|
||
<div style="clear: both"></div>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||
<a rel="prev" href="interlude_projecting_children.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="router/index.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
</nav>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
|
||
<script>
|
||
window.playground_copyable = true;
|
||
</script>
|
||
|
||
|
||
<script src="elasticlunr.min.js"></script>
|
||
<script src="mark.min.js"></script>
|
||
<script src="searcher.js"></script>
|
||
|
||
<script src="clipboard.min.js"></script>
|
||
<script src="highlight.js"></script>
|
||
<script src="book.js"></script>
|
||
|
||
<!-- Custom JS scripts -->
|
||
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|