From 028783577e061e37d252a1f7873a7694b8a288d6 Mon Sep 17 00:00:00 2001 From: Bugo Date: Wed, 18 Jun 2025 14:55:58 +0500 Subject: [PATCH 1/3] Update ru/guide/v10/context.md --- content/ru/guide/v10/context.md | 190 ++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 82 deletions(-) diff --git a/content/ru/guide/v10/context.md b/content/ru/guide/v10/context.md index f8dcee7a4..56cc262b7 100644 --- a/content/ru/guide/v10/context.md +++ b/content/ru/guide/v10/context.md @@ -38,16 +38,16 @@ export const Locale = createContext(null); > Начальное значение, установленное с помощью `createContext`, используется только в отсутствие `Provider` выше потребителя в дереве. Это может быть полезно для тестирования компонентов в изоляции, так как позволяет избежать необходимости создания обёртки `Provider` вокруг вашего компонента. ```jsx -import { createContext } from "preact"; +import { createContext } from 'preact'; -export const Theme = createContext("light"); +export const Theme = createContext('light'); function App() { - return ( - - - - ); + return ( + + + + ); } ``` @@ -55,64 +55,94 @@ function App() { ### Использование контекста -Существует два способа потребления контекста, в значительной степени в зависимости от предпочитаемого вами стиля компонентов: `Consumer` (классовые компоненты) и хук `useContext` (функциональные компоненты/хуки). +Существуют три способа использования контекста, в значительной степени в зависимости от предпочитаемого вами стиля компонентов: `static contextType` (классовые компоненты), хук `useContext` (функциональные компоненты/хуки), и `Context.Consumer` (все компоненты). + + + +```jsx +// --repl +import { render, createContext, Component } from 'preact'; + +const SomeComponent = props => props.children; +// --repl-before +const ThemePrimary = createContext('#673ab8'); + +class ThemedButton extends Component { + static contextType = ThemePrimary; + + render() { + const theme = this.context; + return ; + } +} - +function App() { + return ( + + + + + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` ```jsx // --repl -import { render, createContext } from "preact"; +import { render, createContext } from 'preact'; +import { useContext } from 'preact/hooks'; const SomeComponent = props => props.children; // --repl-before -const ThemePrimary = createContext("#673ab8"); +const ThemePrimary = createContext('#673ab8'); function ThemedButton() { - return ( - - {theme => } - - ); + const theme = useContext(ThemePrimary); + return ; } function App() { - return ( - - - - - - ); + return ( + + + + + + ); } // --repl-after -render(, document.getElementById("app")); +render(, document.getElementById('app')); ``` ```jsx // --repl -import { render, createContext } from "preact"; -import { useContext } from "preact/hooks"; +import { render, createContext } from 'preact'; const SomeComponent = props => props.children; // --repl-before -const ThemePrimary = createContext("#673ab8"); +const ThemePrimary = createContext('#673ab8'); function ThemedButton() { - const theme = useContext(ThemePrimary); - return ; + return ( + + {theme => } + + ); } function App() { - return ( - - - - - - ); + return ( + + + + + + ); } // --repl-after -render(, document.getElementById("app")); +render(, document.getElementById('app')); ``` @@ -123,43 +153,43 @@ render(, document.getElementById("app")); ```jsx // --repl -import { render, createContext } from "preact"; -import { useContext, useState } from "preact/hooks"; +import { render, createContext } from 'preact'; +import { useContext, useState } from 'preact/hooks'; const SomeComponent = props => props.children; // --repl-before const ThemePrimary = createContext(null); function ThemedButton() { - const { theme } = useContext(ThemePrimary); - return ; + const { theme } = useContext(ThemePrimary); + return ; } function ThemePicker() { - const { theme, setTheme } = useContext(ThemePrimary); - return ( - setTheme(e.currentTarget.value)} - /> - ); + const { theme, setTheme } = useContext(ThemePrimary); + return ( + setTheme(e.currentTarget.value)} + /> + ); } function App() { - const [theme, setTheme] = useState("#673ab8"); - return ( - - - - {" - "} - - - - ); + const [theme, setTheme] = useState('#673ab8'); + return ( + + + + {' - '} + + + + ); } // --repl-after -render(, document.getElementById("app")); +render(, document.getElementById('app')); ``` ## Устаревший Context API @@ -172,35 +202,31 @@ render(, document.getElementById("app")); ```jsx // --repl -import { render } from "preact"; +import { render } from 'preact'; const SomeOtherComponent = props => props.children; // --repl-before function ThemedButton(_props, context) { - return ( - - ); + return ; } class App extends Component { - getChildContext() { - return { - theme: "#673ab8" - } - } - - render() { - return ( -
- - - -
- ); - } + getChildContext() { + return { + theme: '#673ab8' + }; + } + + render() { + return ( +
+ + + +
+ ); + } } // --repl-after -render(, document.getElementById("app")); +render(, document.getElementById('app')); ``` From 52f6612d74e382fc4c3a1467ff8d4035374e9e01 Mon Sep 17 00:00:00 2001 From: Bugo Date: Wed, 18 Jun 2025 14:56:23 +0500 Subject: [PATCH 2/3] Update ru/guide/v10/server-side-rendering.md --- content/ru/guide/v10/server-side-rendering.md | 211 +++++++++++++----- 1 file changed, 150 insertions(+), 61 deletions(-) diff --git a/content/ru/guide/v10/server-side-rendering.md b/content/ru/guide/v10/server-side-rendering.md index a11952e23..4696e8ba9 100644 --- a/content/ru/guide/v10/server-side-rendering.md +++ b/content/ru/guide/v10/server-side-rendering.md @@ -21,123 +21,212 @@ Server-Side Rendering (сокращённо SSR) позволяет вывест npm install -S preact-render-to-string ``` -После выполнения указанной выше команды мы можем сразу же приступить к её использованию. +После выполнения указанной выше команды мы можем сразу же приступить к использованию рендерера. -## Пример использования +## HTML-строки -Основную функциональность лучше всего объяснить с помощью простого фрагмента: +Оба варианта ниже возвращают одну HTML-строку, представляющую полностью отрендеренный результат вашего приложения Preact. + +### renderToString + +Самый простой и прямолинейный метод рендеринга, `renderToString` преобразует дерево Preact в строку HTML синхронно. ```jsx -import render from 'preact-render-to-string'; -import { h } from 'preact'; +import { renderToString } from 'preact-render-to-string'; const name = 'пользователь Preact!' const App =
Привет, {name}
; -console.log(render(App)); +const html = renderToString(App); +console.log(html); //
Привет, пользователь Preact!
``` -## Асинхронный рендеринг с `Suspense` и `lazy` - -Вы можете столкнуться с необходимостью визуализации динамически загружаемых компонентов, например, при использовании `Suspense` и `lazy` для облегчения разделения кода (наряду с некоторыми другими случаями использования). Асинхронный рендерер будет ожидать разрешения обещаний, позволяя вам полностью сконструировать вашу HTML-строку: +### renderToStringAsync -```jsx -// page/home.js -export default () => { - return

