preamble
Signalsis a way of describing state that ensures that applications remain fast, no matter how complex they become.SignalsBased on responsive principles, it provides excellent developer ergonomics and a unique implementation for virtual DOM optimization.
SignalsThe heart of the matter is that a signal is a signal that has a.value
attribute of an object that holds some value. When the value value of signal changes, accessing the signal from a component of thevalue
property automatically updates the component.
In addition to being straightforward and easy to write, it also ensures that status updates remain fast, no matter how many components you have in your application.SignalsIt's fast by default, and automatically optimizes the performance of updates for you behind the scenes.
run in repl
import { render } from "preact"; import { signal, computed } from "@preact/signals"; const count = signal(0); const double = computed(() => * 2); function Counter() { return ( <button onClick={() => ++}> {count} x 2 = {double} </button> ); } render(<Counter />, ("app"));
SignalsIt can be used inside or outside the component, it's not the same as hooks.SignalsThey can also be used with hooks and class components, so you can take your existing knowledge and introduce them step by step at your own pace. Try them out in a few components and gradually adopt them over timeSignals。
Oh by the way, we've stayed true to our original intent of providing the smallest possible class library for everyone. InPreact
hit the nail on the headsignalsIn the end it only adds to the size of your package the1.6KB。
If you'd like to skip right into learning, go to our(computer) fileGo deeper in thesignals。
What issues did signals address?
Over the past few years, we've worked on a variety of applications and teams, from small startups to monolithic applications with hundreds of developers working on them at once. In that time, everyone on the core team has noticed recurring issues in the way application state is managed.
Some fantastic solutions have been proposed to solve these problems, but even the best solutions require manual integration into the framework. As a result, we've seen developers hesitate to adopt these solutions in favor of building with the state that the framework originally provided.
we willSignalsBuild a compelling solution that combines optimal performance, developer ergonomics and seamless integration of the framework.
The struggle for global status
The state of an application usually starts out small and simple, perhaps a few simpleuseState
hooks. As the application iterates, more and more components need access to the same state, which is eventually lifted above a common ancestor component. This pattern is repeated many times until most of the state ends up near the root component of the component tree.
This scenario poses a challenge to traditional virtual DOM-based frameworks, which must update the entire tree affected by state failure. Essentially, rendering performance is a function of the number of components in that tree. We can achieve this by using thememo
oruseMemo
Memorizing parts of the component tree solves this problem so that the framework receives the same objects. When there are no changes, it is possible for the framework to skip certain parts of the rendering tree.
While this sounds reasonable in theory, the reality is often much more confusing. In practice, it can be difficult to determine where these optimizations should be placed as the code base grows. Often, even well-intentioned mnemonic optimizations are rendered ineffective by unstable dependency values. Since hooks don't have an explicit dependency tree that can be analyzed, tools can't help developers diagnose why a dependency is unstable.
context-sensitive
Another common solution is to put state into context. This allows for short-circuit rendering by skipping component rendering between the context provider and the consumer. But there is a catch: only the values passed to the context provider can be updated, and only as a whole. Updating a property on an object exposed through a context does not update the consumer of that context - granular updates are not possible. A viable option to solve this problem is to split the state into multiple contexts, or to invalidate any property of a context object by cloning it when it changes.
Moving state values to contexts may initially seem like a tradeoff worth considering, but the downside of increasing the size of the component tree in order to share values eventually becomes a problem. Business logic inevitably ends up relying on multiple context values, which may force it to be implemented at specific locations in the component tree. Adding a component that subscribes to a context in the middle of the tree is expensive because it reduces the number of components that can be skipped when updating the context. More importantly, any components below the subscriber must now be re-rendered. The only way to solve this problem is to make heavy use of memoization, which brings us back to the problems inherent in memoization.
Finding better ways to manage status
We are back to the drawing board of finding the next generation of state primitives. We wanted to create something that solved the problems in the current solution. Manual framework integration, transition dependency memoization, sub-optimal use of context, and a lack of programmable observability all felt backwards.
Developers need to consider the performance of opting in to these strategies. It would be nice if we could turn this around and provide anDefault Fastof the system, making optimal performance something you have to work hard to choose, what happens then?
Our answers to these questions areSignals. It's a fast system by default and doesn't require memorization or other tricks in your application.SignalsProvides the benefit of fine-grained state updates, whether that state is global, passed via props or context, or localized to a component.
Signals to the Future
SignalsThe main idea behind this is that instead of passing a value through the component tree, we pass a signal object that contains that value (similar to theref
). When the value attribute of a signal changes, the signal itself remains unchanged. As a result, signals can be updated without re-rendering the components they pass through, since the components see the signal and not its value. This lets us skip the expensive work of rendering components and jump immediately to the specific component in the tree that actually accesses the value property of the signal.
We are applying the fact that an application's statechart is usually much shallower than its component tree. This allows for faster rendering because it takes much less work to update the statechart compared to the component tree. This difference is most noticeable when measured in the browser - the screenshot below shows the DevTools Profiler trace for the same application measured twice: first time using hooks, second time using Signals.
The signals version is vastly superior to the update mechanism of any traditional virtual DOM-based framework. In some of the applications we tested, signals were so fast that it was difficult to find them at all in the flame map.
The performance of signals is flipped: signals is not selecting performance via memoization or selectors, signals is fast by default. With signals, performance is selectively disregarded (signals were not used before).
To achieve this level of performance, signals is built on these key principles:
- Lazy by default: Only observe and update signals that are currently being used somewhere - disconnected signals do not affect performance.
- Best Updates: If the value of a signal is not modified, components and effects that use the value of that signal will not be updated, even if the dependencies of that signal have changed.
- Optimal dependency tracking: The framework keeps track of the signals that everything depends on for you - there are no dependency arrays like hooks.
- direct access: Accessing the value of a signal in a component automatically subscribes to updates, no longer requiring a selector or hook.
These principles make signal well suited for a wide range of usage scenarios, even those unrelated to rendering user interfaces.
Bringing signals into Preact
After confirming the correct state primitives, we started connecting them to Preact. We've always liked hooks because they can be used directly in the component. This is a user-friendly advantage over third-party state management solutions, which often rely on "selector" functions or wrapping the component in a special function to subscribe to updates.
// Selector based subscription :( function Counter() { const value = useSelector(state => ); // ... } // Wrapper function based subscription :( const counterState = new Counter(); const Counter = observe(props => { const value = ; // ... });
Neither of these approaches satisfies us. The selector approach requires wrapping all state accesses in selectors, which becomes cumbersome for complex or nested state. The method of wrapping components in functions requires wrapping the components manually, which creates a number of problems, such as missing component names and static properties.
We've had the opportunity to work closely with many developers over the past few years. A common struggle, especially for those new to (p)react, is that concepts like selectors and wrappers are additional paradigms that must be learned before feeling productive with each state management solution.
Ideally, we don't need to know about selector or wrapper functions to access the state in the component directly:
// Assuming this is a global state, the entire application has access to. let count = 0; function Counter() { return ( <button onClick={() => count++}> value: {count} </button> ); }
The code above is clear and it's easy to understand what's happening, but unfortunately it doesn't work. When the button is clicked, the component isn't updated because there's no way to know that thecount
It has changed.
We cannot erase this scene from our minds. What could we do to make such a clear model a reality? We started using preact'sPluggable Renderer, prototyping various ideas and implementations. The effort paid off and we eventually found an implementation:
// Assuming this is a global state, the entire application has access to. const count = signal(0); function Counter() { return ( <button onClick={() => ++}> Value: {} </button> ); }
No selectors, no wrapper functions, nothing. Accessing the value of a signal is enough to let the component know that it needs to update when the value of that signal changes. After testing this prototype in a couple of applications, it was clear that we were onto something. Writing code this way feels intuitive and doesn't require any mental gymnastics to keep things in tip-top shape.
Can our code run faster?
We could have stopped here and posted it as issignals, but this is the Preact team: we need to see how far we can push the Preact integration. In the Counter example above, thecount
The value of the signal is only used to display the text, which really shouldn't have to go through the trouble of re-rendering the whole component. What if instead of automatically re-rendering the component when the value of the signal changes, we just re-render the text? Even better, what if we bypassed the virtual DOM altogether and updated the text directly in the DOM?
const count = signal(0); // Instead of this: <p>Value: {}</p> // … we can pass the signal directly into JSX: <p>Value: {count}</p> // … or even passing them as DOM properties: <input value={count} />
So, yes, we did that too. You can pass signal directly into JSX anywhere you would normally use a string. signal's value will be rendered as text, and when signal changes, it will automatically update itself. This also applies to props.
the next step
If you're curious and want to go right in and check it out, head over to our(computer) fileWe'd love to hear how you're going to use them.
Remember, don't be in a hurry to switch to signals. hooks will continue to be supported, and they work well with signals! We recommend experimenting with signals gradually. we recommend experimenting with signals gradually, starting with a few components to get used to the concepts.
to this article on a detailed explanation of preact's high-performance state management Signals article is introduced to this, more related to preact state management Signals content please search for my previous articles or continue to browse the following related articles I hope you will support me in the future !