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