<Transition />

#async-bridge

Official documentation

A transition is like suspense with one key difference; if the resources used in the children are updating (not initially loading), the current ui will stay in place until new UI can be created when the updated resource is ready. Transition also provides a pending or "updating" state that we can set to display loading messages.

Let's create a little example where we can add to a list of items. This is a pretty common pattern of update a parameter with interaction, query new data, and update the ui with the new data.

The async function

To start, we'll make our async function.

Our function will need a delay so that we can simulate network latency. We'll add a crate to our project by typing the following in the terminal when in the rust project folder:

cargo add futures_timer

We can now use futures_timer to provide a WASM friendly async delay. Using standard thread sleeping would sleep the main thread of the application, including its async runtime. We don't want that.

Our function will accept a number and generate an array of numbers from 1 up to the count, so the return type will be a Vec of numbers.

We'll also add a delay from the futures_timer crate and await it so that the time passes before we return with our new array of enumerated items.

#![allow(unused)]
fn main() {
async fn pretend_external_list_items(count: u32) -> Vec<u32> {  

    futures_timer::Delay::new( 
	    std::time::Duration::from_secs(1)
	).await;  
	
    (1..=count).collect()  
}
}

The component

Now we'll create our example component:

#![allow(unused)]
fn main() {
#[component]  
fn TransitionExample(cx: Scope) -> impl IntoView {  
	  
}    
}

We'll setup a resource like we did in the server functions tutorial.

#![allow(unused)]
fn main() {
#[component]  
fn TransitionExample(cx: Scope) -> impl IntoView {  
	
	// we create a signal that stores the size of the list 
    let (list_size, set_list_size) = create_signal(cx, 1_u32); 

	// we use the size as arguments for our 
	// function as a resource
    let list_items = create_resource(
	    cx, 
		list_size, 
		pretend_external_list_items
	);
}
}

We'll now add a view to our component that uses our resource. We'll start with a <Transition> tag and add a fallback view, just like with <Suspense> items. Recall that Transition is like <Suspence> but with glitter.

<Transition>s children will be set to a view that contains a <For /> tag to loop/iterate over the resource's value. The async function returns an option so we'll need to read() it and then upwrap_or_default(). Then for the view we create output for each item as an HTML list item.

#![allow(unused)]
fn main() {
#[component]  
fn TransitionExample(cx: Scope) -> impl IntoView {  
	
	let (list_size, set_list_size) = create_signal(cx, 1_u32); 

	let list_items = create_resource(
	    cx, 
		list_size, 
		pretend_external_list_items
	);
	
	view!{cx,  
	    <Transition  
	        fallback=move||view!{cx, <p>"Loading"</p>}  
		> 
			{view!{cx,  
	            <ul>  
	                <For  
	                    each=move||list_items.read().unwrap_or_default()  
	                    key=move|item|item.clone()  
	                    view=move|item|view!{cx,<li>{item}</li>}  
	                />            
				</ul>  
		        }       
			}    
		</Transition>  
	}
}
}

To make this more interactive we'll add a button that increments the list size:

#![allow(unused)]
fn main() {
<button on:click=move|_| set_list_size.set( list_size.get() + 1 ) >  
    "Add an item"  
</button>
}

And we'll add a header that shows us how big the list should be. This is a nice indicate of expectation on action, showing the delay between the header being updated and the list growing.

#![allow(unused)]
fn main() {
	<h2>"A list of " {list_size} </h2>
}

When assembled we're left with the following component:

#![allow(unused)]
fn main() {
```rust
#[component]  
fn TransitionExample(cx: Scope) -> impl IntoView {  
	
	let (list_size, set_list_size) = create_signal(cx, 1_u32); 

	let list_items = create_resource(
	    cx, 
		list_size, 
		pretend_external_list_items
	);
	
	view!{cx,  
		<h2>"A list of " {list_size} </h2>
		<button on:click=move|_| set_list_size.set( list_size.get() + 1 ) >  
		    "Add an item"  
		</button>
	    <Transition  
	        fallback=move||view!{cx, <p>"Loading"</p>}  
		> 
			{view!{cx,  
	            <ul>  
	                <For  
	                    each=move||list_items.read().unwrap_or_default()  
	                    key=move|item|item.clone()  
	                    view=move|item|view!{cx,<li>{item}</li>}  
	                />            
				</ul>  
		        }       
			}    
		</Transition>  
	}
}
}

The last piece of this example is adding a signal to hold the boolean value for if the transition component is updating (by default it won't be because it'll be loading):

#![allow(unused)]
fn main() {
let (is_list_updating, set_is_list_updating) = create_signal(cx, false);
}

And we'll update the <Transition> component, setting the property to accept the signal.

#![allow(unused)]
fn main() {
<Transition  
    fallback=move||view!{cx, <p>"Loading"</p>}  
    set_pending=set_is_list_updating.into()  
>
}

The above into() call on set_is_list_updating might look weird to you. The optional property set_pending accepts a SignalSetter<bool>. We know that set_is_list_updating is a WriteSignal<bool>. An implementation is written allowing us to turn the WriteSignal into the SignalSetter, which we can perform with the into() method.

We'll use our hand <Show> Leptos UI tag to conditionally display an updating message with a fallback of an empty view.

#![allow(unused)]
fn main() {
<Show  
    when=move || is_list_updating.get()  
    fallback=|_| ""  
>  
    <p><i>"Updating the list"</i></p>  
</Show>
}

When all is said and done we're left with a nice clear example of all of the great features wrapped up in <Transition>, showcasing how it works with some of Leptos' event handlers, signals, and UI tags.

#![allow(unused)]
fn main() {
use leptos::*;  
  
#[component]  
pub fn App(cx: Scope) -> impl IntoView {  
    view! {  
        cx,  
        <TransitionExample/>  
    }}  
  
#[component]  
fn TransitionExample(cx: Scope) -> impl IntoView {  
  
    let (list_size, set_list_size) = create_signal(cx, 1_u32);  
    let list_items = create_resource(cx, list_size, pretend_external_list_items);  
  
    let (is_list_updating, set_is_list_updating) = create_signal(cx, false);  
  
    view!{cx,  
        <h2>"A list of " {list_size} </h2>  
  
        <button on:click=move|_| set_list_size.set( list_size.get() + 1 ) >  
            "Add an item"  
        </button>  
  
        <Show  
            when=move || is_list_updating.get()  
            fallback=|_| ""  
        >  
            <p><i>"Updating the list"</i></p>  
        </Show>  
  
        <Transition  
            fallback=move||view!{cx, <p>"Loading"</p>}  
            set_pending=set_is_list_updating.into()  
        >           
			{ view!{cx,  
                <ul>  
                    <For  
                        each=move||list_items.read().unwrap_or_default()  
                        key=move|item|item.clone()  
                        view=move|item|view!{cx,<li>{item}</li>}  
                    />
				</ul>  
	            }
			}        
		</Transition>  
    }  
}  
  
async fn pretend_external_list_items(count: u32) -> Vec<u32> {  
    futures_timer::Delay::new( std::time::Duration::from_secs(1)).await;  
    (1..=count).collect()  
}
}