1

1. What are Signals?

Signals is a way to deal with state, it refers to SolidJS and absorbs most of its advantages. It guarantees fast response no matter how complex the application is.

What's unique about Signals is that state changes automatically update components and UI in the most efficient way possible.

Signals provides great ergonomics based on automatic state binding and dependency tracking, and has a unique implementation optimized for the virtual DOM.

2. Why Signals?

2.1 The dilemma of state management

As the application becomes more and more complex, there will be more and more components in the project, and more and more states need to be managed.

In order to achieve component state sharing, it is generally necessary to promote the state to the common ancestor component of the component, and pass it down through props . The problem is that when updating, all sub memo components will be updated, which requires cooperation- memo and useMemo to optimize performance.

While this sounds reasonable, as the project code grows, it becomes difficult to determine where these optimizations should go.

Even adding memoization often becomes invalid due to unstable dependency values, and since Hooks don't have an explicit dependency tree to analyze, there is no way to use tools to find the cause.

Another solution is to put it Context , and the sub-component as a consumer can get the required state through useContext .

But there is a problem, only the value passed to the Provider can be updated, and it can only be updated as a whole, and fine-grained update cannot be achieved.

In order to deal with this problem, we can only split Context , and the business logic will inevitably depend on multiple Context , so that Context nesting dolls will appear. Phenomenon.

2.2 Signals to the future

You must feel deja vu when you see this, yes, the solution to the future must be me - Recoil, no, this time the protagonist is Signals.

The core of signal is an object that holds values via the value property. It has an important feature that the value of the signal object can change, but the signal itself always remains the same.

 import { signal } from "@preact/signals";

const count = signal(0);

// Read a signal’s value by accessing .value:
console.log(count.value);   // 0

// Update a signal’s value:
count.value += 1;

// The signal's value has changed:
console.log(count.value);  // 1

In Preact, when a signal is passed down as props or context, a reference to the signal is passed. This allows the signal to be updated without re-rendering the component, since the component is passed the signal object and not its value.

This allows us to skip all the expensive rendering work and immediately jump to any component that accesses the signal .value property.

Here is a comparison of the flame graph between VDOM and Signals when they are updated in Chrome, and it can be found that Signals is very fast. Signals will render faster than component tree updates because it takes far less work to update the state diagram.

Signals have a second important feature, which is that they keep track of when their values are accessed and when they are updated. In Preact, when the value of signal changes, accessing the signal's properties from within the component will automatically re-render the component.

2.3 Chestnuts

We can understand what makes Signals unique with an example:

 import { signal } from "@preact/signals";

const count = signal(0);

const App = () => {
  return (
    <Fragment>
      <h1 onClick={() => count.value++;}>
        +
        {console.log("++")}
      </h1>
      <span>{count}</span>
    </Fragment>
  );
};

When we click the plus sign 10 times, the count will change from 0 to 10, so will "++" be printed 10 times?

From our experience of writing React components, it will definitely be printed 10 times, but this is not the case in Signals.

As you can see from this Gif, "++" is not printed once, which is the uniqueness of Signals, the whole component is not re-rendered.

Not only h1 is not re-rendered, even the span node is not re-rendered, the only place to update is {count} this text node.

💡 Tip: Signal will only update when a new value is set. If the value of the setting has not changed, the update will not be triggered.

In addition to text nodes, Signals can also do fine-grained updates to DOM properties. When the plus sign is clicked, only data-id is updated, and even span in random is not executed.

 const count = signal(0);

const App = () => {
  return (
    <Fragment>
      <h1 onClick={() => count.value++;}>
        +
        {console.log("++");}
      </h1>
      <span data-id={count}>{Math.random()}</span>
    </Fragment>
  );
};

3. Installation

Signals can be installed by adding the @preact/signals package to the project:

 npm install @preact/signals

4. Usage

We will write a TodoList Demo next to learn Signals.

4.1 Create state

First you need a signal containing a to-do list, which can be represented by an array:

 import { signal } from "@preact/signals";

const todos = signal([
  { text: "Buy groceries" },
  { text: "Walk the dog" },
]);

Next, you need to allow the user to edit the input box and create a new Todo item, so you also need to create a signal for the input value, and then directly set .value to implement the modification.

 // We'll use this for our input later
const text = signal("");

function addTodo() {
  todos.value = [...todos.value, { text: text.value }];
  text.value = ""; // Clear input value on add
}

The last feature we're going to add is removing todos from the list. To do this, we'll add a function that removes a given todo item from the todos array:

 function removeTodo(todo) {
  todos.value = todos.value.filter(t => t !== todo);
}

4.2 Building the User Interface

