Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signal projection #3695

Open
E-Mans-Application opened this issue Mar 9, 2025 · 1 comment
Open

Signal projection #3695

E-Mans-Application opened this issue Mar 9, 2025 · 1 comment

Comments

@E-Mans-Application
Copy link

Context
I have a wrapper struct, for example:

struct MyStruct {
  data: Vec<usize>
}

let parent_signal: Signal<MyStruct> = /* ... */;

Then, I have a (third party) component that wants a Signal<Vec<usize>>.
I want to be able to create a signal that provides parent_signal.read().data without unnecessary cloning.

Current way of doing it

Currently, one could create the appropriate signal as follows. However, this clones the Vec everytime the child signal is read. This can probably be improved by using memoization, but the vec will still need to be cloned on the first access.

let child_signal = move || parent_signal.read().data.clone();

Describe the solution you'd like

I think implementing something like the following could be great (if at all possible):

impl<T> Signal<T> where T: Send + Sync + 'static {
  pub fn project<U>(self, f: impl Fn(&T) -> &U + Send + Sync + 'static) -> Signal<U>
  where
    U: Send + Sync + 'static
  {
    todo!()
  }
}
impl<T> Signal<T> where T: 'static {
  pub fn project_local<U>(self, f: impl Fn(&T) -> &U + 'static) -> Signal<U>
  where
    U: Send + Sync + 'static
  {
    todo!()
  }
}

let child_signal = parent_signal.project(|value: &MyStruct| &value.data);

child_signal.get() would still clone the value, but child_signal.read() would not.

This may not be sound for all the types of signals, but I think it does make sense for the regular ReadSignal and RwSignal.

@gbj
Copy link
Collaborator

gbj commented Mar 9, 2025

EDIT up front: After I'd written the below, I re-read to make sure I was understanding correctly. I think there may be some underlying misunderstanding here.

You write:

I have a (third party) component that wants a Signal<Vec<usize>>.

This means, for example, that they want to be able to call .get() and get an owned Vec<usize>. This will involve a clone, inevitably, but it does not need to involve more than one clone.

This will return a signal that clones the data exactly once when they call .get(), which is pretty much what would be needed for an owned Vec<usize> in any case.

let child_signal = Signal::derive(move || parent_signal.read().data.clone());

This does not store the value anywhere, it is just a boxed closure.

Your proposal for a projected signal would be the same: when they called .get() it would clone data exactly once to give an owned Vec<usize>. If they wanted to call .read() instead, that would save a clone (in which case, see the two notes below) but since it's a third-party component you don't actually control what they're doing here.


You can do this with a Store, which is designed for exactly this purpose.

#[derive(Store, Debug)]
struct MyStruct {
    data: Vec<usize>,
}

#[component]
pub fn App() -> impl IntoView {
    let parent = Store::new(MyStruct { data: vec![] });

    view! {
        <TakesProjected data=parent.data()/>
    }
}

#[component]
pub fn TakesProjected(#[prop(into)] data: Field<Vec<usize>>) -> impl IntoView {
    todo!()
}

For what it's worth, you can also do the "projected signal" version in user-land (i.e., you can create a nicer API for it than the example below). This kind of approach is kind of a manual store, but is enabled by the same primitives. I think this is not worth it in most cases, but it does give you the level of control you'd need.

#[derive(Debug)]
struct MyStruct {
    data: Vec<usize>,
}

#[component]
pub fn App() -> impl IntoView {
    let parent_signal = RwSignal::new(MyStruct { data: vec![] });
    let projected = move || guards::Mapped::new_with_guard(parent_signal.read(), |s| &s.data);

    view! {
        <TakesSignalField data=projected />
    }
}

#[component]
pub fn TakesSignalField<T>(data: impl Fn() -> T) -> impl IntoView
where
    T: Deref<Target = Vec<usize>>,
{
    todo!()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants