Remove context & job agent (#2295)

* Remove context and job agent.

* Revert "Remove context and job agent."

This reverts commit 408f6fcdf0.

* Revert "Revert "Remove context and job agent.""

This reverts commit 44a42dfb31.

* Update example.

* Fix docs.

* Fix test.

* Rename examples.

* Fix examples & docs.

* Update website/docs/concepts/function-components/custom-hooks.mdx

Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>

* Examples in alphabetical order.

Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
This commit is contained in:
Kaede Hoshikawa 2021-12-29 21:15:26 +09:00 committed by GitHub
parent b761487bac
commit 7d52858d01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 285 additions and 1315 deletions

View File

@ -8,7 +8,9 @@ members = [
"packages/yew-router-macro",
# Examples
"examples/agents",
"examples/boids",
"examples/contexts",
"examples/counter",
"examples/dyn_create_destroy_apps",
"examples/file_upload",
@ -19,14 +21,11 @@ members = [
"examples/js_callback",
"examples/keyed_list",
"examples/mount_point",
"examples/multi_thread",
"examples/nested_list",
"examples/node_refs",
"examples/password_strength",
"examples/portals",
"examples/pub_sub",
"examples/router",
"examples/store",
"examples/timer",
"examples/todomvc",
"examples/two_apps",

View File

@ -1,5 +1,5 @@
[package]
name = "multi_thread"
name = "agents"
version = "0.1.0"
authors = ["Denis Kolodin <deniskolodin@gmail.com>"]
edition = "2018"

18
examples/agents/README.md Normal file
View File

@ -0,0 +1,18 @@
# Warning
The agents example is a conceptual WIP and is currently blocked on [a future Trunk feature](https://github.com/thedodd/trunk/issues/46)
There is an alternate agent example [here](https://github.com/yewstack/yew/tree/master/examples/web_worker_fib).
# Multi-Thread Example
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fagents)](https://examples.yew.rs/agents)
## Concepts
Uses an [Agent] that runs in a [Web Worker].
[agent]: https://yew.rs/docs/concepts/agents/
[web worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

View File

@ -1,4 +1,4 @@
fn main() {
wasm_logger::init(wasm_logger::Config::default());
yew::start_app::<multi_thread::Model>();
yew::start_app::<agents::Model>();
}

View File

@ -1,4 +1,4 @@
use multi_thread::native_worker::Worker;
use agents::native_worker::Worker;
use yew_agent::Threaded;
fn main() {

View File

@ -0,0 +1,49 @@
pub mod native_worker;
use yew::{html, Component, Context, Html};
use yew_agent::{Bridge, Bridged};
pub enum Msg {
SendToWorker,
DataReceived,
}
pub struct Model {
worker: Box<dyn Bridge<native_worker::Worker>>,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
let link = ctx.link();
let callback = link.callback(|_| Msg::DataReceived);
let worker = native_worker::Worker::bridge(callback);
Self { worker }
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SendToWorker => {
self.worker.send(native_worker::Request::GetDataFromServer);
false
}
Msg::DataReceived => {
log::info!("DataReceived");
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<nav class="menu">
<button onclick={ctx.link().callback(|_| Msg::SendToWorker)}>{ "Send to Thread" }</button>
</nav>
</div>
}
}
}

View File

@ -1,5 +1,5 @@
[package]
name = "pub_sub"
name = "contexts"
version = "0.1.0"
edition = "2018"
license = "MIT OR Apache-2.0"

View File

@ -0,0 +1,10 @@
# Context Example
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fcontexts)](https://examples.yew.rs/contexts)
This is currently a technical demonstration of Context API.
## Concepts
The example has two components, which communicates through a context
as opposed to the traditional method using component links.

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew • Pub Sub</title>
<title>Yew • Context</title>
</head>
<body></body>

View File

@ -0,0 +1,23 @@
mod msg_ctx;
mod producer;
mod subscriber;
use producer::Producer;
use subscriber::Subscriber;
use yew::prelude::*;
use msg_ctx::MessageProvider;
#[function_component]
pub fn Model() -> Html {
html! {
<MessageProvider>
<Producer />
<Subscriber />
</MessageProvider>
}
}
fn main() {
yew::start_app::<Model>();
}

View File

@ -0,0 +1,37 @@
use std::rc::Rc;
use yew::prelude::*;
#[derive(Debug, PartialEq, Clone)]
pub struct Message {
pub inner: String,
}
impl Reducible for Message {
type Action = String;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
Message { inner: action }.into()
}
}
pub type MessageContext = UseReducerHandle<Message>;
#[derive(Properties, Debug, PartialEq)]
pub struct MessageProviderProps {
#[prop_or_default]
pub children: Children,
}
#[function_component]
pub fn MessageProvider(props: &MessageProviderProps) -> Html {
let msg = use_reducer(|| Message {
inner: "No message yet.".to_string(),
});
html! {
<ContextProvider<MessageContext> context={msg}>
{props.children.clone()}
</ContextProvider<MessageContext>>
}
}

View File

@ -0,0 +1,16 @@
use yew::prelude::*;
use super::msg_ctx::MessageContext;
#[function_component]
pub fn Producer() -> Html {
let msg_ctx = use_context::<MessageContext>().unwrap();
let onclick = Callback::from(move |_| msg_ctx.dispatch("Message Received.".to_string()));
html! {
<button {onclick}>
{"PRESS ME"}
</button>
}
}

View File

@ -0,0 +1,14 @@
use super::msg_ctx::MessageContext;
use yew::prelude::*;
#[function_component]
pub fn Subscriber() -> Html {
let msg_ctx = use_context::<MessageContext>().unwrap();
let message = msg_ctx.inner.to_owned();
html! {
<h1>{ message }</h1>
}
}

View File

@ -1,18 +0,0 @@
# Warning
The multi-thread example is a conceptual WIP and is currently blocked on [a future Trunk feature](https://github.com/thedodd/trunk/issues/46)
There is an alternate multi-thread example [here](https://github.com/yewstack/yew/tree/master/examples/web_worker_fib).
# Multi-Thread Example
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fmulti_thread)](https://examples.yew.rs/multi_thread)
## Concepts
Uses an [Agent] that runs in a [Web Worker].
[agent]: https://yew.rs/docs/concepts/agents/
[web worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

View File

@ -1,61 +0,0 @@
use gloo_timers::callback::Interval;
use serde::{Deserialize, Serialize};
use yew_agent::{Agent, AgentLink, Context, HandlerId};
#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
GetDataFromServer,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum Response {
DataFetched,
}
pub enum Msg {
Updating,
}
pub struct Worker {
link: AgentLink<Worker>,
_interval: Interval,
}
impl Agent for Worker {
type Reach = Context<Self>;
type Message = Msg;
type Input = Request;
type Output = Response;
fn create(link: AgentLink<Self>) -> Self {
let duration = 3;
let interval = {
let link = link.clone();
Interval::new(duration, move || link.send_message(Msg::Updating))
};
Self {
link,
_interval: interval,
}
}
fn update(&mut self, msg: Self::Message) {
match msg {
Msg::Updating => {
log::info!("Tick...");
}
}
}
fn handle_input(&mut self, msg: Self::Input, who: HandlerId) {
log::info!("Request: {:?}", msg);
match msg {
Request::GetDataFromServer => {
// TODO fetch actual data
self.link.respond(who, Response::DataFetched);
}
}
}
}

View File

@ -1,71 +0,0 @@
use gloo_timers::callback::Interval;
use serde::{Deserialize, Serialize};
use yew_agent::{Agent, AgentLink, HandlerId, Job};
#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
GetDataFromServer,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum Response {
DataFetched,
}
pub enum Msg {
Initialized,
Updating,
DataFetched,
}
pub struct Worker {
link: AgentLink<Worker>,
_interval: Interval,
}
impl Agent for Worker {
type Reach = Job<Self>;
type Message = Msg;
type Input = Request;
type Output = Response;
fn create(link: AgentLink<Self>) -> Self {
let duration = 3;
let interval = {
let link = link.clone();
Interval::new(duration, move || link.send_message(Msg::Updating))
};
link.send_message(Msg::Initialized);
Self {
link,
_interval: interval,
}
}
fn update(&mut self, msg: Self::Message) {
match msg {
Msg::Initialized => {
log::info!("Initialized!");
}
Msg::Updating => {
log::info!("Tick...");
}
Msg::DataFetched => {
log::info!("Data was fetched");
}
}
}
fn handle_input(&mut self, msg: Self::Input, who: HandlerId) {
log::info!("Request: {:?}", msg);
match msg {
Request::GetDataFromServer => {
// TODO fetch actual data
self.link.respond(who, Response::DataFetched);
self.link.send_message(Msg::DataFetched);
}
}
}
}

View File

@ -1,81 +0,0 @@
pub mod context;
pub mod job;
pub mod native_worker;
use yew::{html, Component, Context, Html};
use yew_agent::{Bridge, Bridged};
pub enum Msg {
SendToWorker,
SendToJob,
SendToContext,
DataReceived,
}
pub struct Model {
worker: Box<dyn Bridge<native_worker::Worker>>,
job: Box<dyn Bridge<job::Worker>>,
context: Box<dyn Bridge<context::Worker>>,
context_2: Box<dyn Bridge<context::Worker>>,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
let link = ctx.link();
let callback = link.callback(|_| Msg::DataReceived);
let worker = native_worker::Worker::bridge(callback);
let callback = link.callback(|_| Msg::DataReceived);
let job = job::Worker::bridge(callback);
let callback = link.callback(|_| Msg::DataReceived);
let context = context::Worker::bridge(callback);
let callback = link.callback(|_| Msg::DataReceived);
let context_2 = context::Worker::bridge(callback);
Self {
worker,
job,
context,
context_2,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SendToWorker => {
self.worker.send(native_worker::Request::GetDataFromServer);
false
}
Msg::SendToJob => {
self.job.send(job::Request::GetDataFromServer);
false
}
Msg::SendToContext => {
self.context.send(context::Request::GetDataFromServer);
self.context_2.send(context::Request::GetDataFromServer);
false
}
Msg::DataReceived => {
log::info!("DataReceived");
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<nav class="menu">
<button onclick={ctx.link().callback(|_| Msg::SendToWorker)}>{ "Send to Thread" }</button>
<button onclick={ctx.link().callback(|_| Msg::SendToJob)}>{ "Send to Job" }</button>
<button onclick={ctx.link().callback(|_| Msg::SendToContext)}>{ "Send to Context" }</button>
</nav>
</div>
}
}
}

View File

@ -1,17 +0,0 @@
# Pub Sub Example
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fpub_sub)](https://examples.yew.rs/pub_sub)
This is currently a technical demonstration of agents.
## Concepts
The example has two components, which communicate through a "broker" agent
as opposed to the traditional method using component links.
## Improvements
As it stands, this example uses a great amount of code to do very little.
The concept should be applied to a more elaborate use case.
This could also be merged into the [nested_list](../nested_list) example to remove the need for `WeakComponentLink`.

View File

@ -1,47 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use yew_agent::{Agent, AgentLink, Context, HandlerId};
#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
EventBusMsg(String),
}
pub struct EventBus {
link: AgentLink<EventBus>,
subscribers: HashSet<HandlerId>,
}
impl Agent for EventBus {
type Reach = Context<Self>;
type Message = ();
type Input = Request;
type Output = String;
fn create(link: AgentLink<Self>) -> Self {
Self {
link,
subscribers: HashSet::new(),
}
}
fn update(&mut self, _msg: Self::Message) {}
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
match msg {
Request::EventBusMsg(s) => {
for sub in self.subscribers.iter() {
self.link.respond(*sub, s.clone());
}
}
}
}
fn connected(&mut self, id: HandlerId) {
self.subscribers.insert(id);
}
fn disconnected(&mut self, id: HandlerId) {
self.subscribers.remove(&id);
}
}

View File

@ -1,31 +0,0 @@
mod event_bus;
mod producer;
mod subscriber;
use producer::Producer;
use subscriber::Subscriber;
use yew::{html, Component, Context, Html};
pub struct Model;
impl Component for Model {
type Message = ();
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<>
<Producer />
<Subscriber />
</>
}
}
}
fn main() {
yew::start_app::<Model>();
}