Now that we have all the states created, we need to write the user interface, which is using Preact.

 function TodoList() {
  const onInput = event => (text.value = event.target.value);

  return (
    <>
      <input value={text.value} onInput={onInput} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.value.map(todo => (
          <li>
            {todo.text}{' '}
            <button onClick={() => removeTodo(todo)}>❌</button>
          </li>
        ))}
      </ul>
    </>
  );
}

At this point, a complete TodoList has been completed, and you can experience the complete function here .

4.3 Derived state

There is a common scene in TodoList, which is to display the number of completed items. How to design the status for this?

I believe your first reaction must be the derived state of Mobx or Vue, which happens to be in Signals.

 import { signal, computed } from "@preact/signals";

const todos = signal([
  { text: "Buy groceries", completed: true },
  { text: "Walk the dog", completed: false },
]);

// 基于其他 signals 创建衍生 signal
const completed = computed(() => {
  // 当 todos 变化,这里会自动重新计算
  return todos.value.filter(todo => todo.completed).length;
});

console.log(completed.value); // 1

4.4 Managing Global State

So far, we've created signal outside the component tree, which is fine for small applications, but can be difficult to test for large, complex applications.

Therefore, we can lift signal into the outermost component and pass it through Context .

 import { createContext } from "preact";
import { useContext } from "preact/hooks";

// 创建 App 状态
function createAppState() {
  const todos = signal([]);

  const completed = computed(() => {
    return todos.value.filter(todo => todo.completed).length
  });

  return { todos, completed }
}

const AppState = createContext();

// 通过 Context 传递给子组件
render(
  <AppState.Provider value={createAppState()}>
    <App />
  </AppState.Provider>
);

// 子组件接收后使用
function App() {
  const state = useContext(AppState);
  return <p>{state.completed}</p>;
}

4.5 Managing local state

In addition to creating state directly through signals , we can also use the provided hooks to create component internal state.

 import { useSignal, useComputed } from "@preact/signals";

function Counter() {
  const count = useSignal(0);
  const double = useComputed(() => count.value * 2);

  return (
    <div>
      <p>{count} x 2 = {double}</p>
      <button onClick={() => count.value++}>click me</button>
    </div>
  );
}

The implementation of ---ab6a843d3abc4afe3e1f7c4793ee62fa useSignal is based on signal , the principle is relatively simple, using useMemo to re-create the cache for signal when updating Added new signal .

 function useSignal(value) {
    return useMemo(() => signal(value), []);
}

4.6 Subscription changes

It can be noticed from the previous example that when accessing signal outside the component, its value is directly read, and the change of the response value is not involved.

Mobx provides autoRun to subscribe to value changes, signal which provides effect method to subscribe.

effect Receive a callback function as a parameter. When the value of the callback function depends on signal has changed, this callback function will also be re-executed

 import { signal, computed, effect } from "@preact/signals-core";

const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => `${name.value} ${surname.value}`);

// 每次名字变化的时候就打印出来
effect(() => console.log(fullName.value)); // 打印: "Jane Doe"

// 更新 name 的值
name.value = "John";
// 触发自动打印: "John Doe"

effect After execution, it will return a new function to unsubscribe.

 const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);

const dispose = effect(() => console.log(fullName.value));

// 取消订阅
dispose();

// 更新 name,会触发 fullName 的更新,但不会触发 effect 回调执行了
name.value = "John";

In rare cases, you may need to update signal effect(fn) but don't want to re-run when signal is updated, so use .peek() to get signal but not to subscribe.

 const delta = signal(0);
const count = signal(0);

effect(() => {
  // 更新 count 但不订阅变化
  count.value = count.peek() + delta.value;
});

delta.value = 1;

// 不会触发 effect 回调函数重新执行
count.value = 10;

4.7 Batch update

Sometimes we may have multiple updates at the same time, but we don't want to trigger multiple updates, so we need to merge updates like React's setState.

Signals provides the batch method that allows us to batch update signal .

Take us creating to-do items and clearing the input box as an example:

 effect(() => console.log(todos.length, text.value););

function addTodo() {
  batch(() => {
    // effect 里面只会执行一次
    todos.value = [...todos.value, { text: text.value }];
    text.value = "";
  });
}

5. Summary

Signals is a new feature of Preact recently. It is not stable at present. It is not recommended to use it in the production environment. If you want to try it, you can consider using it in small projects.

The next article will introduce the implementation principle of Signals, and will also lead you to implement a Signals from scratch.

Recommended reading

  1. Introducing Signals
  2. Signals
  3. Use Solid
  4. Comparison and principle implementation of React state management of various schools

尹光耀
2.2k 声望103 粉丝