Web3 на React: состояния интерфейса
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 26 из 30.
Перенос карточки в React не меняет смысл сетевых операций. Он меняет способ управления состоянием и жизненным циклом интерфейса. В этом уроке создадим компонент чтения, который сохраняет явные статусы и не применяет поздний ответ после изменения экземпляра. Результат — ранее изученные правила работают в декларативном представлении.
React-линия является отдельным учебным профилем: React 19.1.1 и React DOM 19.1.1, viem 2.37.9, а в следующем уроке wagmi 2.16.9 и TanStack Query 5.85.5. Это закреплённые версии, не утверждение о современных последних релизах. Зависимости не устанавливались, компонент не запускался и проект не собирался.
Состояние компонента
Компонент получает адрес экземпляра и номер курса через props. Он не выбирает сеть из window.ethereum и не создаёт запрос подписи при рендеринге. Публичный клиент остаётся прежним. Форма редактирования впоследствии будет отдельным компонентом, чтобы обновление снимка не уничтожало введённый текст.
useEffect синхронизирует компонент с внешним чтением. Результат запроса приходит позже, поэтому очистка запрещает применение ответа предыдущего эффекта. Этот приём соответствует поколению нашего DOM-контроллера, только выражен через жизненный цикл React. Он не останавливает сетевой запрос автоматически и не гарантирует честность RPC.
Состояние ready может содержать отсутствующий курс. Это по-прежнему успешное чтение нулевой записи, а не транспортная ошибка. В компоненте не смешиваем эти понятия, чтобы React не закрепил неправильную модель из-за удобной условной ветки.
Полный компонент чтения
Файл CourseCard.jsx использует ранее определённую функцию readSnapshot. Параметры экземпляра нормализованы до передачи компоненту. Этот листинг — компонент, а не полный проект со сборщиком.
import { useEffect, useState } from 'react';
import { readSnapshot } from './read-snapshot.js';
export function CourseCard({ address, courseId = 17n }) {
const [state, setState] = useState({ status: 'idle' });
const [refreshKey, setRefreshKey] = useState(0);
useEffect(() => {
let obsolete = false;
setState({ status: 'loading' });
readSnapshot(address, courseId).then(
snapshot => { if (!obsolete) setState({ status: 'ready', snapshot }); },
error => { if (!obsolete) setState({ status: 'error', error }); },
);
return () => { obsolete = true; };
}, [address, courseId, refreshKey]);
return <section aria-labelledby="react-course-title">
<h2 id="react-course-title">Курс {courseId.toString()}</h2>
{state.status === 'loading' && <p role="status">Читаем реестр…</p>}
{state.status === 'error' && <p role="status">Не удалось обновить сведения.</p>}
{state.status === 'ready' && <>
<p>{state.snapshot.active ? 'Выпуск активен' : 'Активного выпуска нет'}</p>
<p>Отпечаток: <code>{state.snapshot.contentHash}</code></p>
<p>Блок: {state.snapshot.blockNumber.toString()}</p>
</>}
<button type="button" onClick={() => setRefreshKey(key => key + 1)}>Обновить</button>
</section>;
}
Для нескольких одновременно отображаемых карточек идентификатор заголовка должен быть уникальным. Фиксированный ID подходит только одиночному компоненту упражнения. В настоящем списке используется useId или другой устойчивый способ связи области с её заголовком. Эта деталь относится к доступности, а не к блокчейну.
React выводит значения JSX как текст в этом контексте. Не используем dangerouslySetInnerHTML для заголовков манифеста или исключений. Фреймворк не превращает недоверенные URL в безопасные маршруты: ссылки всё равно проходят политику приложения. При переносе остаются все границы данных, которые мы изучили в DOM.
Очистка и повторная синхронизация
При изменении address прежний эффект очищается, и его ответ больше не вызывает setState. Новый эффект читает новый экземпляр. Если зависимость забыта, карточка может продолжать показывать старые данные под новым адресом. Это не проблема EVM, а неправильная связь состояния компонента с входными параметрами.
Объекты конфигурации не стоит создавать заново внутри каждой функции рендера, если эффект зависит от их идентичности. Иначе любое изменение локального поля формы может создавать новый объект и вызывать лишнее чтение. Передавать нормализованные примитивы или устойчивую конфигурацию обычно понятнее для небольшого упражнения.
В режиме разработки React может выполнить дополнительную последовательность установки и очистки эффекта. Она выявляет отсутствие правильной очистки; её не следует обходить глобальным флагом «никогда больше не читать». Наш пример не отправляет транзакцию в эффекте, поэтому повторная синхронизация не создаёт повторного согласия на запись.
Действие пользователя остаётся обработчиком
Подпись публикации запускается обработчиком явной кнопки, а не эффектом, наблюдающим ненулевое поле intent. Иначе восстановление состояния, повторный рендер или особенности режима разработки могли бы неожиданно открыть кошелёк. Реактивное отображение намерения и исполнение пользовательского действия имеют разные обязанности.
Для отправки сохраняем прежнюю модель: аргументы, актуальный аккаунт, сеть и поколение подготовки. Hook или компонент не отменяет необходимость повторной сверки. После хеша журнал операции находится вне краткоживущей карточки, чтобы размонтирование не уничтожало сетевую историю.
Кеш и следующий слой
Пример эффекта показывает базовый механизм, но не предоставляет общий кеш, объединение одинаковых запросов и политику повторного чтения. Эти функции можно реализовать самостоятельно или использовать библиотеку запросов. В следующем шаге wagmi и TanStack Query выразят их через согласованную конфигурацию и query key.
Важно не применять одновременно два независимых контроллера к одной карточке: старый DOM-рендер и React могут спорить за одни узлы. При переносе компонент становится владельцем своей области, а прежние модули остаются источниками данных. Такая граница позволяет заменить представление без переписывания контрактного смысла.
Для упражнения рассмотрите смену адреса во время чтения и удаление компонента до ответа. Ожидается отсутствие поздней перерисовки старого состояния. Затем объясните, почему подпись в useEffect нарушила бы явный маршрут согласия. API описан в useEffect, а правила пользовательских действий — в Responding to Events.
Черновик и серверные данные
Поле нового отпечатка хранится в локальном состоянии формы, а сетевой снимок приходит из чтения. При обновлении query data или props не следует автоматически копировать сетевой хеш в редактируемое поле: пользователь мог уже готовить другой выпуск. Для первичного заполнения задаётся отдельное явное действие, например «Использовать текущий отпечаток». После него связь становится понятной.
Если необходимо предупредить о новом выпуске, компонент показывает сообщение рядом с формой и инвалидирует подготовленную симуляцию. Он не перезаписывает пользовательский текст молча. React делает автоматическую перерисовку удобной, но именно поэтому нужно отделить источники состояния по назначению. Общая переменная course для чтения, формы и отправки быстро стирает эти границы.
В клиентском приложении важен также стабильный тип courseId. Если родитель передаёт строку, а один компонент превращает её в Number, другой в bigint, ключи и сравнения могут расходиться. Нормализация выполняется один раз на входе, после чего компоненты получают известный тип. Это сохраняет смысл предыдущего урока о точности и не зависит от фреймворка.
Родительский компонент не должен пересоздавать общий Public Client при каждом изменении текста формы. Конфигурация чтения является устойчивым ресурсом приложения; ввод пользователя меняет намерение, а не инфраструктуру сети.