View File

@ -1,40 +0,0 @@
use crate::event_bus::{EventBus, Request};
use yew::prelude::*;
use yew_agent::{Dispatched, Dispatcher};
pub enum Msg {
Clicked,
}
pub struct Producer {
event_bus: Dispatcher<EventBus>,
}
impl Component for Producer {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {
event_bus: EventBus::dispatcher(),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Clicked => {
self.event_bus
.send(Request::EventBusMsg("Message received".to_owned()));
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<button onclick={ctx.link().callback(|_| Msg::Clicked)}>
{ "PRESS ME" }
</button>
}
}
}

View File

@ -1,39 +0,0 @@
use super::event_bus::EventBus;
use yew::{html, Component, Context, Html};
use yew_agent::{Bridge, Bridged};
pub enum Msg {
NewMessage(String),
}
pub struct Subscriber {
message: String,
_producer: Box<dyn Bridge<EventBus>>,
}
impl Component for Subscriber {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
Self {
message: "No message yet.".to_owned(),
_producer: EventBus::bridge(ctx.link().callback(Msg::NewMessage)),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::NewMessage(s) => {
self.message = s;
true
}
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<h1>{ &self.message }</h1>
}
}
}

View File

@ -1,18 +0,0 @@
[package]
name = "store"
version = "0.1.0"
authors = ["Michał Kawalec <michal@monad.cat>"]
edition = "2018"
license = "MIT OR Apache-2.0"
[dependencies]
yew = { path = "../../packages/yew" }
yew-agent = { path = "../../packages/yew-agent" }
wasm-bindgen = "0.2"
gloo-console = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"HtmlInputElement",
]

