DEV Community

zhangfisher
zhangfisher

Posted on

In depth: Signal components and fine-grained render in React

What is a Signal?

In mainstream front-end development frameworks, whether it's React, Vue, or Svelte, the core revolves around rendering the UI more efficiently.

To achieve high performance, based on the assumption that the DOM is always slow, there are two core issues to be addressed:

  • Reactive updates
  • Fine-grained updates

To optimize reactive updates and fine-grained updates to the extreme, various frameworks have shown their unique skills. Taking the most popular React and Vue as examples,

First of all, both have introduced the concept of Virtual DOM.

  • Vue's static template compilation optimizes the logic of fine-grained updates through static analysis at compile time, analyzing the DOM that needs to be rendered as much as possible at compile time.

  • React, on the other hand, uses JSX dynamic syntax, which is essentially a function, making it difficult to perform static analysis. Therefore, React can only try to optimize the rendering logic at runtime.

  • Hence, React came up with the concept of Fiber, optimizing the rendering logic through Fiber scheduling, but the scheduling logic of Fiber is very complex, and the official team has been working on it for a year.

  • Then there are a bunch of React.memo optimization techniques, but when the application is complex, it can also be a considerable mental burden to manage.

  • Therefore, the official team also developed React Compiler, which automatically adds React.memo logic to the code through static analysis at compile time, but this has been in the experimental stage for over two years and seems difficult to implement.
    Due to the characteristics of the Virtual DOM, whether it's React or Vue, they are essentially performing diff algorithms on the Virtual DOM and then performing patch operations, with the difference being the implementation of the diff algorithm.

However, no matter what, with the diff algorithm supported by the Virtual DOM, it is always difficult to precisely match the changes in state with the corresponding DOM.

In layman's terms, when state.xxx is updated, it is not directly finding the DOM that uses state.xxx for precise updates, but rather calculating the DOM elements that need to be updated through the diff algorithm of the Virtual DOM, and then performing patch operations.

The problem is that this diff algorithm is complex and requires various optimizations, which also imposes a certain mental burden on developers, such as the use of React.memo in large React applications, or template optimization in Vue, and so on.

:::warning{title=Note}

Q: Why is the use of React.memo considered a mental burden in large applications?
A: In fact, the logic of React.memo itself is very simple, and both novices and experts can easily master it. However, in large applications, on one hand, the nesting level of components is deep, and the dependency relationship between components is complex; on the other hand, there are hundreds or even thousands of components. If you want to use React.memo to optimize rendering, it becomes a significant mental burden. If you adopt post-optimization, the problem is even more severe, often requiring the use of some performance analysis tools for targeted optimization. Simply put, React.memo only becomes a burden when the application becomes complex.
:::

Therefore, the core issue of the framework is to quickly find the DOM that depends on the state and update it according to the changes in the state, that is, the so-called fine-grained updates.

Since the diff algorithm based on the Virtual DOM has problems in solving fine-grained updates, is it possible not to perform the diff algorithm and directly find the DOM corresponding to state.xxx for updates?

There is a way, which is the concept of signal that is most popular in the front end.

In fact, the concept of signal has been around for a long time, but since frameworks like Svelte came out, they do not use Virtual DOM and do not need diff algorithms, but instead introduce the concept of signal, which can achieve fine-grained updates only when the signal is triggered, and the performance is also very good.

This has suddenly confused the Virtual DOM players like React and Vue, and for a while, signal has become the new favorite of front-end development.

All front-end frameworks are moving closer to signal, with Svelte and solidjs becoming representatives of the signal school, and even Vue cannot avoid it, with Vue Vapor being the signal implementation of Vue (not yet released).

So what is a signal?

Quoting a paragraph from Mr. Kasong's article on signal Signal: More Choices for Front-end Frameworks.

Mr. Kasong said, "The essence of signal is to separate the reference to the state from the acquisition of the state value."

The master is indeed a master, summarizing the essence of signal in one sentence. But it also confused us ordinary people, as this concept is too high and abstract, indeed a master.

Below, let's understand signal in layman's terms and construct a basic process principle of the signal mechanism:

  • Step 1: Make state data observable Make state data reactive or observable, which can be achieved by using methods such as Proxy or Object.defineProperty, turning state data into an observable object, rather than a normal data object.

The role of the observable object is to intercept access to the state, so when the state is read or written, dependency information can be collected.

There are various methods to make data observable, such as mobx does not use Proxy, but uses the get property of Class to implement it. You can even use your own set of APIs to implement it. However, Proxy is now commonly used. The core principle is to intercept access to the state, thereby collecting dependency information.

The purpose of making state data observable is to perceive changes in state data so that the next step of response can be carried out. The finer the granularity of perception, the more fine-grained updates can be achieved.

  • Step 2: Signal publishing/subscribing Since we can intercept access to the state through it, we can know when the state is read or written, so we can publish a signal when the state changes, notifying subscribers that the state has changed.

Therefore, we need a signal publishing/subscribing mechanism to register which signals have changed and who has subscribed to these signals.

You can use libraries like mitt, EventEmitter, etc., to build signal publishing/subscribing, or you can write your own.

The core of signal publishing/subscribing is actually a subscription table, which records who has subscribed to what signal, in the front end, which DOM rendering function depends on which signal (state change).

The purpose of establishing a publishing/subscribing mechanism is to establish a mapping relationship between rendering functions and state data. When state data changes, query the rendering functions that depend on the state data according to this, and then execute these rendering functions, thereby achieving fine-grained updates.

  • Step 3: Rendering function Next, we write the DOM rendering function, as follows:
  function render() {
      element.textContent = countSignal.value.toString();
  }
