sycamore_reactive/
iter.rs

1//! Reactive utilities for dealing with lists and iterables.
2
3use std::collections::HashMap;
4use std::hash::Hash;
5use std::mem;
6
7use crate::*;
8
9/// Function that maps a `Vec` to another `Vec` via a map function and a key.
10///
11/// The mapped `Vec` is lazily computed, meaning that it's value will only be updated when
12/// requested. Modifications to the input `Vec` are diffed using keys to prevent recomputing values
13/// that have not changed.
14///
15/// This function is the underlying utility behind `Keyed`.
16///
17/// # Params
18/// * `list` - The list to be mapped. The list must be a [`ReadSignal`] (obtained from a [`Signal`])
19///   and therefore reactive.
20/// * `map_fn` - A closure that maps from the input type to the output type.
21/// * `key_fn` - A closure that returns an _unique_ key to each entry.
22///
23///  _Credits: Based on TypeScript implementation in <https://github.com/solidjs/solid>_
24pub fn map_keyed<T, K, U>(
25    list: impl Into<MaybeDyn<Vec<T>>> + 'static,
26    mut map_fn: impl FnMut(T) -> U + 'static,
27    key_fn: impl Fn(&T) -> K + 'static,
28) -> ReadSignal<Vec<U>>
29where
30    T: PartialEq + Clone + 'static,
31    K: Eq + Hash,
32    U: Clone,
33{
34    let list = list.into();
35    // Previous state used for diffing.
36    let mut items = Vec::new();
37
38    let mut mapped: Vec<U> = Vec::new();
39    let mut mapped_tmp: Vec<Option<U>> = Vec::new();
40
41    let mut disposers: Vec<Option<NodeHandle>> = Vec::new();
42    let mut disposers_tmp: Vec<Option<NodeHandle>> = Vec::new();
43
44    // Diff and update signal each time list is updated.
45    let _list = list.clone();
46    let mut update = move || {
47        let new_items = _list.get_clone();
48        if new_items.is_empty() {
49            // Fast path for removing all items.
50            for dis in mem::take(&mut disposers) {
51                dis.unwrap().dispose();
52            }
53            mapped = Vec::new();
54        } else if items.is_empty() {
55            // Fast path for new create.
56            mapped.reserve(new_items.len());
57            disposers.reserve(new_items.len());
58
59            for new_item in new_items.iter().cloned() {
60                let map_fn = &mut map_fn;
61                let mapped = &mut mapped;
62                let new_disposer = create_child_scope(move || mapped.push(map_fn(new_item)));
63                disposers.push(Some(new_disposer));
64            }
65        } else {
66            mapped_tmp.clear();
67            mapped_tmp.resize(new_items.len(), None);
68
69            disposers_tmp.clear();
70            disposers_tmp.resize_with(new_items.len(), || None);
71
72            // Skip common prefix.
73            let min_len = usize::min(items.len(), new_items.len());
74            let start = items
75                .iter()
76                .zip(new_items.iter())
77                .position(|(a, b)| a != b)
78                .unwrap_or(min_len);
79            debug_assert!(
80                (items.get(start).is_none() && new_items.get(start).is_none())
81                    || (items.get(start) != new_items.get(start)),
82                "start is the first index where items[start] != new_items[start]"
83            );
84
85            // Skip common suffix.
86            let mut end = items.len();
87            let mut new_end = new_items.len();
88            while end > start && new_end > start && items[end - 1] == new_items[new_end - 1] {
89                end -= 1;
90                new_end -= 1;
91                mapped_tmp[new_end] = Some(mapped[end].clone());
92                disposers_tmp[new_end] = disposers[end].take();
93            }
94            debug_assert!(
95                    if end != 0 && new_end != 0 {
96                        (end == items.len() && new_end == new_items.len())
97                            || (items[end - 1] != new_items[new_end - 1])
98                    } else {
99                        true
100                    },
101                    "end and new_end are the last indexes where items[end - 1] != new_items[new_end - 1]"
102                );
103
104            // 0) Prepare a map of indices in new_items. Scan backwards so we encounter them in
105            // natural order.
106            let mut new_indices = HashMap::with_capacity(new_end - start);
107
108            // Indexes for new_indices_next are shifted by start because values at 0..start are
109            // always None.
110            let mut new_indices_next = vec![None; new_end - start];
111            for j in (start..new_end).rev() {
112                let item = &new_items[j];
113                let key = key_fn(item);
114                let i = new_indices.get(&key);
115                new_indices_next[j - start] = i.copied();
116                new_indices.insert(key, j);
117            }
118
119            // 1) Step through old items and see if they can be found in new set; if so, mark
120            // them as moved.
121            for i in start..end {
122                let item = &items[i];
123                let key = key_fn(item);
124                if let Some(j) = new_indices.get(&key).copied() {
125                    // Moved. j is index of item in new_items.
126                    mapped_tmp[j] = Some(mapped[i].clone());
127                    disposers_tmp[j] = disposers[i].take();
128                    new_indices_next[j - start].and_then(|j| new_indices.insert(key, j));
129                } else {
130                    // Create new.
131                    disposers[i].take().unwrap().dispose();
132                }
133            }
134
135            // 2) Set all the new values, pulling from the moved array if copied, otherwise
136            // entering the new value.
137            for j in start..new_items.len() {
138                if matches!(mapped_tmp.get(j), Some(Some(_))) {
139                    // Pull from moved array.
140                    if j >= mapped.len() {
141                        debug_assert_eq!(mapped.len(), j);
142                        mapped.push(mapped_tmp[j].clone().unwrap());
143                        disposers.push(disposers_tmp[j].take());
144                    } else {
145                        mapped[j] = mapped_tmp[j].clone().unwrap();
146                        disposers[j] = disposers_tmp[j].take();
147                    }
148                } else {
149                    // Create new value.
150                    let mut tmp = None;
151                    let new_item = new_items[j].clone();
152                    let new_disposer = create_child_scope(|| tmp = Some(map_fn(new_item)));
153                    if mapped.len() > j {
154                        mapped[j] = tmp.unwrap();
155                        disposers[j] = Some(new_disposer);
156                    } else {
157                        mapped.push(tmp.unwrap());
158                        disposers.push(Some(new_disposer));
159                    }
160                }
161            }
162        }
163
164        // 3) In case the new set is shorter than the old, set the length of the mapped array.
165        mapped.truncate(new_items.len());
166        disposers.truncate(new_items.len());
167
168        // 4) Save a copy of the mapped items for the next update.
169        debug_assert!([mapped.len(), disposers.len()]
170            .iter()
171            .all(|l| *l == new_items.len()));
172        items = new_items;
173
174        mapped.clone()
175    };
176    let scope = use_current_scope();
177    create_memo(on(list, move || scope.run_in(&mut update)))
178}
179
180/// Function that maps a `Vec` to another `Vec` via a map function.
181///
182/// The mapped `Vec` is lazily computed, meaning that it's value will only be updated when
183/// requested. Modifications to the input `Vec` are diffed by index to prevent recomputing values
184/// that have not changed.
185///
186/// Generally, it is preferred to use [`map_keyed`] instead when a key function
187/// is available.
188///
189/// This function is the underlying utility behind `Indexed`.
190///
191/// # Params
192/// * `list` - The list to be mapped. The list must be a [`ReadSignal`] (obtained from a [`Signal`])
193///   and therefore reactive.
194/// * `map_fn` - A closure that maps from the input type to the output type.
195pub fn map_indexed<T, U>(
196    list: impl Into<MaybeDyn<Vec<T>>> + 'static,
197    mut map_fn: impl FnMut(T) -> U + 'static,
198) -> ReadSignal<Vec<U>>
199where
200    T: PartialEq + Clone + 'static,
201    U: Clone,
202{
203    let list = list.into();
204    // Previous state used for diffing.
205    let mut items = Vec::new();
206    let mut mapped = Vec::new();
207    let mut disposers: Vec<NodeHandle> = Vec::new();
208
209    // Diff and update signal each time list is updated.
210    let _list = list.clone();
211    let mut update = move || {
212        let new_items = _list.get_clone();
213
214        if new_items.is_empty() {
215            // Fast path for removing all items.
216            for dis in mem::take(&mut disposers) {
217                dis.dispose();
218            }
219            items = Vec::new();
220            mapped = Vec::new();
221        } else {
222            // Pre-allocate space needed
223            if new_items.len() > items.len() {
224                let new_count = new_items.len() - items.len();
225                mapped.reserve(new_count);
226                disposers.reserve(new_count);
227            }
228
229            for (i, new_item) in new_items.iter().cloned().enumerate() {
230                let item = items.get(i);
231                // We lift the equality out of the else if branch to satisfy borrow checker.
232                let eqs = item != Some(&new_item);
233
234                if item.is_none() || eqs {
235                    let mut tmp = None;
236                    let new_disposer = create_child_scope(|| tmp = Some(map_fn(new_item)));
237                    if item.is_none() {
238                        mapped.push(tmp.unwrap());
239                        disposers.push(new_disposer);
240                    } else if eqs {
241                        mapped[i] = tmp.unwrap();
242                        let prev = mem::replace(&mut disposers[i], new_disposer);
243                        prev.dispose();
244                    }
245                }
246            }
247
248            if new_items.len() < items.len() {
249                for _i in new_items.len()..items.len() {
250                    disposers.pop().unwrap().dispose();
251                }
252            }
253
254            // In case the new set is shorter than the old, set the length of the mapped array.
255            mapped.truncate(new_items.len());
256
257            // Save a copy of the mapped items for the next update.
258            debug_assert!([mapped.len(), disposers.len()]
259                .iter()
260                .all(|l| *l == new_items.len()));
261            items = new_items;
262        }
263
264        // Update signal to trigger updates.
265        mapped.clone()
266    };
267    let scope = use_current_scope();
268    create_memo(on(list, move || scope.run_in(&mut update)))
269}
270
271#[cfg(test)]
272mod tests {
273    use std::cell::Cell;
274    use std::rc::Rc;
275
276    use super::*;
277
278    #[test]
279    fn keyed() {
280        let _ = create_root(|| {
281            let a = create_signal(vec![1, 2, 3]);
282            let mapped = map_keyed(a, |x| x * 2, |x| *x);
283            assert_eq!(mapped.get_clone(), vec![2, 4, 6]);
284
285            a.set(vec![1, 2, 3, 4]);
286            assert_eq!(mapped.get_clone(), vec![2, 4, 6, 8]);
287
288            a.set(vec![2, 2, 3, 4]);
289            assert_eq!(mapped.get_clone(), vec![4, 4, 6, 8]);
290        });
291    }
292
293    #[test]
294    fn keyed_recompute_everything() {
295        let _ = create_root(|| {
296            let a = create_signal(vec![1, 2, 3]);
297            let mapped = map_keyed(a, |x| x * 2, |x| *x);
298            assert_eq!(mapped.get_clone(), vec![2, 4, 6]);
299
300            a.set(vec![4, 5, 6]);
301            assert_eq!(mapped.get_clone(), vec![8, 10, 12]);
302        });
303    }
304
305    /// Test fast path for clearing Vec.
306    #[test]
307    fn keyed_clear() {
308        let _ = create_root(|| {
309            let a = create_signal(vec![1, 2, 3]);
310            let mapped = map_keyed(a, |x| x * 2, |x| *x);
311
312            a.set(Vec::new());
313            assert_eq!(mapped.get_clone(), Vec::<i32>::new());
314        });
315    }
316
317    /// Test that using [`Scope::map_keyed`] will reuse previous computations.
318    #[test]
319    fn keyed_use_previous_computation() {
320        let _ = create_root(|| {
321            let a = create_signal(vec![1, 2, 3]);
322            let counter = Rc::new(Cell::new(0));
323            let mapped = map_keyed(
324                a,
325                {
326                    let counter = Rc::clone(&counter);
327                    move |_| {
328                        counter.set(counter.get() + 1);
329                        counter.get()
330                    }
331                },
332                |x| *x,
333            );
334            assert_eq!(mapped.get_clone(), vec![1, 2, 3]);
335
336            a.set(vec![1, 2]);
337            assert_eq!(mapped.get_clone(), vec![1, 2]);
338
339            a.set(vec![1, 2, 4]);
340            assert_eq!(mapped.get_clone(), vec![1, 2, 4]);
341
342            a.set(vec![1, 2, 3, 4]);
343            assert_eq!(mapped.get_clone(), vec![1, 2, 5, 4]);
344        });
345    }
346
347    #[test]
348    fn keyed_call_cleanup_on_remove() {
349        let _ = create_root(|| {
350            let a = create_signal(vec![1, 2, 3]);
351            let counter = Rc::new(Cell::new(0));
352            let _mapped = map_keyed(
353                a,
354                {
355                    let counter = Rc::clone(&counter);
356                    move |_| {
357                        let counter = Rc::clone(&counter);
358                        on_cleanup(move || {
359                            counter.set(counter.get() + 1);
360                        });
361                    }
362                },
363                |x| *x,
364            );
365            assert_eq!(counter.get(), 0, "no cleanup yet");
366
367            a.set(vec![1, 2]);
368            assert_eq!(counter.get(), 1);
369
370            a.set(vec![1, 2, 3]);
371            assert_eq!(counter.get(), 1);
372
373            a.set(vec![1, 3]);
374            assert_eq!(counter.get(), 2);
375        });
376    }
377
378    #[test]
379    fn keyed_call_cleanup_on_remove_all() {
380        let _ = create_root(|| {
381            let a = create_signal(vec![1, 2, 3]);
382            let counter = Rc::new(Cell::new(0));
383            let _mapped = map_keyed(
384                a,
385                {
386                    let counter = Rc::clone(&counter);
387                    move |_| {
388                        let counter = Rc::clone(&counter);
389                        on_cleanup(move || {
390                            counter.set(counter.get() + 1);
391                        })
392                    }
393                },
394                |x| *x,
395            );
396            assert_eq!(counter.get(), 0, "no cleanup yet");
397
398            a.set(vec![]);
399            assert_eq!(counter.get(), 3);
400        });
401    }
402
403    #[test]
404    fn indexed() {
405        let _ = create_root(|| {
406            let a = create_signal(vec![1, 2, 3]);
407            let mapped = map_indexed(a, |x| x * 2);
408            assert_eq!(mapped.get_clone(), vec![2, 4, 6]);
409
410            a.set(vec![1, 2, 3, 4]);
411            assert_eq!(mapped.get_clone(), vec![2, 4, 6, 8]);
412
413            a.set(vec![2, 2, 3, 4]);
414            assert_eq!(mapped.get_clone(), vec![4, 4, 6, 8]);
415        });
416    }
417
418    /// Test fast path for clearing Vec.
419    #[test]
420    fn indexed_clear() {
421        let _ = create_root(|| {
422            let a = create_signal(vec![1, 2, 3]);
423            let mapped = map_indexed(a, |x| x * 2);
424
425            a.set(Vec::new());
426            assert_eq!(mapped.get_clone(), Vec::<i32>::new());
427        });
428    }
429
430    /// Test that result of mapped function can be listened to.
431    #[test]
432    fn indexed_react() {
433        let _ = create_root(|| {
434            let a = create_signal(vec![1, 2, 3]);
435            let mapped = map_indexed(a, |x| x * 2);
436
437            let counter = create_signal(0);
438            create_effect(move || {
439                counter.set(counter.get_untracked() + 1);
440                mapped.track();
441            });
442
443            assert_eq!(counter.get(), 1);
444            a.set(vec![1, 2, 3, 4]);
445            assert_eq!(counter.get(), 2);
446        });
447    }
448
449    /// Test that using [`map_indexed`] will reuse previous computations.
450    #[test]
451    fn indexed_use_previous_computation() {
452        let _ = create_root(|| {
453            let a = create_signal(vec![1, 2, 3]);
454            let counter = Rc::new(Cell::new(0));
455            let mapped = map_indexed(a, {
456                let counter = Rc::clone(&counter);
457                move |_| {
458                    counter.set(counter.get() + 1);
459                    counter.get()
460                }
461            });
462            assert_eq!(mapped.get_clone(), vec![1, 2, 3]);
463
464            a.set(vec![1, 2]);
465            assert_eq!(mapped.get_clone(), vec![1, 2]);
466
467            a.set(vec![1, 2, 4]);
468            assert_eq!(mapped.get_clone(), vec![1, 2, 4]);
469
470            a.set(vec![1, 3, 4]);
471            assert_eq!(mapped.get_clone(), vec![1, 5, 4]);
472        });
473    }
474
475    #[test]
476    fn indexed_call_cleanup_on_remove() {
477        let _ = create_root(|| {
478            let a = create_signal(vec![1, 2, 3]);
479            let counter = Rc::new(Cell::new(0));
480            let _mapped = map_indexed(a, {
481                let counter = Rc::clone(&counter);
482                move |_| {
483                    let counter = Rc::clone(&counter);
484                    on_cleanup(move || {
485                        counter.set(counter.get() + 1);
486                    });
487                }
488            });
489            assert_eq!(counter.get(), 0, "no cleanup yet");
490
491            a.set(vec![1, 2]);
492            assert_eq!(counter.get(), 1);
493
494            a.set(vec![1, 2, 3]);
495            assert_eq!(counter.get(), 1);
496
497            a.set(vec![1, 3]);
498            assert_eq!(counter.get(), 3);
499        });
500    }
501
502    #[test]
503    fn indexed_call_cleanup_on_remove_all() {
504        let _ = create_root(|| {
505            let a = create_signal(vec![1, 2, 3]);
506            let counter = Rc::new(Cell::new(0));
507            let _mapped = map_indexed(a, {
508                let counter = Rc::clone(&counter);
509                move |_| {
510                    let counter = Rc::clone(&counter);
511                    on_cleanup(move || {
512                        counter.set(counter.get() + 1);
513                    })
514                }
515            });
516            assert_eq!(counter.get(), 0, "no cleanup yet");
517
518            a.set(vec![]);
519            assert_eq!(counter.get(), 3);
520        });
521    }
522
523    /// Regression test for <https://github.com/sycamore-rs/sycamore/issues/739>
524    #[test]
525    fn issue_739_keyed_should_not_track_nested_signals() {
526        let _ = create_root(|| {
527            let items = create_signal(vec![create_signal(1), create_signal(2)]);
528            map_keyed(items, |x| on_cleanup(move || x.dispose()), |x| x.get());
529            items.set(items.get_clone()[1..].to_vec());
530        });
531    }
532
533    /// Regression test for <https://github.com/sycamore-rs/sycamore/issues/739>
534    #[test]
535    fn issue_739_indexed_should_not_track_nested_signals() {
536        let _ = create_root(|| {
537            let items = create_signal(vec![create_signal(()), create_signal(())]);
538            map_indexed(items, |x| on_cleanup(move || x.dispose()));
539            items.set(items.get_clone()[1..].to_vec());
540        });
541    }
542}