View File

@ -1,16 +0,0 @@
# Store Example
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fstore)](https://examples.yew.rs/store)
A timeline of posts that can be independently updated.
## Concepts
Uses the [`yew_agent`] API to keep track of posts.
## Improvements
- This example desperately needs some styling.
- Posts should persist across sessions.
[`yewtil::store`]: https://docs.rs/yew_agent/latest/

View File

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew • Store</title>
</head>
<body></body>
</html>

View File

@ -1 +0,0 @@
pub mod posts;

View File

@ -1,76 +0,0 @@
use std::collections::HashMap;
use yew_agent::utils::store::{Store, StoreWrapper};
use yew_agent::AgentLink;
pub type PostId = u32;
#[derive(Debug)]
pub enum PostRequest {
Create(String),
Update(PostId, String),
Remove(PostId),
}
#[derive(Debug)]
pub enum Action {
SetPost(Option<PostId>, String),
RemovePost(PostId),
}
pub struct PostStore {
pub posts: HashMap<PostId, String>,
// Stores can have private state too
id_counter: PostId,
}
impl Store for PostStore {
type Action = Action;
type Input = PostRequest;
fn new() -> Self {
let mut posts = HashMap::new();
// We insert one post to show the initial send of state
// when a bridge is opened.
posts.insert(0, "Magic first post".to_owned());
PostStore {
posts,
id_counter: 1,
}
}
fn handle_input(&self, link: AgentLink<StoreWrapper<Self>>, msg: Self::Input) {
match msg {
PostRequest::Create(text) => {
link.send_message(Action::SetPost(None, text));
}
PostRequest::Update(id, text) => {
link.send_message(Action::SetPost(Some(id), text));
}
PostRequest::Remove(id) => {
link.send_message(Action::RemovePost(id));
}
}
}
fn reduce(&mut self, msg: Self::Action) {
match msg {
Action::SetPost(id, text) => {
let id = id.unwrap_or_else(|| self.next_id());
self.posts.insert(id, text);
}
Action::RemovePost(id) => {
self.posts.remove(&id);
}
}
}
}
impl PostStore {
fn next_id(&mut self) -> PostId {
let tmp = self.id_counter;
self.id_counter += 1;
tmp
}
}

View File

@ -1,72 +0,0 @@
mod agents;
mod post;
mod text_input;
use agents::posts::{PostId, PostRequest, PostStore};
use gloo_console as console;
use post::Post;
use text_input::TextInput;
use yew::prelude::*;
use yew_agent::utils::store::{Bridgeable, ReadOnly, StoreWrapper};
use yew_agent::Bridge;
pub enum Msg {
CreatePost(String),
PostStoreMsg(ReadOnly<PostStore>),
}
pub struct Model {
post_ids: Vec<PostId>,
post_store: Box<dyn Bridge<StoreWrapper<PostStore>>>,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
let callback = ctx.link().callback(Msg::PostStoreMsg);
Self {
post_ids: Vec::new(),
post_store: PostStore::bridge(callback),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::CreatePost(text) => {
self.post_store.send(PostRequest::Create(text));
false
}
Msg::PostStoreMsg(state) => {
// We can see this is logged once before we click any button.
// The state of the store is sent when we open a bridge.
console::log!("Received update");
let state = state.borrow();
if state.posts.len() != self.post_ids.len() {
self.post_ids = state.posts.keys().copied().collect();
self.post_ids.sort_unstable();
true
} else {
false
}
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<TextInput value="New post" onsubmit={ctx.link().callback(Msg::CreatePost)} />
<div>
{ for self.post_ids.iter().map(|&id| html!{ <Post key={id} {id} /> }) }
</div>
</>
}
}
}
fn main() {
yew::start_app::<Model>();
}

View File

@ -1,85 +0,0 @@
use crate::agents::posts::{PostId, PostRequest, PostStore};
use crate::text_input::TextInput;
use yew::prelude::*;
use yew_agent::utils::store::{Bridgeable, ReadOnly, StoreWrapper};
use yew_agent::Bridge;
pub enum Msg {
UpdateText(String),
Delete,
PostStore(ReadOnly<PostStore>),
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
pub id: PostId,
}
pub struct Post {
id: PostId,
text: Option<String>,
post_store: Box<dyn Bridge<StoreWrapper<PostStore>>>,
}
impl Component for Post {
type Message = Msg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
let callback = ctx.link().callback(Msg::PostStore);
Self {
id: ctx.props().id,
text: None,
post_store: PostStore::bridge(callback),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::UpdateText(text) => {
self.post_store.send(PostRequest::Update(self.id, text));
false
}
Msg::Delete => {
self.post_store.send(PostRequest::Remove(self.id));
false
}
Msg::PostStore(state) => {
let state = state.borrow();
// Only update if the post changed.
if let Some(text) = state.posts.get(&self.id) {
if self.text.as_ref().map(|it| *it != *text).unwrap_or(false) {
self.text = Some(text.clone());
true
} else {
false
}
} else {
false
}
}
}
}
fn changed(&mut self, ctx: &Context<Self>) -> bool {
self.id = ctx.props().id;
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let text = self.text.as_deref().unwrap_or("<pending>");
html! {
<div>
<h2>{ format!("Post #{}", self.id) }</h2>
<p>{text}</p>
<TextInput value={text.to_owned()} onsubmit={ctx.link().callback(Msg::UpdateText)} />
<button onclick={ctx.link().callback(|_| Msg::Delete)}>
{ "Delete" }
</button>
</div>
}
}
}