Enter fullscreen mode Exit fullscreen mode

In this rendering function:

We directly update the DOM element, without any diff algorithm or Virtual DOM.
The function uses access to state data count to update the DOM element. Since the state is observable, when countSignal.value is executed, we can intercept the access to count, which means we have collected the dependency of this DOM element on count state data.
With this dependency relationship between DOM Render and state data, we can register this dependency relationship in the signal's signal publishing/subscribing mechanism.

Collecting dependencies is to establish a relationship between the rendering function and the state.

  • Step 4: Register the rendering function Finally, we register the render function in the subscriber list of signal, so when the count state data changes, we can notify the render function to update the DOM element.

Simple Example

Below is a simple example of signal, where we create a signal object countSignal, and create a DOM element countElement. When countSignal changes, we update the textContent of countElement.


        class Signal<T> {
          private _value: T;
          private _subscribers: Array<(value: T) => void> = [];
          constructor(initialValue: T) {
              this._value = initialValue;
          }
          get value(): T {
              return this._value;
          }
          set value(newValue: T) {
              if (this._value !== newValue) {
                  this._value = newValue;
                  this.notifySubscribers();
              }
          }
          subscribe(callback: (value: T) => void): () => void {
              this._subscribers.push(callback);
              return () => {
                  this._sub咛ubscribers = this._subscribers.filter(subscriber => subscriber !== callback);
              };
          }

          private notifySubscribers() {
              this._subscribers.forEach(callback => callback(this._value));
          }
      }

      const countSignal = new Signal<number>(0);
      const countElement = document.getElementById('count')!;
      const incrementButton = document.getElementById('increment')!;

      function render() {
          countElement.textContent = countSignal.value.toString();
      }
      function increment() {
          countSignal.value += 1;
      }
      countSignal.subscribe(render);
      incrementButton.addEventListener('click', increment);
      render(); 
<h1>Counter: <span id="count">0</span></h1>
<button id="increment">Increment</button>
Enter fullscreen mode Exit fullscreen mode

Signal Components

So how do we use signal in React?

From the above, we know that signal-driven front-end frameworks do not need Virtual DOM at all.

And essentially, React is not a Signal framework, its rendering scheduling is based on Virtual DOM, fiber, and diff algorithm.

Therefore, React does not support the concept of signal, unless in the future React upgrades like Vue to Vue Vapor mode for a major upgrade, abandoning Virtual DOM, otherwise, signal cannot be truly used in React like solidjs and Svelte.

However, whether it's Virtual DOM or signal, the core is to solve the problem of fine-grained updates in order to improve rendering performance.

Therefore, we can combine React's React.memo and useMemo methods to simulate the concept of signal and achieve fine-grained updates.

Thus, we have the concept of signal components, which are essentially ReactNode components wrapped with React.memo, limiting the scope of rendering updates to a smaller range.

Image description

The core is a set of dependency collection and event distribution mechanisms, used to perceive state changes, and then distribute changes through events.
Signal components are essentially ordinary React components, but wrapped with React.memo(()=>{.....},()=>true) for packaging, where diff always returns true, used to isolate the DOM rendering scope.
Then, within the signal component, it will subscribe to the state changes it depends on from the state distribution, and re-render the component when the state changes.
Since diff always returns true, the re-rendering is constrained within the component itself, not causing a chain reaction, thereby achieving fine-grained updates.
Below is a simple example of signal in AutoStore

/**
* title: Signal Components
* description: When directly writing state such as `state.age=n`, you need to use `{$('age')}` to create a signal component, which internally subscribes to the change events of `age` to trigger local updates.
*/
import { createStore } from '@autostorejs/react';
import { Button,ColorBlock } from "x-react-components"

const { state , $ } = createStore({
  age:18
})

export default () => {

  return <div>
      <div>Age+Signal :{$('age')}</div>
      <div>Age :{state.age}</div>
      <Button onClick={()=>state.age=state.age+1}>+Age</Button>
    </div>
}
Enter fullscreen mode Exit fullscreen mode

AutoStore is a recently open source responsive state library that provides powerful state functionality, with the following main features:

  • Responsive Core: Based on Proxy implementation, data changes automatically trigger view updates.
  • computed: The unique on site calculation feature allows for declaring the calculated attribute at any position in the state tree and writing the calculation result in place.
  • Dependency auto tracking: Automatically tracks dependencies of computed attributes, only recalculating when dependencies change.
  • Asynchronous computing: Powerful asynchronous computing control capability, supporting advanced functions such as timeout, retry, cancel, countdown, progress, etc.
  • State change monitoring: capable of monitoring operations on state objects and arrays such as get/set/delete/insert/update.
  • Signal component: Supports signal mechanism and can achieve fine-grained component updates.
  • Debugging and Diagnosis: Supports the Redux iPadOS Extension debugging tool for Chrome, making it easy to debug state changes.
  • Nested state: Supports nested states of any depth, without worrying about the complexity of state management.
  • Form binding: Powerful and concise bidirectional form binding, with simple and fast data collection.
  • Circular dependencies: can help detect circular dependencies and reduce failures.
  • Typescript: Fully supports Typescript, providing complete type inference and prompts
  • Unit testing: Provide complete coverage of unit testing to ensure code quality. AutoStore can introduce signal components to React to achieve fine-grained update rendering, allowing React to also enjoy the silky feel brought by signals.

AutoStore Github

Top comments (0)