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

Add WritableComputed to allow computed with write operations #55673

Open
Harpush opened this issue May 4, 2024 · 5 comments
Open

Add WritableComputed to allow computed with write operations #55673

Harpush opened this issue May 4, 2024 · 5 comments
Labels
area: core Issues related to the framework runtime core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals
Milestone

Comments

@Harpush
Copy link

Harpush commented May 4, 2024

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

Revival with use case of #50498

Currently signal produces a writable signal.
Currently computed produces a non writable signal.
What I am missing is a writableComputed.

Use case example:
I have a required input called value but inside the component I can edit this value without affecting the outside. At the end an apply button is clicked and I emit the new value via an output.
Something like this:

class Test {
  value = input.required<string>();
  tempValue = signal(/*??*/);
  commit = output<string>()

  onEdit(newValue: string) {
    this.tempValue.set(newValue);
  }

  onApply() {
    this.commit.emit(this.tempValue());
  }
}

The first problem is tempValue isn't based on value.
The second problem is when value is changed from outside. At this point I wish to discard tempValue and start fresh with the new value.
If I had writableComputed I could do:

tempValue = writableComputed(() => this.value());

Which means anytime value changes it becomes the new tempValue value. If tempValue is edited the new edit is the tempValue new value until something gets changed in value.

Proposed solution

A new writableComputed which tracks the value inside based on the writeable part and will get reset each time the computed function runs to what it creates.
Examples:

const a = signal(7);
const b = writableComputed(() => a() + 1);
// b is 8
const a = signal(7);
const b = writableComputed(() => a() + 1);
b.set(9);
// A design question - 8 or 9
const a = signal(7);
const b = writableComputed(() => a() + 1);
a.set(11);
b.set(9);
// A design question - 12 or 9
const a = signal(7);
const b = writableComputed(() => a() + 1);
// b is 8
// Later
b.set(10)
// b is 10
// Later
a.set(20);
// b is 21

Alternatives considered

Using effect. The downside is with required inputs.

tempValue = signal<string | undefined>(undefined);
effect(() => this.tempValue.set(this.value()), {allowSignalWrites: true});

This will result in tempValue having undefined initially and for example in ngOnInit it will still have undefined although value already have the actual value set. Also a computed will have its value set too.
using writableComputed in ngOnInit will have the correct computed value.

Another option is that scenario hints that something I am doing is wrong from the basis and I would like to hear better suggestions how to handle it.

@json-derulo
Copy link

Did you also consider splitting up your computation and writing logic? Something like the following:

class Test {
  value = input.required<string>();
  writableValue = signal<string | undefined>(undefined);
  computedValue = computed(() => this.writableValue() ?? this.value() + 1);
}

IMHO Angular shouldn't offer a signal which is both computed and writable. It leads to ambiguity, you will never know whether the current value of the signal comes from the computation or from a signal write. Also when the input is updated, the previously set value will be reset, which I assume would be an unintended side effect.

@Harpush
Copy link
Author

Harpush commented May 4, 2024

Did you also consider splitting up your computation and writing logic? Something like the following:

class Test {
  value = input.required<string>();
  writableValue = signal<string | undefined>(undefined);
  computedValue = computed(() => this.writableValue() ?? this.value() + 1);
}

IMHO Angular shouldn't offer a signal which is both computed and writable. It leads to ambiguity, you will never know whether the current value of the signal comes from the computation or from a signal write. Also when the input is updated, the previously set value will be reset, which I assume would be an unintended side effect.

This won't reset neither writableValue nor computedValue when value changes though...
I will still need an effect to reset writableValue when value changes.

@alxhub
Copy link
Member

alxhub commented May 4, 2024

class Test {
  value = input.required<string>();
  // the trick: project the input's value to a `WritableSignal` of that value.
  // when the input changes, the inner `WritableSignal` will be replaced with a new one.
  writableInnerValue = computed(() => signal(this.value()));

  // helper to get rid of the double read
  innerValue = () => this.writableInnerValue()();
  
  setInnerValue(value: string): void {
    // the WritableSignal allows for internal mutation
    this.writableInnerValue().set(value);
  }
}

@JoostK
Copy link
Member

JoostK commented May 4, 2024

Or pulled out into a helper function:

interface MirrorSignal<T> {
  (): T;

  set(value: T);
}

function mirror<T>(outer: Signal<T>): MirrorSignal<T> {
  const inner = computed(() => signal(outer()));
  const mirror: MirrorSignal<T> = () = inner()();
  mirror.set = (value: T) => untracked(inner).set(value);
  return mirror;
}

@Harpush
Copy link
Author

Harpush commented May 4, 2024

Thanks @alxhub and @JoostK!
That seems to answer it mostly although that's not a real signal and I need to implement update fn too.
But it's probably the best I can get :)

@alxhub alxhub added area: core Issues related to the framework runtime core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals labels May 6, 2024
@ngbot ngbot bot added this to the needsTriage milestone May 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals
Projects
None yet
Development

No branches or pull requests

4 participants