View File

@ -1,63 +0,0 @@
use web_sys::HtmlInputElement;
use yew::prelude::*;
pub enum Msg {
Submit(String),
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
pub value: String,
pub onsubmit: Callback<String>,
}
pub struct TextInput {
text: String,
}
impl Component for TextInput {
type Message = Msg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
Self {
text: ctx.props().value.clone(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Submit(text) => {
ctx.props().onsubmit.emit(text);
true
}
}
}
fn changed(&mut self, ctx: &Context<Self>) -> bool {
self.text = ctx.props().value.clone();
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let onkeydown = ctx.link().batch_callback(|e: KeyboardEvent| {
e.stop_propagation();
if e.key() == "Enter" {
let input: HtmlInputElement = e.target_unchecked_into();
let value = input.value();
input.set_value("");
Some(Msg::Submit(value))
} else {
None
}
});
html! {
<input
placeholder={ctx.props().value.clone()}
type="text"
{onkeydown}
/>
}
}
}

View File

@ -2,15 +2,12 @@
mod hooks;
mod link;
mod local;
mod pool;
pub mod utils;
mod worker;
pub use hooks::{use_bridge, UseBridgeHandle};
pub use link::AgentLink;
pub(crate) use link::*;
pub use local::{Context, Job};
pub(crate) use pool::*;
pub use pool::{Dispatched, Dispatcher};
pub use worker::{Private, Public, Threaded};

View File

@ -1,141 +0,0 @@
use super::*;
use anymap2::{self, AnyMap};
use slab::Slab;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
use yew::callback::Callback;
use yew::scheduler::Shared;
thread_local! {
static LOCAL_AGENTS_POOL: RefCell<AnyMap> = RefCell::new(AnyMap::new());
}
/// Create a single instance in the current thread.
#[allow(missing_debug_implementations)]
pub struct Context<AGN> {
_agent: PhantomData<AGN>,
}
impl<AGN> Discoverer for Context<AGN>
where
AGN: Agent,
{
type Agent = AGN;
fn spawn_or_join(callback: Option<Callback<AGN::Output>>) -> Box<dyn Bridge<AGN>> {
let mut scope_to_init = None;
let bridge = LOCAL_AGENTS_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
match pool.entry::<LocalAgent<AGN>>() {
anymap2::Entry::Occupied(mut entry) => entry.get_mut().create_bridge(callback),
anymap2::Entry::Vacant(entry) => {
let scope = AgentScope::<AGN>::new();
let launched = LocalAgent::new(&scope);
let responder = SlabResponder {
slab: launched.slab(),
};
scope_to_init = Some((scope, responder));
entry.insert(launched).create_bridge(callback)
}
}
});
if let Some((scope, responder)) = scope_to_init {
let agent_link = AgentLink::connect(&scope, responder);
let upd = AgentLifecycleEvent::Create(agent_link);
scope.send(upd);
}
let upd = AgentLifecycleEvent::Connected(bridge.id);
bridge.scope.send(upd);
Box::new(bridge)
}
}
struct SlabResponder<AGN: Agent> {
slab: Shared<Slab<Option<Callback<AGN::Output>>>>,
}
impl<AGN: Agent> Responder<AGN> for SlabResponder<AGN> {
fn respond(&self, id: HandlerId, output: AGN::Output) {
locate_callback_and_respond::<AGN>(&self.slab, id, output);
}
}
impl<AGN: Agent> Dispatchable for Context<AGN> {}
struct ContextBridge<AGN: Agent> {
scope: AgentScope<AGN>,
id: HandlerId,
}
impl<AGN: Agent> Bridge<AGN> for ContextBridge<AGN> {
fn send(&mut self, msg: AGN::Input) {
let upd = AgentLifecycleEvent::Input(msg, self.id);
self.scope.send(upd);
}
}
impl<AGN: Agent> Drop for ContextBridge<AGN> {
fn drop(&mut self) {
let terminate_worker = LOCAL_AGENTS_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
let terminate_worker = {
if let Some(launched) = pool.get_mut::<LocalAgent<AGN>>() {
launched.remove_bridge(self)
} else {
false
}
};
if terminate_worker {
pool.remove::<LocalAgent<AGN>>();
}
terminate_worker
});
let upd = AgentLifecycleEvent::Disconnected(self.id);
self.scope.send(upd);
if terminate_worker {
let upd = AgentLifecycleEvent::Destroy;
self.scope.send(upd);
}
}
}
struct LocalAgent<AGN: Agent> {
scope: AgentScope<AGN>,
slab: SharedOutputSlab<AGN>,
}
impl<AGN: Agent> LocalAgent<AGN> {
pub fn new(scope: &AgentScope<AGN>) -> Self {
let slab = Rc::new(RefCell::new(Slab::new()));
LocalAgent {
scope: scope.clone(),
slab,
}
}
fn slab(&self) -> SharedOutputSlab<AGN> {
self.slab.clone()
}
fn create_bridge(&mut self, callback: Option<Callback<AGN::Output>>) -> ContextBridge<AGN> {
let respondable = callback.is_some();
let mut slab = self.slab.borrow_mut();
let id: usize = slab.insert(callback);
let id = HandlerId::new(id, respondable);
ContextBridge {
scope: self.scope.clone(),
id,
}
}
fn remove_bridge(&mut self, bridge: &ContextBridge<AGN>) -> Last {
let mut slab = self.slab.borrow_mut();
let _ = slab.remove(bridge.id.raw_id());
slab.is_empty()
}
}

View File

@ -1,62 +0,0 @@
use super::*;
use std::marker::PhantomData;
use yew::callback::Callback;
const SINGLETON_ID: HandlerId = HandlerId(0, true);
/// Create an instance in the current thread.
#[allow(missing_debug_implementations)]
pub struct Job<AGN> {
_agent: PhantomData<AGN>,
}
impl<AGN> Discoverer for Job<AGN>
where
AGN: Agent,
{
type Agent = AGN;
fn spawn_or_join(callback: Option<Callback<AGN::Output>>) -> Box<dyn Bridge<AGN>> {
let callback = callback.expect("Callback required for Job");
let scope = AgentScope::<AGN>::new();
let responder = CallbackResponder { callback };
let agent_link = AgentLink::connect(&scope, responder);
let upd = AgentLifecycleEvent::Create(agent_link);
scope.send(upd);
let upd = AgentLifecycleEvent::Connected(SINGLETON_ID);
scope.send(upd);
let bridge = JobBridge { scope };
Box::new(bridge)
}
}
struct JobBridge<AGN: Agent> {
scope: AgentScope<AGN>,
}
impl<AGN: Agent> Bridge<AGN> for JobBridge<AGN> {
fn send(&mut self, msg: AGN::Input) {
let upd = AgentLifecycleEvent::Input(msg, SINGLETON_ID);
self.scope.send(upd);
}
}
impl<AGN: Agent> Drop for JobBridge<AGN> {
fn drop(&mut self) {
let upd = AgentLifecycleEvent::Disconnected(SINGLETON_ID);
self.scope.send(upd);
let upd = AgentLifecycleEvent::Destroy;
self.scope.send(upd);
}
}
struct CallbackResponder<AGN: Agent> {
callback: Callback<AGN::Output>,
}
impl<AGN: Agent> Responder<AGN> for CallbackResponder<AGN> {
fn respond(&self, id: HandlerId, output: AGN::Output) {
assert_eq!(id.raw_id(), SINGLETON_ID.raw_id());
self.callback.emit(output);
}
}

View File

@ -1,7 +0,0 @@
mod context;
mod job;
use super::*;
pub use context::Context;
pub use job::Job;

View File

@ -1 +0,0 @@
pub mod store;

View File