Домашняя страница

; -}; -``` +Ожидает выполнения промисов перед возвратом полной HTML-строки. Это особенно полезно при использовании Suspense для ленивой загрузки компонентов или получения данных. ```jsx -// main.js +// app.js import { Suspense, lazy } from 'preact/compat'; -// Создание lazy-компонента -const HomePage = lazy(() => import('./pages/home')); +const HomePage = lazy(() => import('./pages/home.js')); -const Main = () => { +function App() { return ( Загрузка

}>
); -}; +} ``` -Выше приведена очень типичная настройка для приложения Preact, использующего разделение кода, без каких-либо изменений, необходимых для использования рендеринга на стороне сервера. +```jsx +import { renderToStringAsync } from 'preact-render-to-string'; +import { App } from './app.js'; -Для рендеринга мы немного отклонимся от базового примера использования и воспользуемся экспортом `renderToStringAsync` для рендеринга нашего приложения: +const html = await renderToStringAsync(); +console.log(html); +//

Домашняя страница

+``` + +> **Примечание:** К сожалению, в реализации «возобновляемой гидратации» в Preact v10 есть несколько известных ограничений — то есть гидратации, которая может приостанавливаться и ожидать загрузки и доступности JS-чанков или данных перед продолжением. Эта проблема решена в предстоящем выпуске Preact v11. +> +> На данный момент следует избегать асинхронных границ, которые возвращают 0 или более 1 DOM-узла в качестве дочерних элементов, как в следующих примерах: +> +> ```jsx +> function X() { +> // Некоторая ленивая операция, например, инициализация аналитики +> return null; +> }; +> +> const LazyOperation = lazy(() => /* import X */); +> ``` +> +> ```jsx +> function Y() { +> // Тег `` исчезает при рендеринге, оставляя два DOM-элемента `

` +> return ( +> +>

Foo

+>

Bar

+>
+> ); +> }; +> +> const SuspendingMultipleChildren = lazy(() => /* import Y */); +> ``` +> +> Для более подробного описания известных проблем и того, как мы их решили, пожалуйста, ознакомьтесь с [Hydration 2.0 (preactjs/preact#4442)](https://github.com/preactjs/preact/issues/4442). + +## HTML-потоки + +Потоковая передача — это метод рендеринга, который позволяет отправлять части вашего приложения Preact на клиент по мере их готовности, не дожидаясь завершения всего рендеринга. + +### renderToPipeableStream + +`renderToPipeableStream` — это потоковый метод, использующий [потоки Node.js](https://nodejs.org/api/stream.html) для рендеринга вашего приложения. Если вы не используете Node, вместо этого рассмотрите [renderToReadableStream](#rendertoreadablestream). ```jsx -import { renderToStringAsync } from 'preact-render-to-string'; -import { Main } from './main'; +import { renderToPipeableStream } from 'preact-render-to-string/stream-node'; + +// Синтаксис и форма обработчика запросов будут различаться в зависимости от фреймворка +function handler(req, res) { + const { pipe, abort } = renderToPipeableStream(, { + onShellReady() { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + pipe(res); + }, + onError(error) { + res.statusCode = 500; + res.send( + `

Произошла ошибка:

${error.message}
` + ); + } + }); + + // Переключаемся на клиентский рендеринг, если прошло достаточно времени. + setTimeout(abort, 2000); +} +``` -const main = async () => { - // Отрисовка lazy-компонента - const html = await renderToStringAsync(
); +### renderToReadableStream - console.log(html); - //

Домашняя страница

-}; +`renderToReadableStream` — это ещё один потоковый метод, похожий на `renderToPipeableStream`, но предназначенный для использования в средах, поддерживающих стандартизированные [веб-потоки](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API). -// Выполнение и обработка ошибок -main().catch((error) => { - console.error(error); -}); +```jsx +import { renderToReadableStream } from 'preact-render-to-string/stream'; + +// Синтаксис и форма обработчика запросов будут различаться в зависимости от фреймворка +function handler(req, res) { + const stream = renderToReadableStream(); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/html' + } + }); +} ``` -## Неглубокий рендеринг +## Настройка вывода рендера -Для некоторых целей часто предпочтительнее отображать не всё дерево, а только один его уровень. Для этого у нас есть неглубокий рендерер, который будет выводить дочерние компоненты по имени, а не по их возвращаемому значению. +Модуль `/jsx` предоставляет несколько опций для настройки вывода рендера для ряда популярных сценариев использования. + +## Режим JSX + +Режим рендеринга JSX особенно полезен, если вы занимаетесь каким-либо видом тестирования моментальных снимков. Он отображает вывод так, как если бы он был написан на JSX. ```jsx -import { shallow } from 'preact-render-to-string'; -import { h } from 'preact'; +import renderToString from 'preact-render-to-string/jsx'; -const Foo = () =>
foo
; -const App = ( -
- -
-); +const App =
; -console.log(shallow(App)); -//
+const html = renderToString(App, {}, { jsx: true }); +console.log(html); +//
``` ## Режим Pretty -Если вам нужно получить вывод в более удобном для человека виде, мы поможем вам! Передав опцию `pretty`, мы сохраним пробельные символы и отступы в выводе, как и ожидалось. +Если вам нужно получить вывод в более удобном для человека виде, мы поможем вам! При передаче опции `pretty` мы сохраним пробельные символы и отступы в выводе, как и ожидается. ```jsx -import render from 'preact-render-to-string/jsx'; -import { h } from 'preact'; +import renderToString from 'preact-render-to-string/jsx'; const Foo = () =>
foo
; const App = ( -
- -
+
+ +
); -console.log(render(App, {}, { pretty: true })); -// Лог: +const html = renderToString(App, {}, { pretty: true }); +console.log(html); //
//
foo
//
``` -## Режим JSX +### Режим Shallow -Режим рендеринга JSX особенно полезен, если вы занимаетесь каким-либо видом тестирования моментальных снимков. Он отображает вывод так, как если бы он был написан на JSX. +Для некоторых целей часто предпочтительнее не рендерить всё дерево, а только один уровень. Для этого у нас есть shallow-рендерер, который выводит дочерние компоненты по их именам, а не по возвращаемому значению. ```jsx -import render from 'preact-render-to-string/jsx'; -import { h } from 'preact'; +import renderToString from 'preact-render-to-string/jsx'; -const App =
; +const Foo = () =>
foo
; +const App = ( +
+ +
+); + +const html = renderToString(App, {}, { shallow: true }); +console.log(html); +//
+``` + +### Режим XML + +Для элементов без потомков режим XML будет рендерить их как самозакрывающиеся теги. + +```jsx +import renderToString from 'preact-render-to-string/jsx'; + +const Foo = () =>
; +const App = ( +
+ +
+); + +let html = renderToString(App, {}, { xml: true }); +console.log(html); +//
-console.log(render(App)); -// Лог:
+html = renderToString(App, {}, { xml: false }); +console.log(html); +//
``` From b43c288d7e818d69e7eac71f90eac546cda10420 Mon Sep 17 00:00:00 2001 From: Bugo Date: Wed, 18 Jun 2025 14:56:38 +0500 Subject: [PATCH 3/3] Update ru/guide/v10/signals.md --- content/ru/guide/v10/signals.md | 315 +++++++++++++++++++++++--------- 1 file changed, 224 insertions(+), 91 deletions(-) diff --git a/content/ru/guide/v10/signals.md b/content/ru/guide/v10/signals.md index c21febd09..b21152ca4 100644 --- a/content/ru/guide/v10/signals.md +++ b/content/ru/guide/v10/signals.md @@ -59,20 +59,20 @@ import { signal } from '@preact/signals'; const count = signal(0); function Counter() { - // Компонент автоматически перерисовывается при доступе к .value`: - const value = count.value; - - const increment = () => { - // Сигнал обновляется путём присвоения значения свойству `.value`: - count.value++; - }; - - return ( -
-

Счётчик: {value}

- -
- ); + // Компонент автоматически перерисовывается при доступе к .value`: + const value = count.value; + + const increment = () => { + // Сигнал обновляется путём присвоения значения свойству `.value`: + count.value++; + }; + + return ( +
+

Счётчик: {value}

+ +
+ ); } // --repl-after render(, document.getElementById('app')); @@ -89,12 +89,12 @@ import { signal } from '@preact/signals'; const count = signal(0); function Counter() { - return ( -
-

