Signals 2

3 views
Skip to first unread message

Dorian Aldrege

unread,
Aug 5, 2024, 7:44:20 AM8/5/24
to unarincan
doubleCounts derivation function does not run to calculate its value until the first time you read doubleCount. The calculated value is then cached, and if you read doubleCount again, it will return the cached value without recalculating.

When you read conditionalCount, if showCount is false the "Nothing to see here!" message is returned without reading the count signal. This means that if you later update count it will not result in a recomputation of conditionalCount.


If you set showCount to true and then read conditionalCount again, the derivation will re-execute and take the branch where showCount is true, returning the message which shows the value of count. Changing count will then invalidate conditionalCount's cached value.


When you read a signal within an OnPush component's template, Angular tracks the signal as a dependency of that component. When the value of that signal changes, Angular automatically marks the component to ensure it gets updated the next time change detection runs. Refer to the Skipping component subtrees guide for more information about OnPush components.


Signals are useful because they notify interested consumers when they change. An effect is an operation that runs whenever one or more signal values change. You can create an effect with the effect function:


Effects always run at least once. When an effect runs, it tracks any signal value reads. Whenever any of these signal values change, the effect runs again. Similar to computed signals, effects keep track of their dependencies dynamically, and only track signals which were read in the most recent execution.


By default, you can only create an effect() within an injection context (where you have access to the inject function). The easiest way to satisfy this requirement is to call effect within a component, directive, or service constructor:


When you create an effect, it is automatically destroyed when its enclosing context is destroyed. This means that effects created within components are destroyed when the component is destroyed. The same goes for effects within directives, services, etc.


Effects return an EffectRef that you can use to destroy them manually, by calling the .destroy() method. You can combine this with the manualCleanup option to create an effect that lasts until it is manually destroyed. Be careful to actually clean up such effects when they're no longer required.


This example will log a message when either currentUser or counter changes. However, if the effect should only run when currentUser changes, then the read of counter is only incidental and changes to counter shouldn't log a new message.


Effects might start long-running operations, which you should cancel if the effect is destroyed or runs again before the first operation finished. When you create an effect, your function can optionally accept an onCleanup function as its first parameter. This onCleanup function lets you register a callback that is invoked before the next run of the effect begins, or when the effect is destroyed.


In these cases, you can register to receive signals sent only by particularsenders. In the case of django.db.models.signals.pre_save, the senderwill be the model class being saved, so you can indicate that you only wantsignals sent by some model:


In some circumstances, the code connecting receivers to signals may runmultiple times. This can cause your receiver function to be registered morethan once, and thus called as many times for a signal event. For example, theready() method may be executed more than onceduring testing. More generally, this occurs everywhere your project imports themodule where you define the signals, because signal registration runs as manytimes as it is imported.


If this behavior is problematic (such as when using signals tosend an email whenever a model is saved), pass a unique identifier asthe dispatch_uid argument to identify your receiver function. Thisidentifier will usually be a string, although any hashable object willsuffice. The end result is that your receiver function will only bebound to the signal once for each unique dispatch_uid value:


send() differs from send_robust() in how exceptions raised by receiverfunctions are handled. send() does not catch any exceptions raised byreceivers; it simply allows errors to propagate. Thus not all receivers maybe notified of a signal in the face of an error.


Whether synchronous or asynchronous, receivers will be correctly adapted towhether send() or asend() is used. Synchronous receivers will becalled using sync_to_async() when invoked via asend(). Asynchronousreceivers will be called using async_to_sync() when invoked viasync(). Similar to the case for middleware,there is a small performance cost to adapting receivers in this way. Note thatin order to reduce the number of sync/async calling-style switches within asend() or asend() call, the receivers are grouped by whether or notthey are async before being called. This means that an asynchronous receiverregistered before a synchronous receiver may be executed after the synchronousreceiver. In addition, async receivers are executed concurrently usingasyncio.gather().


To disconnect a receiver from a signal, call Signal.disconnect(). Thearguments are as described in Signal.connect(). The method returnsTrue if a receiver was disconnected and False if not. When senderis passed as a lazy reference to ., this method alwaysreturns None.


What makes Signals unique is that state changes automatically update components and UI in the most efficient way possible. Automatic state binding and dependency tracking allows Signals to provide excellent ergonomics and productivity while eliminating the most common state management footguns.


This guide will go over using Signals in Preact, and while this is largely applicable to both the Core and React libraries, there will be some usage differences. The best references for their usage is in their respective docs: @preact/signals-core, @preact/signals-react


Much of the pain of state management in JavaScript is reacting to changes for a given value, because values are not directly observable. Solutions typically work around this by storing values in a variable and continuously checking to see if they have changed, which is cumbersome and not ideal for performance. Ideally, we want a way to express a value that tells us when it changes. That's what Signals do.


In Preact, when a signal is passed down through a tree as props or context, we're only passing around references to the signal. The signal can be updated without re-rendering any components, since components see the signal and not its value. This lets us skip all of the expensive rendering work and jump immediately to any components in the tree that actually access the signal's .value property.


Signals have a second important characteristic, which is that they track when their value is accessed and when it is updated. In Preact, accessing a signal's .value property from within a component automatically re-renders the component when that signal's value changes.


Finally, Signals are deeply integrated into Preact to provide the best possible performance and ergonomics. In the example above, we accessed count.value to retrieve the current value of the count signal, however this is unnecessary. Instead, we can let Preact do all of the work for us by using the count signal directly in JSX:


Let's use signals in a real world scenario. We're going to build a todo list app, where you can add and remove items in a todo list. We'll start by modeling the state. We're going to need a signal that holds a list of todos first, which we can represent with an Array:


To let the user enter text for a new todo item, we'll need one more signal that we'll connect up to an element shortly. For now, we can use this signal already to create a function that adds a todo item to our list. Remember, we can update a signal's value by assigning to its .value property:


Let's check if our logic is correct so far. When we update the text signal and call addTodo(), we should see a new item being added to the todos signal. We can simulate this scenario by calling these functions directly - no need for a user interface yet!


Let's add one more feature to our todo app: each todo item can be checked off as completed, and we'll show the user the number of items they've completed. To do that we'll import the computed(fn) function, which lets us create a new signal that is computed based on the values of other signals. The returned computed signal is read-only, and its value is automatically updated when any signals accessed from within the callback function change.


? Tip: Deriving as much state as possible ensures that your state always has a single source of truth. It is a key principle of signals. This makes debugging a lot easier in case there is a flaw in application logic later on, as there are less places to worry about.


Up until now, we've only created signals outside the component tree. This is fine for a small app like a todo list, but for larger and more complex apps this can make testing difficult. Tests typically involve changing values in your app state to reproduce a certain scenario, then passing that state to components and asserting on the rendered HTML. To do this, we can extract our todo list state into a function:


? Tip: Notice that we're consciously not including the addTodo() and removeTodo(todo) functions here. Separating data from functions that modify it often helps simplify application architecture. For more details, check out data-oriented design.


This works in our todo list app because the state is global, however larger apps typically end up with multiple components that require access to the same pieces of state. This usually involves "lifting state up" to a common shared ancestor component. To avoid passing state manually through each component via props, the state can be placed into Context so any component in the tree can access it. Here is a quick example of how that typically looks:

3a8082e126
Reply all
Reply to author
Forward
0 new messages