@ -1,162 +0,0 @@
use crate::{Agent, AgentLink, Bridge, Context, Discoverer, Dispatched, Dispatcher, HandlerId};
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::Deref;
use std::rc::Rc;
use yew::prelude::*;
/// A functional state wrapper, enforcing a unidirectional
/// data flow and consistent state to the observers.
///
/// `handle_input` receives incoming messages from components,
/// `reduce` applies changes to the state
///
/// The state is sent once whenever a bridge is opened and then once
/// for each `Action` sent by the `handle_input` function. This means
/// the initial state of the store must be valid for the consumers.
///
/// Once created with a first bridge, a Store will never be destroyed
/// for the lifetime of the application.
pub trait Store: Sized + 'static {
/// Messages instructing the store to do somethin
type Input;
/// State updates to be consumed by `reduce`
type Action;
/// Create a new Store
fn new() -> Self;
/// Receives messages from components and other agents. Use the `link`
/// to send actions to itself in order to notify `reduce` once your
/// operation completes. This is the place to do side effects, like
/// talking to the server, or asking the user for input.
///
/// Note that you can look at the state of your Store, but you
/// cannot modify it here. If you want to modify it, send a Message
/// to the reducer
fn handle_input(&self, link: AgentLink<StoreWrapper<Self>>, msg: Self::Input);
/// A pure function, with no side effects. Receives a message,
/// and applies it to the state as it sees fit.
fn reduce(&mut self, msg: Self::Action);
}
/// Hides the full context Agent from a Store and does
/// the boring data wrangling logic
#[derive(Debug)]
pub struct StoreWrapper<S: Store> {
/// Currently subscribed components and agents
pub handlers: HashSet<HandlerId>,
/// Link to itself so Store::handle_input can send actions to reducer
pub link: AgentLink<Self>,
/// The actual Store
pub state: Shared<S>,
/// A circular dispatcher to itself so the store is not removed
pub self_dispatcher: Dispatcher<Self>,
}
type Shared<T> = Rc<RefCell<T>>;
/// A wrapper ensuring state observers can only
/// borrow the state immutably
#[derive(Debug)]
pub struct ReadOnly<S> {
state: Shared<S>,
}
impl<S> ReadOnly<S> {
/// Allow only immutable borrows to the underlying data
pub fn borrow(&self) -> impl Deref<Target = S> + '_ {
self.state.borrow()
}
}
/// This is a wrapper, intended to be used as an opaque
/// machinery allowing the Store to do it's things.
impl<S: Store> Agent for StoreWrapper<S> {
type Reach = Context<Self>;
type Message = S::Action;
type Input = S::Input;
type Output = ReadOnly<S>;
fn create(link: AgentLink<Self>) -> Self {
let state = Rc::new(RefCell::new(S::new()));
let handlers = HashSet::new();
// Link to self to never go out of scope
let self_dispatcher = Self::dispatcher();
StoreWrapper {
handlers,
link,
state,
self_dispatcher,
}
}
fn update(&mut self, msg: Self::Message) {
{
self.state.borrow_mut().reduce(msg);
}
for handler in self.handlers.iter() {
self.link.respond(
*handler,
ReadOnly {
state: self.state.clone(),
},
);
}
}
fn connected(&mut self, id: HandlerId) {
self.handlers.insert(id);
self.link.respond(
id,
ReadOnly {
state: self.state.clone(),
},
);
}
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
self.state.borrow().handle_input(self.link.clone(), msg);
}
fn disconnected(&mut self, id: HandlerId) {
self.handlers.remove(&id);
}
}
// This instance is quite unfortunate, as the Rust compiler
// does not support mutually exclusive trait bounds (https://github.com/rust-lang/rust/issues/51774),
// we have to create a new trait with the same function as in the original one.
/// Allows us to communicate with a store
pub trait Bridgeable: Sized + 'static {
/// A wrapper for the store we want to bridge to,
/// which serves as a communication intermediary
type Wrapper: Agent;
/// Creates a messaging bridge between a worker and the component.
fn bridge(
callback: Callback<<Self::Wrapper as Agent>::Output>,
) -> Box<dyn Bridge<Self::Wrapper>>;
}
/// Implementation of bridge creation
impl<T> Bridgeable for T
where
T: Store,
{
/// The hiding wrapper
type Wrapper = StoreWrapper<T>;
fn bridge(
callback: Callback<<Self::Wrapper as Agent>::Output>,
) -> Box<dyn Bridge<Self::Wrapper>> {
<Self::Wrapper as Agent>::Reach::spawn_or_join(Some(callback))
}
}

View File

@ -11,8 +11,7 @@ yew-agent = { path = "../../packages/yew-agent/" }
[dev-dependencies]
boolinator = "2.4"
derive_more = "0.99"
gloo-events = "0.1"
gloo-utils = "0.1"
gloo = "0.5"
js-sys = "0.3"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

View File

@ -1,50 +0,0 @@
//! Agent types that compile to be used by website code snippets
use yew_agent::{Agent, AgentLink, Context, HandlerId};
pub struct EventBus;
impl Agent for EventBus {
type Reach = Context<Self>;
type Message = ();
type Input = ();
type Output = String;
fn create(_link: AgentLink<Self>) -> Self {
Self
}
fn update(&mut self, _msg: Self::Message) {
// impl
}
fn handle_input(&mut self, _msg: Self::Input, _id: HandlerId) {
// impl
}
}
pub enum WorkerMsg {
Process,
}
pub struct MyWorker;
impl Agent for MyWorker {
type Reach = Context<Self>;
type Message = ();
type Input = WorkerMsg;
type Output = ();
fn create(_link: AgentLink<Self>) -> Self {
Self
}
fn update(&mut self, _msg: Self::Message) {
// impl
}
fn handle_input(&mut self, _msg: Self::Input, _id: HandlerId) {
// impl
}
}

View File

@ -1,4 +1,3 @@
pub mod agents;
pub mod tutorial;
include!(concat!(env!("OUT_DIR"), "/website_tests.rs"));

View File