Счётчик: {count}

- -
- ); + return ( +
+

Count: {count}

+ +
+ ); } // --repl-after render(, document.getElementById('app')); @@ -127,8 +127,8 @@ const todos = signal([{ text: 'Купить продукты' }, { text: 'Выг const text = signal(''); function addTodo() { - todos.value = [...todos.value, { text: text.value }]; - text.value = ''; // Очистить входное значение при добавлении + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; // Очистить входное значение при добавлении } ``` @@ -153,8 +153,8 @@ const todos = signal([{ text: 'Купить продукты' }, { text: 'Выг const text = signal(''); function addTodo() { - todos.value = [...todos.value, { text: text.value }]; - text.value = ''; // Сбросить входное значение при добавлении + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; // Сбросить входное значение при добавлении } // Проверим, работает ли наша логика @@ -176,7 +176,7 @@ console.log(text.value); // Лог: "" ```jsx function removeTodo(todo) { - todos.value = todos.value.filter((t) => t !== todo); + todos.value = todos.value.filter(t => t !== todo); } ``` @@ -186,21 +186,21 @@ function removeTodo(todo) { ```jsx function TodoList() { - const onInput = (event) => (text.value = event.currentTarget.value); - - return ( - <> - - -
    - {todos.value.map((todo) => ( -
  • - {todo.text} -
  • - ))} -
