Skip to content

Commit

Permalink
Merge pull request #5 from nicoburniske/query_client_fixes
Browse files Browse the repository at this point in the history
Query Client Improvements
  • Loading branch information
nicoburniske authored Aug 5, 2023
2 parents 6897c94 + 51b7a3f commit d85c7ef
Show file tree
Hide file tree
Showing 11 changed files with 691 additions and 170 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
[![Crates.io](https://img.shields.io/crates/v/leptos_query.svg)](https://crates.io/crates/leptos_query)
[![docs.rs](https://docs.rs/leptos_query/badge.svg)](https://docs.rs/leptos_query)

<p align="center">
<a href="https://docs.rs/leptos_query">
<img src="https://raw.githubusercontent.com/nicoburniske/leptos_query/main/logo.svg" alt="Leptos Query" width="150"/>
</a>
</p>

## About

Leptos Query is a robust asynchronous state management library for [Leptos](https://github.com/leptos-rs/leptos), providing simplified data fetching, integrated reactivity, server-side rendering support, and intelligent cache management.
Expand Down Expand Up @@ -32,7 +38,7 @@ Leptos Query focuses on simplifying your data fetching process and keeping your
## Installation

```bash
cargo add leptos_query --optional
cargo add leptos_query
```

Then add the relevant feature(s) to your `Cargo.toml`
Expand Down
2 changes: 1 addition & 1 deletion example/start-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ thiserror = "1.0.38"
tracing = { version = "0.1.37", optional = true }
http = "0.2.8"
serde = "1.0.171"
leptos_query = { path = "../../", optional = true}
leptos_query = { path = "../../"}

[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", "leptos_query/hydrate"]
Expand Down
36 changes: 27 additions & 9 deletions example/start-axum/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::error_template::{AppError, ErrorTemplate};
use crate::{
error_template::{AppError, ErrorTemplate},
todo::InteractiveTodo,
};
use leptos::*;
use leptos_meta::*;
use leptos_query::*;
Expand Down Expand Up @@ -63,6 +66,12 @@ pub fn App(cx: Scope) -> impl IntoView {
view! { cx, <UniqueKey/> }
}
/>
<Route
path="todos"
view=|cx| {
view! { cx, <InteractiveTodo/> }
}
/>
</Route>
</Routes>
</main>
Expand Down Expand Up @@ -99,6 +108,9 @@ fn HomePage(cx: Scope) -> impl IntoView {
<li>
<a href="/unique">"Non-Dynamic Key"</a>
</li>
<li>
<a href="/todos">"Todos"</a>
</li>
</ul>
<br/>
<div
Expand All @@ -124,7 +136,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
fn use_post_query(
cx: Scope,
key: impl Fn() -> u32 + 'static,
) -> QueryResult<String, impl RefetchFn> {
) -> QueryResult<Option<String>, impl RefetchFn> {
use_query(
cx,
key,
Expand All @@ -139,8 +151,8 @@ fn use_post_query(
)
}

async fn get_post_unwrapped(id: u32) -> String {
get_post(id).await.expect("Post to exist")
async fn get_post_unwrapped(id: u32) -> Option<String> {
get_post(id).await.ok()
}

// Server function that fetches a post.
Expand Down Expand Up @@ -180,7 +192,7 @@ fn Post(cx: Scope, #[prop(into)] post_id: MaybeSignal<u32>) -> impl IntoView {
is_stale,
is_invalid,
refetch,
} = use_post_query(cx, post_id.clone());
} = use_post_query(cx, post_id);

create_effect(cx, move |_| log!("State: {:#?}", state.get()));

Expand Down Expand Up @@ -209,12 +221,18 @@ fn Post(cx: Scope, #[prop(into)] post_id: MaybeSignal<u32>) -> impl IntoView {
<Transition fallback=move || {
view! { cx, <h2>"Loading..."</h2> }
}>
{move || {
data.get()
<h2>
{
data
.get()
.map(|post| {
view! { cx, <h2>{post}</h2> }
match post {
Some(post) => post,
None => "Not Found".into(),
}
})
}}
}
</h2>
</Transition>
</div>
<div>
Expand Down
1 change: 1 addition & 0 deletions example/start-axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use cfg_if::cfg_if;
pub mod app;
pub mod error_template;
pub mod fileserv;
pub mod todo;

cfg_if! { if #[cfg(feature = "hydrate")] {
use leptos::*;
Expand Down
273 changes: 273 additions & 0 deletions example/start-axum/src/todo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use leptos::*;
use leptos_query::*;
use leptos_router::ActionForm;

use serde::*;
#[derive(Serialize, Deserialize, Clone)]
pub struct Todo {
id: u32,
content: String,
}

#[component]
pub fn InteractiveTodo(cx: Scope) -> impl IntoView {
view! { cx,
<div>
<div style:display="flex" style:gap="10rem">
<TodoWithResource/>
<TodoWithQuery/>
</div>
<AddTodoComponent/>
<AllTodos/>
</div>
}
}

#[component]
fn TodoWithResource(cx: Scope) -> impl IntoView {
let (todo_id, set_todo_id) = create_signal(cx, 0_u32);

// todo_id is a Signal<String>, and that is fed into the resource fetcher function.
// any time todo_id changes, the resource will re-execute.
let todo_resource: Resource<u32, TodoResponse> = create_resource(cx, todo_id, get_todo);

view! { cx,
<div
style:display="flex"
style:flex-direction="column"
style:justify-content="between"
style:align-items="center"
style:height="30vh"
>
<h2>"Todo with Resource"</h2>
<label>"Todo ID"</label>
<input
type="number"
on:input=move |ev| {
if let Ok(todo_id) = event_target_value(&ev).parse() {
set_todo_id(todo_id);
}
}
prop:value=todo_id
/>
<Transition fallback=move || {
view! { cx, <p>"Loading..."</p> }
}>
<p>
{move || {
todo_resource
.read(cx)
.map(|a| {
match a.ok().flatten() {
Some(todo) => todo.content,
None => "Not found".into(),
}
})
}}
</p>
</Transition>
</div>
}
}

#[component]
fn TodoWithQuery(cx: Scope) -> impl IntoView {
let (todo_id, set_todo_id) = create_signal(cx, 0_u32);

let QueryResult { data, .. } = use_query(cx, todo_id, get_todo, QueryOptions::default());

view! { cx,
<div
style:display="flex"
style:flex-direction="column"
style:justify-content="between"
style:align-items="center"
style:height="30vh"
>
<h2>"Todo with Query"</h2>
<label>"Todo ID"</label>
<input
type="number"
on:input=move |ev| {
if let Ok(todo_id) = event_target_value(&ev).parse() {
set_todo_id(todo_id);
}
}
prop:value=todo_id
/>
<Transition fallback=move || {
view! { cx, <p>"Loading..."</p> }
}>
<p>
{move || {
data.get()
.map(|a| {
match a.ok().flatten() {
Some(todo) => todo.content,
None => "Not found".into(),
}
})
}}
</p>
</Transition>
</div>
}
}

// When using this, you get a ton of hydration errors.
#[component]
fn TodoBody(cx: Scope, todo: Signal<Option<Option<Todo>>>) -> impl IntoView {
view! { cx,
<Transition fallback=move || {
view! { cx, <p>"Loading..."</p> }
}>
<p>
{move || {
todo.get()
.map(|a| {
match a {
Some(todo) => todo.content,
None => "Not found".into(),
}
})
}}
</p>
</Transition>
}
}

#[component]
fn AllTodos(cx: Scope) -> impl IntoView {
let QueryResult { data, refetch, .. } = use_query(
cx,
|| (),
|_| async move { get_todos().await.unwrap_or_default() },
QueryOptions::default(),
);

let todos: Signal<Vec<Todo>> = Signal::derive(cx, move || data.get().unwrap_or_default());

let delete_todo = create_action(cx, move |id: &u32| {
let id = *id;
let refetch = refetch.clone();
async move {
let _ = delete_todo(id).await;
refetch();
use_query_client(cx).invalidate_query::<u32, TodoResponse>(&id);
}
});

view! { cx,
<h2>"All Todos"</h2>
<Transition fallback=move || {
view! { cx, <p>"Loading..."</p> }
}>
<ul>
<Show
when=move || !todos.get().is_empty()
fallback=|cx| {
view! { cx, <p>"No todos"</p> }
}
>
<For
each=todos
key=|todo| todo.id
view=move |cx, todo| {
view! { cx,
<li>
<span>{todo.id}</span>
<span>": "</span>
<span>{todo.content}</span>
<span>" "</span>
<button on:click=move |_| delete_todo.dispatch(todo.id)>"X"</button>
</li>
}
}
/>
</Show>
</ul>
</Transition>
}
}

#[component]
fn AddTodoComponent(cx: Scope) -> impl IntoView {
let add_todo = create_server_action::<AddTodo>(cx);

let response = add_todo.value();

let client = use_query_client(cx);

create_effect(cx, move |_| {
// If action is successful.
if let Some(Ok(todo)) = response.get() {
let id = todo.id;
// Invalidate individual TodoResponse.
client.clone().invalidate_query::<u32, TodoResponse>(id);

// Invalidate AllTodos.
client.clone().invalidate_query::<(), Vec<Todo>>(());

// Optimistic update.
let as_response = Ok(Some(todo));
client.set_query_data::<u32, TodoResponse>(id, |_| Some(as_response));
}
});

view! { cx,
<ActionForm action=add_todo>
<label>"Add a Todo " <input type="text" name="content"/></label>
<input type="submit" autocomplete="off" value="Add"/>
</ActionForm>
}
}

cfg_if::cfg_if! {
if #[cfg(feature = "ssr")] {
use std::{sync::RwLock, time::Duration};
static GLOBAL_TODOS: RwLock<Vec<Todo>> = RwLock::new(vec![]);
}
}

// Read.

type TodoResponse = Result<Option<Todo>, ServerFnError>;

#[server(GetTodo, "/api")]
async fn get_todo(id: u32) -> Result<Option<Todo>, ServerFnError> {
tokio::time::sleep(Duration::from_millis(1000)).await;
let todos = GLOBAL_TODOS.read().unwrap();
Ok(todos.iter().find(|t| t.id == id).cloned())
}

#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
tokio::time::sleep(Duration::from_millis(1000)).await;
let todos = GLOBAL_TODOS.read().unwrap();
Ok(todos.clone())
}

// Mutate.

#[server(AddTodo, "/api")]
pub async fn add_todo(content: String) -> Result<Todo, ServerFnError> {
let mut todos = GLOBAL_TODOS.write().unwrap();

let new_id = todos.last().map(|t| t.id + 1).unwrap_or(0);

let new_todo = Todo {
id: new_id as u32,
content,
};

todos.push(new_todo.clone());

Ok(new_todo)
}

#[server(DeleteTodo, "/api")]
async fn delete_todo(id: u32) -> Result<(), ServerFnError> {
let mut todos = GLOBAL_TODOS.write().unwrap();
todos.retain(|t| t.id != id);
Ok(())
}
Loading

0 comments on commit d85c7ef

Please sign in to comment.