@ -26,7 +26,7 @@ pub struct ModalProps {
#[function_component(Modal)]
fn modal(props: &ModalProps) -> Html {
let modal_host = gloo_utils::document()
let modal_host = gloo::utils::document()
.get_element_by_id("modal_host")
.expect("a #modal_host element");

View File

@ -6,13 +6,7 @@ description: "Yew's Actor System"
import useBaseUrl from "@docusaurus/useBaseUrl";
import ThemedImage from '@theme/ThemedImage';
Agents are similar to Angular's [Services](https://angular.io/guide/architecture-services)
\(but without dependency injection\), and provide Yew with an
[Actor Model](https://en.wikipedia.org/wiki/Actor_model). Agents can be used to route messages
between components independently of where they sit in the component hierarchy, or they can be used
to create shared state between different components. Agents can also be used to offload
computationally expensive tasks from the main thread which renders the UI. There is also planned
support for using agents to allow Yew applications to communicate across tabs \(in the future\).
Agents are a way to offload tasks to web workers or achieve inter-tab communication(WIP).
In order for agents to run concurrently, Yew uses
[web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
@ -37,19 +31,14 @@ The code can be found in the <desc> tag of the svgs.
### Reaches
* Context - There will exist at most one instance of a Context Agent at any given time. Bridges will
spawn or connect to an already spawned agent on the UI thread. This can be used to coordinate
state between components or other agents. When no bridges are connected to this agent, the agent
will disappear.
* Public - There will exist at most one instance of a Public Agent at any given time. Bridges will
spawn or connect to an already spawned agent in a web worker.
When no bridges are connected to this agent, the agent will disappear.
* Job - Spawn a new agent on the UI thread for every new bridge. This is good for moving shared but
* Private - Spawn a new agent in a web worker for every new bridge. This is good for moving shared but
independent behavior that communicates with the browser out of components. \(TODO verify\) When
the task is done, the agent will disappear.
* Public - Same as Context, but runs on its own web worker.
* Private - Same as Job, but runs on its own web worker.
* Global \(WIP\)
## Communication between Agents and Components
@ -66,13 +55,11 @@ A dispatcher allows uni-directional communication between a component and an age
## Overhead
Agents that use web workers \(i.e. Private and Public\) will incur a serialization overhead on the
messages they send and receive. They use [bincode](https://github.com/servo/bincode) to communicate
with other threads, so the cost is substantially higher than just calling a function. Unless the
cost of computation will outweigh the cost of message passing, you should use agents running on the
UI thread \(i.e. Job or Context\).
Agents use web workers \(i.e. Private and Public\). They incur a serialization overhead on the
messages they send and receive. Agents use [bincode](https://github.com/servo/bincode) to communicate
with other threads, so the cost is substantially higher than just calling a function.
## Further reading
* The [pub\_sub](https://github.com/yewstack/yew/tree/master/examples/pub_sub) example shows how
components can use agents to communicate with each other.
* The [multi\_thread](https://github.com/yewstack/yew/tree/master/examples/multi_thread) example shows how
components can send message to and receive message from agents.

View File

@ -5,84 +5,114 @@ description: "Defining your own Hooks "
## Defining custom Hooks
Component's stateful logic can be extracted into usable function by creating custom Hooks.
Component's stateful logic can be extracted into usable function by creating custom Hooks
Consider that we wish to create an event listener that listens to an event on the `window`
object.
Consider that we have a component which subscribes to an agent and displays the messages sent to it.
```rust
use yew::{function_component, html, use_effect, use_state, Callback};
use yew_agent::Bridged;
// EventBus is an implementation yew_agent::Agent
use website_test::agents::EventBus;
use yew::prelude::*;
use gloo::events::EventListener;
use gloo::utils::window;
use std::mem::drop;
#[function_component(ShowMessages)]
pub fn show_messages() -> Html {
let state = use_state(Vec::new);
#[function_component(ShowStorageChanged)]
pub fn show_storage_changed() -> Html {
let state_storage_changed = use_state(|| false);
{
let state = state.clone();
use_effect(move || {
let producer = EventBus::bridge(Callback::from(move |msg| {
let mut messages = (*state).clone();
messages.push(msg);
state.set(messages)
}));
let state_storage_changed = state_storage_changed.clone();
use_effect(|| {
let listener = EventListener::new(&window(), "storage", move |_| state_storage_changed.set(true));
|| drop(producer)
move || { drop(listener); }
});
}
let output = state.iter().map(|it| html! { <p>{ it }</p> });
html! { <div>{ for output }</div> }
html! { <div>{"Storage Event Fired: "}{*state_storage_changed}</div> }
}
```
There's one problem with this code: the logic can't be reused by another component.
If we build another component which keeps track of the messages, instead of copying the code we can move the logic into a custom hook.
If we build another component which keeps track of the an event,
instead of copying the code we can move the logic into a custom hook.
We'll start by creating a new function called `use_subscribe`.
We'll start by creating a new function called `use_event`.
The `use_` prefix conventionally denotes that a function is a hook.
This function will take no arguments and return `Rc<RefCell<Vec<String>>>`.
This function will take an event target, a event type and a callback.
```rust
use std::{cell::RefCell, rc::Rc};
use web_sys::{Event, EventTarget};
use std::borrow::Cow;
use gloo::events::EventListener;
fn use_subscribe() -> Rc<RefCell<Vec<String>>> {
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
where
E: Into<Cow<'static, str>>,
F: Fn(&Event) + 'static,
{
todo!()
}
```
This is a simple hook which can be created by combining other hooks. For this example, we'll two pre-defined hooks.
We'll use `use_state` hook to store the `Vec` for messages, so they persist between component re-renders.
We'll also use `use_effect` to subscribe to the `EventBus` `Agent` so the subscription can be tied to component's lifecycle.
This is a simple hook which can be created by using built-in hooks. For this example, we'll use the `use_effect_with_deps` hook,
which subscribes to the dependencies so an event listener can be recreated when hook arguments change.
```rust
use std::collections::HashSet;
use yew::{use_effect, use_state, Callback};
use yew_agent::Bridged;
// EventBus is an implementation yew_agent::Agent
use website_test::agents::EventBus;
use yew::prelude::*;
use web_sys::{Event, EventTarget};
use std::borrow::Cow;
use std::rc::Rc;
use gloo::events::EventListener;
fn use_subscribe() -> Vec<String> {
let state = use_state(Vec::new);
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
where
E: Into<Cow<'static, str>>,
F: Fn(&Event) + 'static,
{
#[derive(Clone)]
struct EventDependents {
target: EventTarget,
event_type: Cow<'static, str>,
callback: Rc<dyn Fn(&Event)>,
}
let effect_state = state.clone();
#[allow(clippy::vtable_address_comparisons)]
impl PartialEq for EventDependents {
fn eq(&self, rhs: &Self) -> bool {
self.target == rhs.target
&& self.event_type == rhs.event_type
&& Rc::ptr_eq(&self.callback, &rhs.callback)
}
}
use_effect(move || {
let producer = EventBus::bridge(Callback::from(move |msg| {
let mut messages = (*effect_state).clone();
messages.push(msg);
effect_state.set(messages)
}));
|| drop(producer)
});
let deps = EventDependents {
target: target.clone(),
event_type: event_type.into(),
callback: Rc::new(callback) as Rc<dyn Fn(&Event)>,
};
(*state).clone()
use_effect_with_deps(
|deps| {
let EventDependents {
target,
event_type,
callback,
} = deps.clone();
let listener = EventListener::new(&target, event_type, move |e| {
callback(e);
});
move || {
drop(listener);
}
},
deps,
);
}
```
Although this approach works in almost all cases, it can't be used to write primitive hooks like the pre-defined hooks we've been using already
Although this approach works in almost all cases, it can't be used to write primitive hooks like the pre-defined hooks we've been using already.
### Writing primitive hooks
`use_hook` function is used to write such hooks. View the docs on [docs.rs](https://docs.rs/yew/0.18.0/yew-functional/use_hook.html) for the documentation
and `hooks` directory to see implementations of pre-defined hooks.
View the docs on [docs.rs](https://docs.rs/yew) for documentation and `hooks` directory to see implementations of pre-defined hooks.

View File

@ -68,36 +68,26 @@ This hook requires the state object to implement `PartialEq`.
`use_ref` is used for obtaining an immutable reference to a value.
Its state persists across renders.
`use_ref` can be useful for keeping things in scope for the lifetime of the component, so long as
`use_ref` can be useful for keeping things in scope for the lifetime of the component, so long as
you don't store a clone of the resulting `Rc` anywhere that outlives the component.
If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref).
If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state).
```rust
// EventBus is an implementation of yew_agent::Agent
use website_test::agents::EventBus;
use yew::{function_component, html, use_ref, use_state, Callback};
use yew_agent::Bridged;
#[function_component(UseRef)]
fn ref_hook() -> Html {
let greeting = use_state(|| "No one has greeted me yet!".to_owned());
{
let greeting = greeting.clone();
use_ref(|| EventBus::bridge(Callback::from(move |msg| {
greeting.set(msg);
})));
}
fn ref_hook() -> Html {
let message = use_ref(|| "Some Expensive State.".to_string());
html! {
<div>
<span>{ (*greeting).clone() }</span>
<span>{ (*message).clone() }</span>
</div>
}
}
```
```
## `use_mut_ref`
`use_mut_ref` is used for obtaining a mutable reference to a value.
@ -122,7 +112,7 @@ fn mut_ref_hook() -> Html {
let message_count = use_mut_ref(|| 0);
let onclick = Callback::from(move |_| {
let window = gloo_utils::window();
let window = gloo::utils::window();
if *message_count.borrow_mut() > 3 {
window.alert_with_message("Message limit reached").unwrap();
@ -316,10 +306,10 @@ fn effect() -> Html {
let counter = counter.clone();
use_effect(move || {
// Make a call to DOM API after component is rendered
gloo_utils::document().set_title(&format!("You clicked {} times", *counter));
gloo::utils::document().set_title(&format!("You clicked {} times", *counter));
// Perform the cleanup
|| gloo_utils::document().set_title("You clicked 0 times")
|| gloo::utils::document().set_title("You clicked 0 times")
});
}
let onclick = {

View File

@ -17,7 +17,7 @@ used as a `Html` value using `VRef`:
```rust
use web_sys::{Element, Node};
use yew::{Component, Context, html, Html};
use gloo_utils::document;
use gloo::utils::document;
struct Comp;
@ -161,45 +161,6 @@ impl Component for MyComponent {
}
}
}
```
</TabItem>
<TabItem value="Agent Handler" label="Agent Handler">
```rust
use yew::{html, Component, Context, Html};
use yew_agent::{Dispatcher, Dispatched};
use website_test::agents::{MyWorker, WorkerMsg};
struct MyComponent {
worker: Dispatcher<MyWorker>,
}
impl Component for MyComponent {
type Message = WorkerMsg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
MyComponent {
worker: MyWorker::dispatcher(),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
self.worker.send(msg);
false
}
fn view(&self, ctx: &Context<Self>) -> Html {
// Create a callback from a worker to handle it in another context
let click_callback = ctx.link().callback(|_| WorkerMsg::Process);
html! {
<button onclick={click_callback}>
{ "Click me!" }
</button>
}
}
}
```
</TabItem>

View File

@ -630,7 +630,7 @@ use yew::{
Component, Context, Html, NodeRef,
};
use gloo_events::EventListener;
use gloo::events::EventListener;
pub struct Comp {
my_div: NodeRef,
@ -693,4 +693,4 @@ component is about to be destroyed as the `EventListener` has a `drop` implement
which will remove the event listener from the element.
For more information on `EventListener`, see the
[gloo_events docs.rs](https://docs.rs/gloo-events/0.1.1/gloo_events/struct.EventListener.html).
[gloo_events docs.rs](https://docs.rs/gloo-events/0.1.1/gloo_events/struct.EventListener.html).

View File

@ -0,0 +1,9 @@
---
title: "From 0.1.0 to 0.2.0"
---
The `Context` and `Job` Agents have been removed in favour of Yew's Context API.
You can see the updated [`pub_sub`](https://github.com/yewstack/yew/tree/master/examples/pub_sub) example about how to use Context API.
For users of `yew_agent::utils::store`, you may switch to third party solutions like: [Yewdux](https://github.com/intendednull/yewdux) or [Bounce](https://github.com/futursolo/bounce).