- - ); + const onInput = event => (text.value = event.currentTarget.value); + + return ( + <> + + +
    + {todos.value.map(todo => ( +
  • + {todo.text} +
  • + ))} +
+ + ); } ``` @@ -215,14 +215,14 @@ function TodoList() { import { signal, computed } from '@preact/signals'; const todos = signal([ - { text: 'Купить продукты', completed: true }, - { text: 'Выгулять собаку', completed: false }, + { text: 'Buy groceries', completed: true }, + { text: 'Walk the dog', completed: false } ]); // Создаём сигнал, вычисляемый из других сигналов const completed = computed(() => { - // Когда `todos` изменяется, это автоматически повторяется: - return todos.value.filter((todo) => todo.completed).length; + // Когда `todos` изменяется, это автоматически повторяется: + return todos.value.filter(todo => todo.completed).length; }); // Лог: 1, потому что одна задача помечена как выполненная @@ -239,13 +239,13 @@ console.log(completed.value); ```jsx function createAppState() { - const todos = signal([]); + const todos = signal([]); - const completed = computed(() => { - return todos.value.filter((todo) => todo.completed).length; - }); + const completed = computed(() => { + return todos.value.filter(todo => todo.completed).length; + }); - return { todos, completed }; + return { todos, completed }; } ``` @@ -270,15 +270,15 @@ import { createAppState } from './my-app-state'; const AppState = createContext(); render( - - - + + + ); // ...позже, когда вам понадобится доступ к состоянию вашего приложения function App() { - const state = useContext(AppState); - return

{state.completed}

; + const state = useContext(AppState); + return

{state.completed}

; } ``` @@ -292,17 +292,17 @@ function App() { import { useSignal, useComputed } from '@preact/signals'; function Counter() { - const count = useSignal(0); - const double = useComputed(() => count.value * 2); - - return ( -
-

- {count} x 2 = {double} -

- -
- ); + const count = useSignal(0); + const double = useComputed(() => count.value * 2); + + return ( +
+

+ {count} x 2 = {double} +

+ +
+ ); } ``` @@ -356,6 +356,16 @@ name.value = 'Джон'; // Лог: "Джон Доу" ``` +Опционально, вы можете вернуть функцию очистки из колбэка, переданного в [`effect()`](#effectfn), которая будет выполнена перед следующим обновлением. Это позволяет «очистить» побочный эффект и, при необходимости, сбросить состояние для следующего вызова колбэка. + +```js +effect(() => { + Chat.connect(username.value); + + return () => Chat.disconnect(username.value); +}); +``` + Вы можете уничтожить эффект и отказаться от подписки на все сигналы, к которым он получил доступ, вызвав возвращаемую функцию. ```js @@ -382,13 +392,14 @@ name.value = 'Джон'; В тех редких случаях, когда вам нужно записать сигнал внутри [`effect(fn)`](#effectfn), но вы не хотите, чтобы эффект повторно запускался при изменении этого сигнала, вы можете использовать `.peek()`, чтобы получить текущее значение сигнала без подписки. + ```js const delta = signal(0); const count = signal(0); effect(() => { - // Обновляем `count` без подписки на `count`: - count.value = count.peek() + delta.value; + // Обновляем `count` без подписки на `count`: + count.value = count.peek() + delta.value; }); // Установка значения `delta` повторно запускает эффект: @@ -398,19 +409,21 @@ delta.value = 1; count.value = 10; ``` -> :bulb: Совет: Сценарии, в которых вы не хотите подписываться на сигнал, встречаются редко. В большинстве случаев вы хотите, чтобы ваш эффект подписывался на все сигналы. Используйте `.peek()` только тогда, когда это вам действительно нужно. +> :bulb: Совет: Сценарии, в которых вы не хотите подписываться на сигнал, встречаются редко. В большинстве случаев вы хотите, чтобы ваш эффект подписывался на все сигналы. Используйте `.peek()` только тогда, когда вам это действительно нужно. В качестве альтернативы `.peek()` у нас есть функция `untracked`, которая принимает функцию в качестве аргумента и возвращает результат выполнения этой функции. В `untracked` вы можете ссылаться на любой сигнал с помощью `.value` без создания подписки. Это может быть полезно, когда у вас есть многоразовая функция, которая обращается к `.value`, или вам нужно получить доступ к более чем одному сигналу. + + ```js const delta = signal(0); const count = signal(0); effect(() => { - // Обновляем `count` без подписки на `count` или `delta`: - count.value = untracked(() => { - return count.value + delta.value - }); + // Обновляем `count` без подписки на `count` или `delta`: + count.value = untracked(() => { + return count.value + delta.value; + }); }); ``` @@ -423,8 +436,8 @@ const todos = signal([]); const text = signal(''); function addTodo() { - todos.value = [...todos.value, { text: text.value }]; - text.value = ''; + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; } ``` @@ -432,10 +445,10 @@ function addTodo() { ```js function addTodo() { - batch(() => { - todos.value = [...todos.value, { text: text.value }]; - text.value = ''; - }); + batch(() => { + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; + }); } ``` @@ -452,12 +465,12 @@ const triple = computed(() => count.value * 3); effect(() => console.log(double.value, triple.value)); batch(() => { - // Устанавливаем `count`, делая недействительными `double` и `triple`: - count.value = 1; + // Устанавливаем `count`, делая недействительными `double` и `triple`: + count.value = 1; - // Несмотря на пакетную обработку, `double` отражает новое вычисленное значение. - // Однако `triple` будет обновляться только после завершения обратного вызова. - console.log(double.value); // Лог: 2 + // Несмотря на пакетную обработку, `double` отражает новое вычисленное значение. + // Однако `triple` будет обновляться только после завершения обратного вызова. + console.log(double.value); // Лог: 2 }); ``` @@ -471,13 +484,13 @@ batch(() => { const count = signal(0); function Unoptimized() { - // Перерисовывает компонент при изменении `count`: - return

{count.value}

; + // Перерисовывает компонент при изменении `count`: + return

{count.value}

; } function Optimized() { - // Текст автоматически обновляется без повторной отрисовки компонента: - return

{count}

; + // Текст автоматически обновляется без повторной отрисовки компонента: + return

{count}

; } ``` @@ -497,13 +510,21 @@ function Optimized() { const count = signal(0); ``` -При создании сигналов внутри компонента используйте вариант с хуком: `useSignal(initialValue)`. +Возвращённый сигнал имеет свойство `.value`, которое можно получить или установить для чтения и записи его значения. Чтобы прочитать сигнал без подписки на него, используйте `signal.peek()`. + +#### useSignal(initialValue) -Возвращенный сигнал имеет свойство `.value`, которое можно получить или установить для чтения и записи его значения. Чтобы прочитать сигнал без подписки на него, используйте `signal.peek()`. +При создании сигналов внутри компонента используйте вариант с хуком: `useSignal(initialValue)`. Он работает аналогично `signal()`, но использует мемоизацию, чтобы гарантировать использование одного и того же экземпляра сигнала при повторных рендерах компонента. + +```jsx +function MyComponent() { + const count = useSignal(0); +} +``` ### computed(fn) -Создает новый сигнал, который вычисляется на основе значений других сигналов. Возвращенный вычисленный сигнал доступен только для чтения, и его значение автоматически обновляется при изменении любых сигналов, к которым осуществляется доступ из функции обратного вызова. +Создает новый сигнал, который вычисляется на основе значений других сигналов. Возвращённый вычисленный сигнал доступен только для чтения, и его значение автоматически обновляется при изменении любых сигналов, к которым осуществляется доступ из функции обратного вызова. ```js const name = signal('Джейн'); @@ -512,8 +533,19 @@ const surname = signal('Доу'); const fullName = computed(() => `${name.value} ${surname.value}`); ``` +#### useComputed(fn) + При создании вычисляемых сигналов внутри компонента используйте вариант с хуком: `useComputed(fn)`. +```jsx +function MyComponent() { + const name = useSignal('Jane'); + const surname = useSignal('Doe'); + + const fullName = useComputed(() => `${name.value} ${surname.value}`); +} +``` + ### effect(fn) Чтобы запустить произвольный код в ответ на изменение сигнала, мы можем использовать `effect(fn)`. Подобно вычисляемым сигналам, эффекты отслеживают, к каким сигналам осуществляется доступ, и повторно запускают обратный вызов при изменении этих сигналов. В отличие от вычисляемых сигналов, `effect()` не возвращает сигнал — это конец последовательности изменений. @@ -529,8 +561,19 @@ name.value = 'Джон'; // Лог: "Привет, Джон" ``` +#### useSignalEffect(fn) + При реагировании на изменения сигнала внутри компонента используйте вариант с хуком: `useSignalEffect(fn)`. +```jsx +function MyComponent() { + const name = useSignal('Джейн'); + + // Отображаем сообщение в консоли при изменении `name`: + useSignalEffect(() => console.log('Привет, ', name.value)); +} +``` + ### batch(fn) Функцию `batch(fn)` можно использовать для объединения нескольких обновлений значений в одну «фиксацию» в конце предоставленного обратного вызова. Пакеты могут быть вложенными, а изменения сбрасываются только после завершения обратного вызова самого внешнего пакета. Доступ к сигналу, который был изменен в пакете, отразит его обновлённое значение. @@ -556,7 +599,97 @@ const surname = signal("Doe"); effect(() => { untracked(() => { - console.log(`${name.value} ${surname.value}`) + console.log(`${name.value} ${surname.value}`) }) }) ``` + +## Вспомогательные компоненты и хуки + +Начиная с версии v2.1.0, пакет `@preact/signals/utils` предоставляет дополнительные вспомогательные компоненты и хуки, упрощающие работу с сигналами. + +### Компонент Show + +Компонент `` предоставляет декларативный способ условного отображения контента на основе значения сигнала. + +```jsx +import { signal } from '@preact/signals'; +import { Show } from '@preact/signals/utils'; + +const isVisible = signal(false); + +function App() { + return ( + Здесь ничего нет

}> +

