React Signals: как устроены сигналы в React
Tags: react, signal
Published: 2023-04-28 13:46:48

Следуя трендам, можно было обнаружить для себя React Signals. Возможно, вам попадался такой фрагмент кода:

```jsx
import { signal } from "@preact/signals-react";

const count = signal(0);

export default function App() {
  return <button onClick={() => count.value++}>{count.value}</button>;
}
```

Код вызывает удивление — как это сделано? Почему `App` обновляется при изменении `count`? Что позволило так сделать? Похоже на хуки, но нет, хуки нельзя использовать вне компонента…

### Найдем код `@preact/signals-react`

Пакеты берутся из npm-реестра, найдем `@preact/signals-react` на npmjs.com. На [странице пакета](https://www.npmjs.com/package/@preact/signals-react) есть ссылка на репозиторий модуля на [GitHub](https://github.com/preactjs/signals). В репозитории есть несколько директорий: docs, patches, scripts, packages. Первые три нам сейчас не интересны — это документация и какие-то вспомогательные вещи, посмотрим в packages. В packages есть core, preact, react. preact — нас сейчас не интересует, core — нечто очень важное и общее, но нам нужно конкретное — react. Внутри найдем [src/index.ts](https://github.com/preactjs/signals/blob/b08732dfcb55b7e99df2fd644cb61b5b14acdb9e/packages/react/src/index.ts).

### С чего начать — код выполняемый при инициализации модуля

Разберемся, что происходит при инициализации модуля, посмотрим, что делает код, выполняемый при импорте модуля. Проигнорируем, для начала, определения функций и внутренних переменных, у нас останется:

```jsx
const JsxPro: JsxRuntimeModule = jsxRuntime;
const JsxDev: JsxRuntimeModule = jsxRuntimeDev;
React.createElement = WrapJsx(React.createElement);
JsxDev.jsx && /*   */ (JsxDev.jsx = WrapJsx(JsxDev.jsx));
JsxPro.jsx && /*   */ (JsxPro.jsx = WrapJsx(JsxPro.jsx));
JsxDev.jsxs && /*  */ (JsxDev.jsxs = WrapJsx(JsxDev.jsxs));
JsxPro.jsxs && /*  */ (JsxPro.jsxs = WrapJsx(JsxPro.jsxs));
JsxDev.jsxDEV && /**/ (JsxDev.jsxDEV = WrapJsx(JsxDev.jsxDEV));
JsxPro.jsxDEV && /**/ (JsxPro.jsxDEV = WrapJsx(JsxPro.jsxDEV));

// Decorate Signals so React renders them as <Text> components.
Object.defineProperties(Signal.prototype, {
	$$typeof: { configurable: true, value: ReactElemType },
	type: { configurable: true, value: ProxyFunctionalComponent(Text) },
	props: {
		configurable: true,
		get() {
			return { data: this };
		},
	},
	ref: { configurable: true, value: null },
});
```

Последний блок кода (с `Object.defineProperties`) добавляет всем сигналам возможность рендериться: `$$typeof`, `type`, `props`, `ref` — свойства всех React-элементов.

Остановимся подробней на первой части, в ней участвуют: `jsx`, `jsxs` (из `react/jsx-runtime`), `jsx`, `jsxs` (из `react/jsx-dev-runtime`), `createElement` из React и некая функция `WrapJsx`.

### Что такое jsx-runtime?

React-компоненты преобразуются в вызовы `React.createElement`, `jsx` или `jsxs`

Например, такой код:

```jsx
const Foo = () => (<button>Click Me</button>);

const Bar = () => (
  <div>
    <Foo />
  </div>
);
```

будет преобразован компилятором в:

```jsx
import { jsx as _jsx } from "react/jsx-runtime";
const Foo = () =>
	_jsx("button", {
    children: "Click Me"
  });
const Bar = () =>
	_jsx("div", {
    children: _jsx(Foo, {})
  });
```

В зависимости от настроек js-компилятора (например, babel), вместо `React.createElement` может быть `jsx` из `react/jsx-runtime`, будем считать их эквивалентными. 

`React.createElement` — это функция создающая  React-элемент. У нее три аргумента: `type`, `config`, `children`. Первый аргумент `type` — это тип элемента, может быть:

- строкой — для хост-элеметов: `div`, `span`, `main`;
- объектом — в случае экзотичных React-элементов: `forwardRef`, `memo`;
- функцией — для функциональных или класс-компонентов.

Второй аргумент `config` — это объект, содержащий в себе пропсы элемента, `ref` и `key`.

Последний аргумент `children` — список дочерних элементов. Точно такую же роль выполняют функции `jsx`, `jsxs`, `jsxDev` — создают React-элементы, имеют такие же аргументы. 

Вернемся к коду инициализации модуля:

```jsx
const JsxPro: JsxRuntimeModule = jsxRuntime;
const JsxDev: JsxRuntimeModule = jsxRuntimeDev;
React.createElement = WrapJsx(React.createElement);
JsxDev.jsx && /*   */ (JsxDev.jsx = WrapJsx(JsxDev.jsx));
JsxPro.jsx && /*   */ (JsxPro.jsx = WrapJsx(JsxPro.jsx));
JsxDev.jsxs && /*  */ (JsxDev.jsxs = WrapJsx(JsxDev.jsxs));
JsxPro.jsxs && /*  */ (JsxPro.jsxs = WrapJsx(JsxPro.jsxs));
JsxDev.jsxDEV && /**/ (JsxDev.jsxDEV = WrapJsx(JsxDev.jsxDEV));
JsxPro.jsxDEV && /**/ (JsxPro.jsxDEV = WrapJsx(JsxPro.jsxDEV));
```

Теперь понятно, что модуль переопределяет функции создания React-элементов результатом вызова функции `WrapJsx`. Каждая функция: `jsx`, `jsxs`, `jsxDEV`, `createElment` преобразуется с помощью `WrapJsx`.

### Что делает функция-декоратор `WrapJsx`?

Посмотрим код функции `WrapJsx`:

```jsx
function WrapJsx<T>(jsx: T): T {
	if (typeof jsx !== "function") return jsx;

	return function (type: any, props: any, ...rest: any[]) {
		if (typeof type === "function" && !(type instanceof Component)) {
			return jsx.call(jsx, ProxyFunctionalComponent(type), props, ...rest);
		}

		if (type && typeof type === "object") {
			if (type.$$typeof === ReactMemoType) {
				type.type = ProxyFunctionalComponent(type.type);
				return jsx.call(jsx, type, props, ...rest);
			} else if (type.$$typeof === ReactForwardRefType) {
				type.render = ProxyFunctionalComponent(type.render);
				return jsx.call(jsx, type, props, ...rest);
			}
		}

		if (typeof type === "string" && props) {
			for (let i in props) {
				let v = props[i];
				if (i !== "children" && v instanceof Signal) {
					props[i] = v.value;
				}
			}
		}

		return jsx.call(jsx, type, props, ...rest);
	} as any as T;
}
```

`WrapJsx` вызывается с единственным аргументом `jsx` — оригинальной функцией создания React-элемента и возвращает функцию с такими же как у `jsx` аргументами. 

`WrapJsx` обрабатывает четыре сценария:

1. Функциональный компонент или класс-компонент:

```jsx
if (typeof type === "function" && !(type instanceof Component)) {
	return jsx.call(jsx, ProxyFunctionalComponent(type), props, ...rest);
}
```

Если создается React-элемент, тип у которого — функция, значит элементу соответствует фукнциональный компонент или класс-компонент. Например, для такого jsx-выражения: `<div><Foo>text</Foo></div>` под условие выше подходит `Foo`. Внутри условия вызывается оригинальная функция `jsx`, но вместо исходного `type` передается `ProxyFunctionalComponent(type)`, это эквивалентно оборачиванию `Foo` в `ProxyFunctionalComponent`:

```jsx
const Foo = ProxyFunctionalComponent(props => {
	return (
		...
	);
})
```

1. Экзотичные React-элементы:

```jsx
if (type && typeof type === "object") {
	if (type.$$typeof === ReactMemoType) {
		type.type = ProxyFunctionalComponent(type.type);
		return jsx.call(jsx, type, props, ...rest);
	} else if (type.$$typeof === ReactForwardRefType) {
		type.render = ProxyFunctionalComponent(type.render);
		return jsx.call(jsx, type, props, ...rest);
	}
}
```

Если тип элемента — объект, значит создается экзотичный React-элемент, их два: `memo` и `forwardRef`. Оба эти элемента ссылаются на функциональный компонент, который нужно отрендерить. Элемент `memo` ссылается на компонент через свойство `type`, `forwardRef` ссылается через свойство `render`. Во фрагменте выше, они также оборачиваются в `ProxyFunctionalComponent`.

1. Хост-компоненты.

Хост-компоненты (`div`, `span`, `main` и тд) попадают в третью ветку, в этом случае `type` — это строка.

```jsx
if (typeof type === "string" && props) {
	for (let i in props) {
		let v = props[i];
		if (i !== "children" && v instanceof Signal) {
			props[i] = v.value;
		}
	}
}
```

Для таких элементов просматривают все пропсы и, если среди них есть экземпляр Сигнала, он заменяется на его значение `value`.

Таким образом, декоратор `WrapJsx` оборачивает все пользовательские компоненты в `ProxyFunctionalComponent`. При каждом обновлении (рендере) пользовательского компонента будет сначала происходить вызов `ProxyFunctionalComponent`. Так как можно быть уверенным, что это происходит в момент обновления, внутри `ProxyFunctionalComponent` можно использовать хуки, создавать локальный компоненту стейт, подписываться на события. `@preact/signals-react` использует эту возможность, чтобы отслеживать обращения к Сигналам внутри компонента и вызывать обновление компонента, когда Сигнал изменяется.