I have a slightly different opinion.
Signals as I’d like them are a way to do reactive, declarative programming.
Instead of saying each time
new_x = 123
socket
|> assign(:x, new_x)
|> assign(:y, compute_new_y(new_x))
I’d like a way to “declare” that y is a derived value and do not worry about updating it explicitly in every place - it leads to bugs.
To some extend, I can do it in render
function
def render(assigns) do
assigns = Map.put(assigns, :y, compute_new_y(new_x))
~H"""
...
"""
end
but it has a drawback - template thinks that y
is always updated, since it’s not present in __changed__
. You can do some dark magic to update assigns.__changed__
manually as I’m doing in live_vue, but it’s often an overkill.
Btw, phoenix already has a logic we want. If we’ll define computed value inline in template, everything works just fine - it’s recomputed automatically when one of ingredients is updated
def render(assigns) do
~H"""
<div>{ @x * 2 }</div>
"""
end
I think we can even use function there
def render(assigns) do
~H"""
<div>{ compute_new_y(@x) }</div>
"""
end
but the problem:
- we need to put that function explicitly in each template which would like to use it
- it would be computed multiple times, one for each invocation
- It get’s more and more complex if we want to use result of one computation inside another one.
- We cannot do it outside of HEEX template
I’m experimenting a bit with a macro for doing it - at compile time, calculating dependencies of a function. It seem to work, but it might be tricky to make it “fully” working. In JS they’re using proxies to detect property access, here we don’t have it - we’d need to either provide our own way of accessing these values so we could track accesses, or statically determine all the dependencies. Either not ideal and tricky…
test "logs accessed keys and returns computed value (dot access)" do
assigns =
assign_computed(%{val1: 5, val2: 10}, :result, fn data -> data.val1 + data.val2 * 2 end)
# 5 + 10 * 2 = 25
assert assigns.result == 25
assert assigns.__deps == %{val1: [:result], val2: [:result]}
end