Теперь вы меня видите!

+
+ ); +} + +// Вы также можете использовать функцию для доступа к значению +function App() { + return {value =>

Значение: {value}

}
; +} +``` + +### Компонент For + +Компонент `` помогает отображать списки из массивов-сигналов с автоматическим кэшированием отрендеренных элементов. + +```jsx +import { signal } from '@preact/signals'; +import { For } from '@preact/signals/utils'; + +const items = signal(['A', 'B', 'C']); + +function App() { + return ( + Нет элементов

}> + {(item, index) =>
Элемент: {item}
} +
+ ); +} +``` + +### Дополнительные хуки + +#### useLiveSignal(signal) + +Хук `useLiveSignal(signal)` позволяет создать локальный сигнал, который остаётся синхронизированным с внешним сигналом. + +```jsx +import { signal } from '@preact/signals'; +import { useLiveSignal } from '@preact/signals/utils'; + +const external = signal(0); + +function Component() { + const local = useLiveSignal(external); + // локальное значение будет автоматически обновляться при изменении внешнего +} +``` + +#### useSignalRef(initialValue) + +Хук `useSignalRef(initialValue)` создаёт сигнал, который ведёт себя как реф React со свойством `.current`. + +```jsx +import { useSignalEffect } from '@preact/signals'; +import { useSignalRef } from '@preact/signals/utils'; + +function Component() { + const ref = useSignalRef(null); + + useSignalEffect(() => { + if (ref.current) { + console.log('Реф получил значение:', ref.current); + } + }); + + return ( +
+ Реф был прикреплён к элементу {ref.current?.tagName}. +
+ ); +} +```