leptos_use/
sync_signal.rs

1use crate::core::UseRwSignal;
2use default_struct_builder::DefaultBuilder;
3use leptos::prelude::*;
4use std::{ops::Deref, rc::Rc};
5
6/// Two-way Signals synchronization.
7///
8/// > Note: Please consider first if you can achieve your goals with the
9/// > ["Good Options" described in the Leptos book](https://book.leptos.dev/reactivity/working_with_signals.html#making-signals-depend-on-each-other)
10/// > Only if you really have to, use this function. This is, in effect, the
11/// > ["If you really must..." option](https://book.leptos.dev/reactivity/working_with_signals.html#if-you-really-must).
12///
13/// ## Demo
14///
15/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/sync_signal)
16///
17/// ## Usage
18///
19/// ```
20/// # use leptos::prelude::*;
21/// # use leptos::logging::log;
22/// # use leptos_use::sync_signal;
23/// #
24/// # #[component]
25/// # fn Demo() -> impl IntoView {
26/// let (a, set_a) = signal(1);
27/// let (b, set_b) = signal(2);
28///
29/// let stop = sync_signal((a, set_a), (b, set_b));
30///
31/// log!("a: {}, b: {}", a.get(), b.get()); // a: 1, b: 1
32///
33/// set_b.set(3);
34///
35/// log!("a: {}, b: {}", a.get(), b.get()); // a: 3, b: 3
36///
37/// set_a.set(4);
38///
39/// log!("a: {}, b: {}", a.get(), b.get()); // a: 4, b: 4
40/// #
41/// # view! { }
42/// # }
43/// ```
44///
45/// ### `RwSignal`
46///
47/// You can mix and match `RwSignal`s and `Signal`-`WriteSignal` pairs.
48///
49/// ```
50/// # use leptos::prelude::*;
51/// # use leptos_use::sync_signal;
52/// #
53/// # #[component]
54/// # fn Demo() -> impl IntoView {
55/// let (a, set_a) = signal(1);
56/// let (b, set_b) = signal(2);
57/// let c_rw = RwSignal::new(3);
58/// let d_rw = RwSignal::new(4);
59///
60/// sync_signal((a, set_a), c_rw);
61/// sync_signal(d_rw, (b, set_b));
62/// sync_signal(c_rw, d_rw);
63///
64/// #
65/// # view! { }
66/// # }
67/// ```
68///
69/// ### One directional
70///
71/// You can synchronize a signal only from left to right or right to left.
72///
73/// ```
74/// # use leptos::prelude::*;
75/// # use leptos::logging::log;
76/// # use leptos_use::{sync_signal_with_options, SyncSignalOptions, SyncDirection};
77/// #
78/// # #[component]
79/// # fn Demo() -> impl IntoView {
80/// let (a, set_a) = signal(1);
81/// let (b, set_b) = signal(2);
82///
83/// let stop = sync_signal_with_options(
84///     (a, set_a),
85///     (b, set_b),
86///     SyncSignalOptions::default().direction(SyncDirection::LeftToRight)
87/// );
88///
89/// set_b.set(3); // doesn't sync
90///
91/// log!("a: {}, b: {}", a.get(), b.get()); // a: 1, b: 3
92///
93/// set_a.set(4);
94///
95/// log!("a: {}, b: {}", a.get(), b.get()); // a: 4, b: 4
96/// #
97/// # view! { }
98/// # }
99/// ```
100///
101/// ### Custom Transform
102///
103/// You can optionally provide custom transforms between the two signals.
104///
105/// ```
106/// # use leptos::prelude::*;
107/// # use leptos::logging::log;
108/// # use leptos_use::{sync_signal_with_options, SyncSignalOptions};
109/// #
110/// # #[component]
111/// # fn Demo() -> impl IntoView {
112/// let (a, set_a) = signal(10);
113/// let (b, set_b) = signal(2);
114///
115/// let stop = sync_signal_with_options(
116///     (a, set_a),
117///     (b, set_b),
118///     SyncSignalOptions::with_transforms(
119///         |left| *left * 2,
120///         |right| *right / 2,
121///     ),
122/// );
123///
124/// log!("a: {}, b: {}", a.get(), b.get()); // a: 10, b: 20
125///
126/// set_b.set(30);
127///
128/// log!("a: {}, b: {}", a.get(), b.get()); // a: 15, b: 30
129/// #
130/// # view! { }
131/// # }
132/// ```
133///
134/// #### Different Types
135///
136/// `SyncSignalOptions::default()` is only defined if the two signal types are identical.
137/// Otherwise, you have to initialize the options with `with_transforms` or `with_assigns` instead
138/// of `default`.
139///
140/// ```
141/// # use leptos::prelude::*;
142/// # use leptos_use::{sync_signal_with_options, SyncSignalOptions};
143/// # use std::str::FromStr;
144/// #
145/// # #[component]
146/// # fn Demo() -> impl IntoView {
147/// let (a, set_a) = signal("10".to_string());
148/// let (b, set_b) = signal(2);
149///
150/// let stop = sync_signal_with_options(
151///     (a, set_a),
152///     (b, set_b),
153///     SyncSignalOptions::with_transforms(
154///         |left: &String| i32::from_str(left).unwrap_or_default(),
155///         |right: &i32| right.to_string(),
156///     ),
157/// );
158/// #
159/// # view! { }
160/// # }
161/// ```
162///
163/// ```
164/// # use leptos::prelude::*;
165/// # use leptos_use::{sync_signal_with_options, SyncSignalOptions};
166/// # use std::str::FromStr;
167/// #
168/// #[derive(Clone)]
169/// pub struct Foo {
170///     bar: i32,
171/// }
172///
173/// # #[component]
174/// # fn Demo() -> impl IntoView {
175/// let (a, set_a) = signal(Foo { bar: 10 });
176/// let (b, set_b) = signal(2);
177///
178/// let stop = sync_signal_with_options(
179///     (a, set_a),
180///     (b, set_b),
181///     SyncSignalOptions::with_assigns(
182///         |b: &mut i32, a: &Foo| *b = a.bar,
183///         |a: &mut Foo, b: &i32| a.bar = *b,
184///     ),
185/// );
186/// #
187/// # view! { }
188/// # }
189/// ```
190///
191/// ## Server-Side Rendering
192///
193/// On the server the signals are not continuously synced. If the option `immediate` is `true`, the
194/// signals are synced once initially. If the option `immediate` is `false`, then this function
195/// does nothing.
196pub fn sync_signal<LeftRead, RightRead, LeftWrite, RightWrite, T>(
197    left: impl Into<UseRwSignal<LeftRead, LeftWrite, T>>,
198    right: impl Into<UseRwSignal<RightRead, RightWrite, T>>,
199) -> impl Fn() + Clone
200where
201    T: Clone + Send + Sync + 'static,
202
203    LeftRead: Read + ReadUntracked + Track + Copy + Send + Sync + 'static,
204    <LeftRead as Read>::Value: Deref<Target = T>,
205    <LeftRead as ReadUntracked>::Value: Deref<Target = T>,
206    LeftWrite: Write<Value = T> + Copy + 'static,
207
208    RightRead: Read + ReadUntracked + Track + Copy + Send + Sync + 'static,
209    <RightRead as Read>::Value: Deref<Target = T>,
210    <RightRead as ReadUntracked>::Value: Deref<Target = T>,
211    RightWrite: Write<Value = T> + Copy + 'static,
212{
213    sync_signal_with_options(left, right, SyncSignalOptions::<T, T>::default())
214}
215
216/// Version of [`sync_signal`] that takes a `SyncSignalOptions`. See [`sync_signal`] for how to use.
217pub fn sync_signal_with_options<LeftRead, RightRead, LeftWrite, RightWrite, Left, Right>(
218    left: impl Into<UseRwSignal<LeftRead, LeftWrite, Left>>,
219    right: impl Into<UseRwSignal<RightRead, RightWrite, Right>>,
220    options: SyncSignalOptions<Left, Right>,
221) -> impl Fn() + Clone
222where
223    Left: Clone + Send + Sync + 'static,
224    LeftRead: Read + ReadUntracked + Track + Copy + Send + Sync + 'static,
225    <LeftRead as Read>::Value: Deref<Target = Left>,
226    <LeftRead as ReadUntracked>::Value: Deref<Target = Left>,
227    LeftWrite: Write<Value = Left> + Copy + 'static,
228
229    Right: Clone + Send + Sync + 'static,
230    RightRead: Read + ReadUntracked + Track + Copy + Send + Sync + 'static,
231    <RightRead as Read>::Value: Deref<Target = Right>,
232    <RightRead as ReadUntracked>::Value: Deref<Target = Right>,
233    RightWrite: Write<Value = Right> + Copy + 'static,
234{
235    let SyncSignalOptions {
236        immediate,
237        direction,
238        transforms,
239    } = options;
240
241    let (assign_ltr, assign_rtl) = transforms.assigns();
242
243    let left = left.into();
244    let right = right.into();
245
246    let mut stop_watch_left = None;
247    let mut stop_watch_right = None;
248
249    let is_sync_update = StoredValue::new(false);
250
251    if matches!(direction, SyncDirection::Both | SyncDirection::LeftToRight) {
252        #[cfg(feature = "ssr")]
253        {
254            if immediate {
255                let assign_ltr = Rc::clone(&assign_ltr);
256                right.try_update(move |right| assign_ltr(right, &left.get_untracked()));
257            }
258        }
259
260        stop_watch_left = Some(Effect::watch(
261            move || left.get(),
262            move |new_value, _, _| {
263                if !is_sync_update.get_value() || !matches!(direction, SyncDirection::Both) {
264                    is_sync_update.set_value(true);
265                    right.try_update(|right| {
266                        assign_ltr(right, new_value);
267                    });
268                } else {
269                    is_sync_update.set_value(false);
270                }
271            },
272            immediate,
273        ));
274    }
275
276    if matches!(direction, SyncDirection::Both | SyncDirection::RightToLeft) {
277        #[cfg(feature = "ssr")]
278        {
279            if immediate && matches!(direction, SyncDirection::RightToLeft) {
280                let assign_rtl = Rc::clone(&assign_rtl);
281                left.try_update(move |left| assign_rtl(left, &right.get_untracked()));
282            }
283        }
284
285        stop_watch_right = Some(Effect::watch(
286            move || right.get(),
287            move |new_value, _, _| {
288                if !is_sync_update.get_value() || !matches!(direction, SyncDirection::Both) {
289                    is_sync_update.set_value(true);
290                    left.try_update(|left| assign_rtl(left, new_value));
291                } else {
292                    is_sync_update.set_value(false);
293                }
294            },
295            immediate,
296        ));
297    }
298
299    move || {
300        if let Some(stop_watch_left) = &stop_watch_left {
301            stop_watch_left.stop();
302        }
303        if let Some(stop_watch_right) = &stop_watch_right {
304            stop_watch_right.stop();
305        }
306    }
307}
308
309/// Direction of syncing.
310#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
311pub enum SyncDirection {
312    LeftToRight,
313    RightToLeft,
314    #[default]
315    Both,
316}
317
318pub type AssignFn<T, S> = Rc<dyn Fn(&mut T, &S)>;
319
320/// Transforms or assigns for syncing.
321pub enum SyncTransforms<L, R> {
322    /// Transform the signal into each other by calling the transform functions.
323    /// The values are then simply assigned.
324    Transforms {
325        /// Transforms the left signal into the right signal.
326        ltr: Rc<dyn Fn(&L) -> R>,
327        /// Transforms the right signal into the left signal.
328        rtl: Rc<dyn Fn(&R) -> L>,
329    },
330
331    /// Assign the signals to each other. Instead of using `=` to assign the signals,
332    /// these functions are called.
333    Assigns {
334        /// Assigns the left signal to the right signal.
335        ltr: AssignFn<R, L>,
336        /// Assigns the right signal to the left signal.
337        rtl: AssignFn<L, R>,
338    },
339}
340
341impl<T> Default for SyncTransforms<T, T>
342where
343    T: Clone,
344{
345    fn default() -> Self {
346        Self::Assigns {
347            ltr: Rc::new(|right, left| *right = left.clone()),
348            rtl: Rc::new(|left, right| *left = right.clone()),
349        }
350    }
351}
352
353impl<L, R> SyncTransforms<L, R>
354where
355    L: 'static,
356    R: 'static,
357{
358    /// Returns assign functions for both directions that respect the value of this enum.
359    pub fn assigns(&self) -> (AssignFn<R, L>, AssignFn<L, R>) {
360        match self {
361            SyncTransforms::Transforms { ltr, rtl } => {
362                let ltr = Rc::clone(ltr);
363                let rtl = Rc::clone(rtl);
364                (
365                    Rc::new(move |right, left| *right = ltr(left)),
366                    Rc::new(move |left, right| *left = rtl(right)),
367                )
368            }
369            SyncTransforms::Assigns { ltr, rtl } => (Rc::clone(ltr), Rc::clone(rtl)),
370        }
371    }
372}
373
374/// Options for [`sync_signal_with_options`].
375#[derive(DefaultBuilder)]
376pub struct SyncSignalOptions<L, R> {
377    /// If `true`, the signals will be immediately synced when this function is called.
378    /// If `false`, a signal is only updated when the other signal's value changes.
379    /// Defaults to `true`.
380    immediate: bool,
381
382    /// Direction of syncing. Defaults to `SyncDirection::Both`.
383    direction: SyncDirection,
384
385    /// How to transform or assign the values to each other
386    /// If `L` and `R` are identical this defaults to the simple `=` operator. If the types are
387    /// not the same, then you have to choose to either use [`SyncSignalOptions::with_transforms`]
388    /// or [`SyncSignalOptions::with_assigns`].
389    #[builder(skip)]
390    transforms: SyncTransforms<L, R>,
391}
392
393impl<L, R> SyncSignalOptions<L, R> {
394    /// Initializes options with transforms functions that convert the signals into each other.
395    pub fn with_transforms(
396        transform_ltr: impl Fn(&L) -> R + 'static,
397        transform_rtl: impl Fn(&R) -> L + 'static,
398    ) -> Self {
399        Self {
400            immediate: true,
401            direction: SyncDirection::Both,
402            transforms: SyncTransforms::Transforms {
403                ltr: Rc::new(transform_ltr),
404                rtl: Rc::new(transform_rtl),
405            },
406        }
407    }
408
409    /// Initializes options with assign functions that replace the default `=` operator.
410    pub fn with_assigns(
411        assign_ltr: impl Fn(&mut R, &L) + 'static,
412        assign_rtl: impl Fn(&mut L, &R) + 'static,
413    ) -> Self {
414        Self {
415            immediate: true,
416            direction: SyncDirection::Both,
417            transforms: SyncTransforms::Assigns {
418                ltr: Rc::new(assign_ltr),
419                rtl: Rc::new(assign_rtl),
420            },
421        }
422    }
423}
424
425impl<T> Default for SyncSignalOptions<T, T>
426where
427    T: Clone,
428{
429    fn default() -> Self {
430        Self {
431            immediate: true,
432            direction: Default::default(),
433            transforms: Default::default(),
434        }
435    }
436}