<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 18:08:11 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="P2IrWz3omfsXqbvKizfQh8gv6zhWEO1WgMhqcwRNk4zQs-Bsz5Y0m6Hqn1KHOCDwCCbGkl4nE_Q9KPAnVkp04g";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Состояние отображения (UI State) | JS: Архитектура фронтенда</title>
<meta name="description" content="Состояние отображения (UI State) / JS: Архитектура фронтенда: Знакомимся с UI-состоянием и учимся правильно его организовывать, не смешивая с данными приложения">
<link rel="canonical" href="https://ru.hexlet.io/courses/js-frontend-architecture/lessons/ui-state/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Состояние отображения (UI State)">
<meta property="og:title" content="JS: Архитектура фронтенда">
<meta property="og:description" content="Состояние отображения (UI State) / JS: Архитектура фронтенда: Знакомимся с UI-состоянием и учимся правильно его организовывать, не смешивая с данными приложения">
<meta property="og:url" content="https://ru.hexlet.io/courses/js-frontend-architecture/lessons/ui-state/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="7KOQyzCLwF25OzmZ8lX2Tx-61UzImHALjC5fnPiOTV4Dclv8wvVtPQ94HQH-WgY437P45sCvjqkxzsXIqomqMA" />
<script src="/vite/assets/inertia-BIn5nEMk.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-DOv3_-Z_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-cb8xch9l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzcyNywicHVyIjoiYmxvYl9pZCJ9fQ==--2d5cbbf5c3b4a73ae4b2c50632305d78f5872e4d/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-rafiki.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDA0MywicHVyIjoiYmxvYl9pZCJ9fQ==--e2c6c0775e2308e42fbc5dc592ba2db0470632ca/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-rafiki.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTIyNSwicHVyIjoiYmxvYl9pZCJ9fQ==--3c9f823d2a682639c9e5cb055e85378898a46a22/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-rafiki.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDc3NiwicHVyIjoiYmxvYl9pZCJ9fQ==--fb9f66ea5309a88440a060f2c88dc8495472a29e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Website%20Creator-bro.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T18:08:10.806Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"F0Gmp5H6FSwPR8ke7iR63V3wKijSOI274xzfp_utZ0D4kG2QY4S4TLkE7YbiK4qqnfkHgtoPcxle_EXzqaqALg","topics":[{"id":50517,"title":"Добрый день! Пожалуйста, помогите разобраться с причиной, почему не скрывается описание. Состояние отображения меняется четко, вывожу через console.log innerHTML контейнера, там и появляется и убирается div после нажатия на кнопки. Рендеринг все перерисовывает каждый раз. Но в web-доступе расшифровка не убирается и тесты не проходят. https://ru.hexlet.io/code_reviews/353746","plain_title":"Добрый день! Пожалуйста, помогите разобраться с причиной, почему не скрывается описание. Состояние отображения меняется четко, вывожу через console.log innerHTML контейнера, там и появляется и убирается div после нажатия на кнопки. Рендеринг все перерисовывает каждый раз. Но в web-доступе расшифровка не убирается и тесты не проходят. https://ru.hexlet.io/code_reviews/353746 ","creator":{"public_name":"Алексей Демин","id":244027,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":108246,"body":"Приветствую, Алексей!\n\nВ данном случае не принципиально, можно и выделить в отдельную функцию. Дело в том, что решение задачи подразумевает перерендер элементов вместе с обработчиками, поэтому вполне нормально, что их определение находится внутри функции render.","topic_id":50517},{"creator":{"public_name":"Алексей Демин","id":244027,"is_tutor":false},"id":108253,"body":"**Станислав Дзисяк**, спасибо! Больше вопросов не осталось)","topic_id":50517},{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":108173,"body":"Приветствую, Алексей!\n\nОбратите внимание, что в процессе работы функции render очищается содержимое container и создаются новые кнопки, которые уже не имеют обработчиков. Так как обработчики вешаются на кнопки один раз только при старте приложения. Потому повторные нажатия ни к чему не приводят. Вешайте обработчики на кнопки внутри функции render.","topic_id":50517},{"creator":{"public_name":"Алексей Демин","id":244027,"is_tutor":false},"id":108208,"body":"**Станислав Дзисяк**, здравствуйте! Большое спасибо! Скажите, а корректно ли будет разделить render на инициализирующий и отрабатывающий нажатие? Спрашиваю, потому что по теории, вроде как, правильно обработчики отделять от функции отображения. ","topic_id":50517}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":50412,"title":"Добрый день! В этом курсе неоднократно упоминалось, что рендер не должен менять состояние, нарушая принципы MVC, хотя в учительском решении меняет:\n```\nif (companyId === state.uiState.selectedCompanyId) {\n state.uiState.selectedCompanyId = null;\n}\n```\nда и обработчик в рендере не совсем вписывается в MVC, наверное...\nИ еще в курсе проскакивало где-то, что в состояние надо заносить только то, что в приложении подвержено изменениям, а в учительском решении почему-то в state попал companies, который в приложении не меняется. Хотелось бы прояснить эти моменты.","plain_title":"Добрый день! В этом курсе неоднократно упоминалось, что рендер не должен менять состояние, нарушая принципы MVC, хотя в учительском решении меняет: if (companyId === state.uiState.selectedCompanyId) { state.uiState.selectedCompanyId = null; } да и обработчик в рендере не совсем вписывается в MVC, наверное... И еще в курсе проскакивало где-то, что в состояние надо заносить только то, что в приложении подвержено изменениям, а в учительском решении почему-то в state попал companies, который в приложении не меняется. Хотелось бы прояснить эти моменты. ","creator":{"public_name":"Юрий Ткачук","id":248887,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":107953,"body":"В примере выше не render меняет состояние, а обработчик, который устанавливается в рендер. По другому быть и не может, обработчики должен кто-то вешать.\n\n> да и обработчик в рендере не совсем вписывается в MVC, наверное... \n\nЭто физически сделать по другому невозможно. Обработчики навешиваются в дом, соответственно делается это там, где меняется дом.\n\n> а в учительском решении почему-то в state попал companies, который в приложении не меняется.\n\nЭто для простоты, понятно что в реальном приложении это были бы как раз те самые данные, которыми оперирует приложение, добавляет, меняет и удаляет их.\n","topic_id":50412}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":50409,"title":"Добрый день!\nу меня аналогичная проблема как в предыдущем топике - в веб-доступе всё работает, но в тестах склеиваются названия кнопок:\n\n```\n Expected element to have text content:\n online courses\n Received:\n HexletGoogleFacebook\n```\n\nP.S. При повторном клике описание скрывается. [Ревью](https://ru.hexlet.io/code_reviews/352330)","plain_title":"Добрый день! у меня аналогичная проблема как в предыдущем топике - в веб-доступе всё работает, но в тестах склеиваются названия кнопок: Expected element to have text content: online courses Received: HexletGoogleFacebook P.S. При повторном клике описание скрывается. Ревью (https://ru.hexlet.io/code_reviews/352330) ","creator":{"public_name":"Анна Казакова","id":267128,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":107954,"body":"Это кстати интересно откуда все берут innerText, про который мы даже не рассказываем. В наших примерах только textContent.","topic_id":50409},{"creator":{"public_name":"Роман Емперор","id":290238,"is_tutor":false},"id":108007,"body":"innerText'а хватает в решениях для того, чтобы очистить содержимое элемента, например. Возможно, оттуда.","topic_id":50409},{"creator":{"public_name":"Анна Казакова","id":267128,"is_tutor":false},"id":107940,"body":"Собралась с силами и прогнала код через дебагер. Выяснилось, что e.target.innerText пуст. После замены на e.target.textContent тесты прошли.\nХотя в веб-доступе работают обе версии.\n\n[Вторая версия](https://ru.hexlet.io/code_reviews/352330?submission_id=446514)","topic_id":50409}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":50771,"title":"Добрый вечер!\n\nhttps://ru.hexlet.io/code_reviews/356904\\\nПодскажите, почему не проходят тесты? В web-доступе все работает как указано в задании!\n\nСпасибо!","plain_title":"Добрый вечер! https://ru.hexlet.io/code_reviews/356904\\ Подскажите, почему не проходят тесты? В web-доступе все работает как указано в задании! Спасибо! ","creator":{"public_name":"Denis","id":258546,"is_tutor":false},"comments":[{"creator":{"public_name":"Roman Makarov","id":181967,"is_tutor":false},"id":108682,"body":"**Denis**, добрый день!\n\nПодразумевается, что нажатие на кнопку меняет текст, а не просто добавляет новый. Из текста задания это может быть неочевидно (поправим!), поэтому в спорных случаях смотрите в тесты, они являются техзаданием и документацией.","topic_id":50771},{"creator":{"public_name":"Denis","id":258546,"is_tutor":false},"id":109260,"body":"**Roman Makarov**, ранее вы писали, что \"нажатие на кнопку меняет текст, а не просто добавляет новый\". Я подкорректировал. Теперь, если посмотреть Dev tools в web-доступе, то все нажатия на кнопки точно соответствуют заданию! Каждое нажатие создает новый div элемент с описанием (descpription), либо, в случае повторного нажатия, div элемент отсутствует. Тогда надо задание корректировать. Если я получаю верный результат, то либо тесты не верные, либо текст задания. Логично же?","topic_id":50771},{"creator":{"public_name":"Denis","id":258546,"is_tutor":false},"id":109027,"body":"**Roman Makarov**, подкорректировал. Теперь текст меняется в web-доступе, а не добавляется. Тесты не проходят снова!\\\nОшибка в тесте не соответствует выдаваемому web-доступом: при двукратном нажатии на Hexlet body не содержит 'online courses'. Что и требует тест. Но не проходит. Чего-то недопонимаю...\\\nhttps://ru.hexlet.io/code_reviews/356904","topic_id":50771},{"creator":{"public_name":"Roman Makarov","id":181967,"is_tutor":false},"id":109354,"body":"**Denis**, тесты изменили. После прохождения тестов сравните, пожалуйста, своё решение с эталонным. Не забудьте сбросить практику для получения новой версии приложения.","topic_id":50771},{"creator":{"public_name":"Roman Makarov","id":181967,"is_tutor":false},"id":109255,"body":"**Denis**, у вас происходит минимум три рендера на каждый клик, даже если клик не туда. Попробуйте поправить ошибки линтинга для начала.","topic_id":50771}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":50226,"title":"Подскажите, в чем дело. [Решение](https://ru.hexlet.io/code_reviews/349996?submission_id=443542) отлично работает в веб-доступе, но тесты не проходят.\n\nТесты выдают следующее:\n\n```\nExpected element not to have text content:\n online courses\nReceived:\n HexletGoogleFacebookonline courses\n```\n\nЯ думаю, что слушатель после первого срабатывания теряется. Но в решении учителя тот же подход: каждый раз удаляем все из контейнера и рендерим заново.\n\nЯ уже решил задачу, отрендерив изначально кнопки, чтобы слушатели оставались на месте, но все же интересно, в чем отличия.","plain_title":"Подскажите, в чем дело. Решение (https://ru.hexlet.io/code_reviews/349996?submission_id=443542) отлично работает в веб-доступе, но тесты не проходят. Тесты выдают следующее: Expected element not to have text content: online courses Received: HexletGoogleFacebookonline courses Я думаю, что слушатель после первого срабатывания теряется. Но в решении учителя тот же подход: каждыцй раз удаляем все из контейнера и рендерим заново. Я уже решил задачу, отрендерив изначально кнопки, чтобы слушатели оставались на месте, но все же интересно, в чем отличия. ","creator":{"public_name":"Андрей Забелин","id":205551,"is_tutor":false},"comments":[{"creator":{"public_name":"Sergei Melodyn","id":162475,"is_tutor":true},"id":107666,"body":"**Андрей Забелин**, приветствую.\n\nПопробуйте спросить в нашем слаке в канале #frontend или у своего наставника, если занимаетесь с ним. Менторы не занимаются отладкой и ревью кода.","topic_id":50226}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":51473,"title":"В большинстве решений учителя он создаёт html через строчки. Я - создаю элементы и по очереди их добавляю. Какой из вариантов лучше и почему? Какой используется в реальной работе?\nВ моём варианте кнопки склеились, в варианте учителя - нет.\n\n\n\nДля решающих. Тесты не через снэпшоты, ваш html может выглядеть не так, как в задании. Можно вписать id в кнопки.\n","plain_title":"В большинстве решений учителя он создаёт html через строчки. Я - создаю елементы и по очереди их добавляю. Какой из вариантов лучше и почему? Какой используется в реальной работе? В моём варианте кнопки склеились, в варианте учителя - нет. Для решающих. Тесты не через снэпшоты, ваш html может выглядеть не так, как в задании. Можно вписать id в кнопки. ","creator":{"public_name":"Александр Усанов","id":167373,"is_tutor":false},"comments":[{"creator":{"public_name":"Roman Makarov","id":181967,"is_tutor":false},"id":110137,"body":"**Александр Усанов**, добрый день.\n\nПреимущество в рендеринге из строки перед добавлением элементов _по очереди_ в том, что обновление страницы происходит за одну отрисовку. Если добавляете элементы через `createElement`, то делайте это без вставки в дом, собирая элементы в один контейнер или фрагмент, а потом добавляйте сам контейнер. У `innerHTML` есть проблема с безопасностью - не используйте его для вставки данных, которые вы не контролируете.","topic_id":51473}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":51533,"title":"https://ru.hexlet.io/code_reviews/366238\nНе понимаю, почему моя функция не проходит тесты.","plain_title":"https://ru.hexlet.io/code_reviews/366238 Не понимаю, почему моя функция не проходит тесты. ","creator":{"public_name":"V-Tan","id":313004,"is_tutor":false},"comments":[{"creator":{"public_name":"Roman Makarov","id":181967,"is_tutor":false},"id":110136,"body":"**V-tan**, добрый день.\n\nУсловие задания:\n> Повторное нажатие скрывает вывод.\n\nОбратите внимание: дом-узел не должен храниться в стейте.","topic_id":51533}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":50882,"title":"День добрый. Не понимаю, почему в решении учителя нет вью (когда он вообще нужен, как это понять?) и по клику на кнопку идет полный рендер всего содержимого контейнера. Зачем перерисовывать кнопки? \nИ буду благодарен хотя бы поверхностной обратке по моему решению. Тесты прошли и всё работает, но визуально это похоже на кашу или взрыв на фабрике плохо обученных джунов. Спасибо.\nhttps://ru.hexlet.io/code_reviews/358385","plain_title":"День добрый. Не понимаю, почему в решении учителя нет вью (когда он вообще нужен, как это понять?) и по клику на кнопку идет полный рендер всего содержимого контейнера. Зачем перерисовывать кнопки? И буду благодарен хотя бы поверхностной обратке по моему решению. Тесты прошли и всё работает, но визуально это похоже на кашу или взрыв на фабрике плохо обученных джунов. Спасибо. https://ru.hexlet.io/code_reviews/358385 ","creator":{"public_name":"Артем Прыгин","id":302498,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":108959,"body":"> Не понимаю, почему в решении учителя нет вью, и по клику на кнопку идет полный рендер всего содержимого контейнера. \n\nЕсть, это функция render. Вью это не о том КАК обрабатывать, это о том что обработкой дома занимается что-то одно, в данном случае функция render.\n\n> Зачем перерисовывать кнопки?\n\nНамного проще не париться и просто перерисовывать все с нуля. Именно так работает реакт (он сам оптимизирует вставку уже внутри себя) и об этом будет в следующем уроке.\n\n\n\n","topic_id":50882}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":61546,"title":"Мысли вслух:\n- зачем в решении учителя заложена возможность динамически перерисовывать кнопки каждый рендер? На мой взгляд, это статичные данные, которыми инициализируется приложение. А рендер можно облегчить до обновления только description компании.\n- не понятно, почему не разделены view и controller. Установка обработчика и колбэк описаны прямо в рендере. ","plain_title":"Мысли вслух: - зачем в решении учителя заложена возможность динамически перерисовывать кнопки каждый рендер? На мой взгляд, это статичные данные, которыми инициализируется приложение. А рендер можно облегчить до обновления только description компании. - не понятно, почему не разделены view и controller. Установка обработчика и колбэк описаны прямо в рендере. ","creator":{"public_name":"","id":399870,"is_tutor":false},"comments":[{"creator":{"public_name":"","id":399870,"is_tutor":false},"id":130060,"body":"Спасибо!","topic_id":61546},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129931,"body":"**user-0d72fab4be7a2997**, здравствуйте.\n\n- кнопки не получится сделать статичными, так как каждая кнопка рисуется на элемент списка компаний, то есть появляется зависимость от стейта. Не смотря на то, что список не меняется, рендер всё равно не должен об этом знать\n- view и controller - это всего лишь слои приложения, то есть это не значит что их нужно разделять физически в нашем коде. Приведу вырезку [из урока по `MVC`](https://ru.hexlet.io/courses/js-frontend-architecture/lessons/mvc/theory_unit):\n\n> Model, Controller или View — это не файлы, не классы, ни что-либо ещё конкретное. Это логические слои, которые выполняют свою задачу и определённым образом взаимодействуют друг с другом.","topic_id":61546},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":130042,"body":"**user-0d72fab4be7a2997**, очень хороший вопрос) На самом деле в том упражнении laptops тоже является состоянием. Я переделал то упражнение, чтобы оно не противоречило","topic_id":61546},{"creator":{"public_name":"","id":399870,"is_tutor":false},"id":129969,"body":"Спасибо!\n\nПодскажите, почему в упражнении к уроку \"Отрисовка (рендеринг) состояния\" laptops не являются частью состояния, а в данном уроке companies - это часть состояния?","topic_id":61546}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}},{"id":68907,"title":"Добрый день, почему в моем варианте работает только последняя 3 кнопка? Изменение ui state отслеживается.\nhttps://ru.hexlet.io/code_reviews/613719?submission_id=784960","plain_title":"Добрый день, почему в моем варианте работает только последняя 3 кнопка? Изменение ui state отслеживается. https://ru.hexlet.io/code_reviews/613719 ","creator":{"public_name":"Alexey","id":383030,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":144348,"body":"**Alexey**, приветствую! В группе уже ответил, тут продублирую: В цикле map при первом проходе div меняется. Но потом, при следующих прогонах, свитч отрабатывает на false, и содержимое div заменяется на пустую строку","topic_id":68907}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Состояние отображения (UI State)","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":1358,"slug":"js_frontend_architecture_ui_state_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"В этой практике вам нужно будет реализовать [Collapse](https://getbootstrap.com/docs/4.5/components/collapse/#example)\n\n## src/application.js\n\nРеализуйте и экспортируйте функцию по умолчанию, которая принимает на вход список компаний (пример списка в файле *src/index.js*) и использует этот список для формирования кнопок (по одной на каждую компанию). Нажатие на кнопку приводит к выводу описания компании (если есть описание другой компании, оно заменяется). Повторное нажатие удаляет вывод. Данные должны полностью удаляться, скрытие с помощью классов не подходит. По умолчанию не показывается никакое описание.\n\n\n\nПример начального вывода (может отличаться от вашего):\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n</div>\n```\n\nПосле клика на второй кнопке добавляется описание:\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n <div>search engine</div>\n</div>\n```\n\nПосле клика на третьей кнопке описание заменяется на новое:\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n <div>social network</div>\n</div>\n```\n\nПосле повторного клика на третьей кнопке описание удаляется:\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n</div>\n```\n","prepared_readme":"В этой практике вам нужно будет реализовать [Collapse](https://getbootstrap.com/docs/4.5/components/collapse/#example)\n\n## src/application.js\n\nРеализуйте и экспортируйте функцию по умолчанию, которая принимает на вход список компаний (пример списка в файле *src/index.js*) и использует этот список для формирования кнопок (по одной на каждую компанию). Нажатие на кнопку приводит к выводу описания компании (если есть описание другой компании, оно заменяется). Повторное нажатие удаляет вывод. Данные должны полностью удаляться, скрытие с помощью классов не подходит. По умолчанию не показывается никакое описание.\n\n\n\nПример начального вывода (может отличаться от вашего):\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n</div>\n```\n\nПосле клика на второй кнопке добавляется описание:\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n <div>search engine</div>\n</div>\n```\n\nПосле клика на третьей кнопке описание заменяется на новое:\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n <div>social network</div>\n</div>\n```\n\nПосле повторного клика на третьей кнопке описание удаляется:\n\n```html\n<div class=\"container m-3\">\n <button class=\"btn btn-primary\">\n Hexlet\n </button>\n <button class=\"btn btn-primary\">\n Google\n </button>\n <button class=\"btn btn-primary\">\n Facebook\n </button>\n</div>\n```\n","has_solution":true,"entity_name":"Состояние отображения (UI State)"},"units":[{"id":4216,"name":"theory","url":"/courses/js-frontend-architecture/lessons/ui-state/theory_unit"},{"id":13372,"name":"quiz","url":"/courses/js-frontend-architecture/lessons/ui-state/quiz_unit"},{"id":4220,"name":"exercise","url":"/courses/js-frontend-architecture/lessons/ui-state/exercise_unit"}],"links":[],"ordered_units":[{"id":4216,"name":"theory","url":"/courses/js-frontend-architecture/lessons/ui-state/theory_unit"},{"id":13372,"name":"quiz","url":"/courses/js-frontend-architecture/lessons/ui-state/quiz_unit"},{"id":4220,"name":"exercise","url":"/courses/js-frontend-architecture/lessons/ui-state/exercise_unit"}],"id":1874,"slug":"ui-state","state":"approved","name":"Состояние отображения (UI State)","course_order":290,"goal":"Знакомимся с UI-состоянием и учимся правильно его организовывать, не смешивая с данными приложения","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Изменение состояния фронтенд-приложения не всегда означает изменение данных, с которыми работает приложение. У данных может быть состояние, которое влияет только на внешний вид. Такое состояние называется UI-состоянием, то есть состоянием интерфейса пользователя. Его особенность в том, что оно существует только на клиенте во время взаимодействия с интерфейсом.\n\nНапример, в интернет-магазине у карточки товара может быть состояние \"в фокусе\" при наведении курсора. Это влияет только на отображение (например, карточка увеличивается или меняет цвет), но не меняет данные о товаре. Другой пример — индикатор загрузки. Когда пользователь отправляет форму, интерфейс может отображать спиннер или затемнять кнопку отправки. Это состояние актуально только во время запроса и не сохраняется в базе данных. Еще один пример — раскрытие или сворачивание списка комментариев. Сам факт того, что пользователь нажал кнопку \"Показать больше\", изменяет только локальное состояние отображения, но не сами комментарии и их содержание в базе данных.\n\nПредставьте себе обычный [аккордеон](https://getbootstrap.com/docs/5.3/components/accordion/#how-it-works). Это способ отображения данных, с помощью которого можно скрыть или раскрыть какой-то из элементов списка. Для работы подобного аккордеона нужно состояние, описывающее отображение каждого элемента: свернут/раскрыт.\n\n\n\nГде должно храниться это состояние? Например, его можно поместить внутрь самих данных:\n\n```javascript\n// Список компаний. За отображение в аккордеоне отвечает флаг visibility\nconst state = {\n companies: [\n // Данные, которые пришли с сервера\n {\n id: 1,\n name: 'Hexlet',\n description: 'Онлайн-курсы',\n visibility: 'hidden', // UI-состояние\n },\n {\n id: 2,\n name: 'Yandex',\n description: 'Поисковая система',\n visibility: 'shown', // UI-состояние\n },\n {\n id: 3,\n name: 'VK',\n description: 'Социальная сеть',\n visibility: 'hidden', // UI-состояние\n },\n ],\n}\n```\n\nГде-то дальше, в слое отображения, происходит вывод этих данных с учетом флага. Технически задача решена, но у данного способа хранения есть существенные недостатки.\n\nНачнем с главного. Данные на фронтенде не появляются из ниоткуда. Данные приложения хранятся на сервере, приходят с сервера и уходят на сервер. А сервер ничего про внешний вид не знает и знать не должен, это не касается данных. UI-состояние временное и изменяется только на клиенте. И тут возникает серьезная проблема. Если UI-состояние хранится внутри данных, то придется постоянно выполнять две вещи:\n\n- Вводить дополнительную обработку для всех приходящих данных с сервера, добавляя туда UI-состояние.\n- Вводить дополнительную обработку для всех данных, уходящих на сервер, удаляя из них все UI-состояние.\n\nА подобных элементов отображения, как правило, значительно больше, чем один. Сюда можно отнести видимость модальных окон, сортировку, скрытие/раскрытие во всех возможных проявлениях, различные режимы (редактирование), подтверждения и многое другое. Все это придется не просто хранить внутри данных, но и постоянно помнить про необходимость дополнительной обработки.\n\nНо это еще не все. Далеко не всегда весь набор данных обрабатывается одинаково. Возможно, что один набор данных выводится на странице в разных местах либо только частично. Это значит, что UI-состояние у разных элементов может быть разное, либо у каких-то элементов его может не быть вообще. Как поступать в таком случае? Игнорировать различия и добавлять всем одинаковый набор данных или усложнять логику и делать заполнение выборочным?\n\nИз-за перечисленных проблем UI-состояние хранят отдельно от самих данных. Причем для каждой ситуации это будет свой набор данных. Например, для аккордеона состояние может выглядеть так:\n\n```javascript\nconst state = {\n companies: [\n // ...\n ],\n uiState: {\n accordion: {\n 1: false, // свернут\n 2: true, // раскрыт\n 3: false, // свернут\n },\n },\n}\n```\n\nВ какой момент это состояние появляется внутри `state`? UI-состояние может формироваться как в процессе работы приложения, так и на этапе инициализации при его запуске. Например:\n\n- При запуске приложения: состояние видимости элементов аккордеона задаётся по умолчанию (`visibility: 'hidden'`).\n- Во время работы: состояние модального окна изменяется на visible, когда пользователь кликает по кнопке открытия этого окна.\n\n## Пример: Реализация аккордиона\n\nСоберем все вмести и посмотрим на код аккордиона, который демонстрирует принцип работы с UI-состоянием. В этом примере используется подход инициализации состояния по запросу (кроме первого элемента, который должен быть показан сразу).\n\n```html\n<div id=\"accordion\"></div>\n```\n\n```javascript\nconst state = {\n companies: [\n { id: 1, name: 'Hexlet', description: 'Онлайн-курсы' },\n { id: 2, name: 'Google', description: 'Поисковая система' },\n { id: 3, name: 'VK', description: 'Социальная сеть' },\n ],\n uiState: {\n accordion: { 1: true }, // по умолчанию раскрыт только первый элемент\n },\n}\n\nfunction renderAccordion() {\n const container = document.getElementById('accordion')\n container.innerHTML = ''\n\n state.companies.forEach(company => {\n const card = document.createElement('div')\n\n const header = document.createElement('div')\n header.textContent = company.name\n header.style.fontWeight = 'bold'\n header.style.cursor = 'pointer'\n header.addEventListener('click', () => toggleAccordion(company.id))\n\n const body = document.createElement('div')\n body.textContent = company.description\n body.style.display = state.uiState.accordion[company.id] ? 'block' : 'none'\n\n card.appendChild(header)\n card.appendChild(body)\n container.appendChild(card)\n })\n}\n\nfunction toggleAccordion(companyId) {\n state.uiState.accordion[companyId] = !state.uiState.accordion[companyId]\n renderAccordion()\n}\n\nrenderAccordion()\n```\n\n[Попрактиковаться](https://codepen.io/hexlet/pen/emNveOj)\n\n## Плюсы и минусы разделения\n\nОтделяя UI-состояние от основных данных, мы получаем значительные преимущества:\n\n- Упрощение взаимодействия с сервером. Данные, отправляемые и получаемые от сервера, остаются чистыми и понятными. UI-состояние обрабатывается исключительно на клиенте, без дополнительной очистки или обогащения.\n\n- Повышение гибкости. Поскольку интерфейс может иметь несколько представлений одних и тех же данных, отдельное хранение UI-состояния позволяет легко настраивать каждое представление независимо друг от друга.\n\n- Удобство отладки и тестирования. Разделение данных и UI-состояния облегчает поиск и устранение ошибок, так как можно отдельно проверять корректность данных и отдельно проверять состояние интерфейса.\n\nОднако при таком подходе появляется новая задача: нужно поддерживать синхронизацию данных и UI-состояния. Изменение данных, например, удаление компании из списка, должно приводить к соответствующим изменениям в UI-состоянии, иначе возможны ошибки и некорректное отображение.\n\n```javascript\nfunction removeCompany(companyId) {\n // Удаляем компанию\n state.companies = state.companies.filter(c => c.id !== companyId)\n\n // Синхронизируем UI-состояние\n delete state.uiState.accordion[companyId]\n}\n\n// Пример\nremoveCompany(2)\n```\n\n[Полный пример](https://codepen.io/hexlet/pen/OPVpOVb)\n\nДля автоматизации таких задач применяют:\n\n- Общую функцию-обработчик, которая управляет и данными, и UI-состоянием.\n- Реактивные инструменты управления состоянием (Redux, Zustand, MobX), автоматически синхронизирующие изменения.\n\n## Итог\n\nРазделение UI-состояния и данных позволяет снизить сложность и повысить управляемость приложения, упрощает работу с сервером и облегчает тестирование. Однако требует продуманной синхронизации между состоянием интерфейса и основными данными. Применение подходящих инструментов и практик помогает эффективно решать эти задачи.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":3526,"name":"theory","url":"/courses/js-frontend-architecture/lessons/intro/theory_unit"}],"links":[],"ordered_units":[{"id":3526,"name":"theory","url":"/courses/js-frontend-architecture/lessons/intro/theory_unit"}],"id":1648,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с курсом и его целями","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Знание JavaScript и умение работать с DOM — это базовые кирпичики, на которых строится все остальное. Они необходимы, но не достаточны для создания приложений, которые хорошо работают, легко поддерживаются и расширяются. Скорее наоборот. Работа с чистым DOM без глубокого понимания принципов организации кода буквально сразу превратится в кашу.\n\nТакой подход еще работает для тех разработчиков, которые делают небольшие виджеты, например, на jQuery. Но когда появится задача реализовать полноценное фронтенд-приложение, те подходы, которые использовались при создании виджетов, сразу начнут давать сбои. Достаточно добавить десяток-другой обработчиков, как код превратится в неподдерживаемую лапшу.\n\nК счастью, научиться строить архитектуру фронтенд-приложений не так сложно. Более того, все эти подходы были разработаны десятки лет назад, буквально тогда, когда только появились первые визуальные интерфейсы. Сейчас в это трудно поверить, но все уже придумано довольно давно.\n\nБолее того, эти подходы практически не меняются от фреймворка к фреймворку. Именно поэтому в этом курсе они даются \"сырыми\" без привязки к каким-то фреймворкам. Здесь рассказываются и изучаются глубинные подходы, которые являются определяющими в архитектуре.\n\nОсновные темы этого курса:\n\n* Управление состоянием и его организация\n* Model-View-Controller\n* Контролируемые и не контролируемые формы\n* Автоматное программирование\n* Работа с текстами. Интернационализация, локализация, плюрализация\n\nЧтобы эффективно изучать материалы этого курса, у вас должно быть представление о том, как работает JavaScript в браузере. Вы должны уметь взаимодействовать с DOM понимать как работать с асинхронными запросами:\n\n```javascript\n document.getElementById(\"addTaskButton\").addEventListener(\"click\", async function() {\n const taskInput = document.getElementById(\"taskInput\")\n const taskText = taskInput.value.trim()\n\n if (taskText) {\n // Отправка задачи на сервер\n const response = await fetch(apiUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n title: taskText,\n completed: false\n })\n })\n\n const newTask = await response.json()\n addTaskToDOM(newTask.title, newTask.id)\n taskInput.value = \"\"\n }\n})\n```\n"},"id":203,"slug":"js-frontend-architecture","challenges_count":3,"name":"JS: Архитектура фронтенда","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите фундаментальные принципы, которые используются в разработке фронтенд-приложений. Вы узнаете, как разбивать приложение на слои (MVC), выделять состояние и правильно его организовывать. Вы научитесь работать с текстами, формами и узнаете, как правильно выделять процессы. Курс пригодится, если вы решите научиться создавать легко расширяемые веб-приложения. Знания из этого курса помогут выстроить архитектуру веб-приложения без привязки к конкретным веб-фреймворкам и их особенностям.","kind":"advanced","updated_at":"2026-02-13T17:05:53.713Z","language":"javascript","duration_cache":47820,"skills":["Создавать модульные и легко расширяемые фронтенд-приложения","Правильно разделять приложения на слои и строить зависимости между ними","Структурировать состояние приложения оптимальным способом","Использовать теорию автоматов для описания происходящих процессов в коде"],"keywords":["состояние","нормализация данных","конечные автоматы","MVC","i18n"],"lessons_count":11,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ1NDksInB1ciI6ImJsb2JfaWQifX0=--d5a0bbf1770a180b2b2c0c1aae1546606f3b7340/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--39ba06fa99226096df9fc6bb31f84e1d29ea98e9/image.png"},"recommendedLandings":[{"stack":{"id":12,"slug":"frontend","title":"Фронтенд-разработчик","audience":"for_beginners","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"finished","order":20,"duration_in_months":10},"id":17,"slug":"frontend","title":"Фронтенд-разработчик","subtitle":"Изучите HTML, CSS, JavaScript и React","subtitle_for_lists":"Изучите HTML, CSS, JavaScript и React","locale":"ru","current":true,"duration_in_months_text":"10 месяцев","stack_slug":"frontend","price_text":"от 6 792 ₽","duration_text":"10 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzcyNywicHVyIjoiYmxvYl9pZCJ9fQ==--2d5cbbf5c3b4a73ae4b2c50632305d78f5872e4d/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-rafiki.png"},{"stack":{"id":43,"slug":"fullstack-javascript","title":"Fullstack-разработчик на Node.js","audience":"for_beginners","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"finished","order":140,"duration_in_months":12},"id":74,"slug":"fullstack-javascript","title":"Fullstack-разработчик на Node.js","subtitle":"Освоите JavaScript, Node.js, Fastify и React для фронтенда и бэкенда.","subtitle_for_lists":"Освоите JavaScript, Node.js, Fastify и React для фронтенда и бэкенда.","locale":"ru","current":true,"duration_in_months_text":"12 месяцев","stack_slug":"fullstack-javascript","price_text":"от 7 934 ₽","duration_text":"12 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDA0MywicHVyIjoiYmxvYl9pZCJ9fQ==--e2c6c0775e2308e42fbc5dc592ba2db0470632ca/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-rafiki.png"},{"stack":{"id":64,"slug":"middle-frontend","title":"Middle-фронтенд разработчик","audience":"for_programmers","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"not_finished","order":2006,"duration_in_months":5},"id":115,"slug":"middle-frontend","title":"Middle-фронтенд разработчик","subtitle":"Научитесь строить сложные интерфейсы и оптимизировать веб-приложения","subtitle_for_lists":"Научитесь строить сложные интерфейсы и оптимизировать веб-приложения","locale":"ru","current":true,"duration_in_months_text":"5 месяцев","stack_slug":"middle-frontend","price_text":"от 4 050 ₽","duration_text":"5 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTIyNSwicHVyIjoiYmxvYl9pZCJ9fQ==--3c9f823d2a682639c9e5cb055e85378898a46a22/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-rafiki.png"},{"stack":{"id":408,"slug":"frontend-architecture","title":"Архитектура фронтенда","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":null,"duration_in_months":1},"id":585,"slug":"frontend-architecture","title":"Архитектура фронтенда","subtitle":"Изучите архитектуру фронтенда: слои, состояние, процессы, тексты и формы","subtitle_for_lists":"Изучите архитектуру фронтенда: слои, состояние, процессы, тексты и формы","locale":"ru","current":true,"duration_in_months_text":"1 месяц","stack_slug":"frontend-architecture","price_text":"от 3 900 ₽","duration_text":"1 месяц","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDc3NiwicHVyIjoiYmxvYl9pZCJ9fQ==--fb9f66ea5309a88440a060f2c88dc8495472a29e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Website%20Creator-bro.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/js-frontend-architecture/lessons/ui-state/theory_unit","version":"143505ecd123087a8fdfa4acb7147980e9d23d76","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">JS: Архитектура фронтенда</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Состояние отображения (UI State)</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Состояние отображения (UI State)","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"JS: Архитектура фронтенда"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Изменение состояния фронтенд-приложения не всегда означает изменение данных, с которыми работает приложение. У данных может быть состояние, которое влияет только на внешний вид. Такое состояние называется UI-состоянием, то есть состоянием интерфейса пользователя. Его особенность в том, что оно существует только на клиенте во время взаимодействия с интерфейсом.</p>
<p>Например, в интернет-магазине у карточки товара может быть состояние "в фокусе" при наведении курсора. Это влияет только на отображение (например, карточка увеличивается или меняет цвет), но не меняет данные о товаре. Другой пример — индикатор загрузки. Когда пользователь отправляет форму, интерфейс может отображать спиннер или затемнять кнопку отправки. Это состояние актуально только во время запроса и не сохраняется в базе данных. Еще один пример — раскрытие или сворачивание списка комментариев. Сам факт того, что пользователь нажал кнопку "Показать больше", изменяет только локальное состояние отображения, но не сами комментарии и их содержание в базе данных.</p>
<p>Представьте себе обычный <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://getbootstrap.com/docs/5.3/components/accordion/#how-it-works" rel="noopener noreferrer" target="_blank">аккордеон</a>. Это способ отображения данных, с помощью которого можно скрыть или раскрыть какой-то из элементов списка. Для работы подобного аккордеона нужно состояние, описывающее отображение каждого элемента: свернут/раскрыт.</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ1NTEsInB1ciI6ImJsb2JfaWQifX0=--8aa89fa1a93c16d16e8e2df229e9cf6b99d6f355/bootstrap-accordion.png" alt="Bootstrap Accordion" loading="lazy"/></p>
<p>Где должно храниться это состояние? Например, его можно поместить внутрь самих данных:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// Список компаний. За отображение в аккордеоне отвечает флаг visibility
const state = {
companies: [
// Данные, которые пришли с сервера
{
id: 1,
name: 'Hexlet',
description: 'Онлайн-курсы',
visibility: 'hidden', // UI-состояние
},
{
id: 2,
name: 'Yandex',
description: 'Поисковая система',
visibility: 'shown', // UI-состояние
},
{
id: 3,
name: 'VK',
description: 'Социальная сеть',
visibility: 'hidden', // UI-состояние
},
],
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Где-то дальше, в слое отображения, происходит вывод этих данных с учетом флага. Технически задача решена, но у данного способа хранения есть существенные недостатки.</p>
<p>Начнем с главного. Данные на фронтенде не появляются из ниоткуда. Данные приложения хранятся на сервере, приходят с сервера и уходят на сервер. А сервер ничего про внешний вид не знает и знать не должен, это не касается данных. UI-состояние временное и изменяется только на клиенте. И тут возникает серьезная проблема. Если UI-состояние хранится внутри данных, то придется постоянно выполнять две вещи:</p>
<ul>
<li>Вводить дополнительную обработку для всех приходящих данных с сервера, добавляя туда UI-состояние.</li>
<li>Вводить дополнительную обработку для всех данных, уходящих на сервер, удаляя из них все UI-состояние.</li>
</ul>
<p>А подобных элементов отображения, как правило, значительно больше, чем один. Сюда можно отнести видимость модальных окон, сортировку, скрытие/раскрытие во всех возможных проявлениях, различные режимы (редактирование), подтверждения и многое другое. Все это придется не просто хранить внутри данных, но и постоянно помнить про необходимость дополнительной обработки.</p>
<p>Но это еще не все. Далеко не всегда весь набор данных обрабатывается одинаково. Возможно, что один набор данных выводится на странице в разных местах либо только частично. Это значит, что UI-состояние у разных элементов может быть разное, либо у каких-то элементов его может не быть вообще. Как поступать в таком случае? Игнорировать различия и добавлять всем одинаковый набор данных или усложнять логику и делать заполнение выборочным?</p>
<p>Из-за перечисленных проблем UI-состояние хранят отдельно от самих данных. Причем для каждой ситуации это будет свой набор данных. Например, для аккордеона состояние может выглядеть так:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">const state = {
companies: [
// ...
],
uiState: {
accordion: {
1: false, // свернут
2: true, // раскрыт
3: false, // свернут
},
},
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В какой момент это состояние появляется внутри <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">state</code>? UI-состояние может формироваться как в процессе работы приложения, так и на этапе инициализации при его запуске. Например:</p>
<ul>
<li>При запуске приложения: состояние видимости элементов аккордеона задаётся по умолчанию (<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">visibility: 'hidden'</code>).</li>
<li>Во время работы: состояние модального окна изменяется на visible, когда пользователь кликает по кнопке открытия этого окна.</li>
</ul>
<h2 id="heading-2-1">Пример: Реализация аккордиона</h2>
<p>Соберем все вмести и посмотрим на код аккордиона, который демонстрирует принцип работы с UI-состоянием. В этом примере используется подход инициализации состояния по запросу (кроме первого элемента, который должен быть показан сразу).</p>
<div class="m_5cb1b9c8 mantine-CodeHighlightTabs-root"><div style="--sa-corner-width:0px;--sa-corner-height:0px" class="m_7b14120b mantine-CodeHighlightTabs-filesScrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><div class="m_38d99e51 mantine-CodeHighlightTabs-files"><button class="mantine-focus-auto m_5cac2e62 mantine-CodeHighlightTabs-file m_87cf2631 mantine-UnstyledButton-root" data-active="true" type="button"><span>html</span></button><button class="mantine-focus-auto m_5cac2e62 mantine-CodeHighlightTabs-file m_87cf2631 mantine-UnstyledButton-root" type="button"><span>javascript</span></button></div></div></div><div data-orientation="horizontal" class="m_c44ba933 mantine-ScrollArea-scrollbar" data-hidden="true" style="position:absolute;--sa-thumb-width:18px" data-mantine-scrollbar="true"></div><div class="m_c44ba933 mantine-ScrollArea-scrollbar" data-hidden="true" data-orientation="vertical" style="position:absolute;--sa-thumb-height:18px" data-mantine-scrollbar="true"></div></div><div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlightTabs-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlightTabs-controls" data-with-offset="true"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlightTabs-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlightTabs-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlightTabs-pre" data-with-offset="true"><code class="m_5caae6d3 mantine-CodeHighlightTabs-code"><div id="accordion"></div></code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlightTabs-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div></div><p><a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://codepen.io/hexlet/pen/emNveOj" rel="noopener noreferrer" target="_blank">Попрактиковаться</a></p>
<h2 id="heading-2-2">Плюсы и минусы разделения</h2>
<p>Отделяя UI-состояние от основных данных, мы получаем значительные преимущества:</p>
<ul>
<li>
<p>Упрощение взаимодействия с сервером. Данные, отправляемые и получаемые от сервера, остаются чистыми и понятными. UI-состояние обрабатывается исключительно на клиенте, без дополнительной очистки или обогащения.</p>
</li>
<li>
<p>Повышение гибкости. Поскольку интерфейс может иметь несколько представлений одних и тех же данных, отдельное хранение UI-состояния позволяет легко настраивать каждое представление независимо друг от друга.</p>
</li>
<li>
<p>Удобство отладки и тестирования. Разделение данных и UI-состояния облегчает поиск и устранение ошибок, так как можно отдельно проверять корректность данных и отдельно проверять состояние интерфейса.</p>
</li>
</ul>
<p>Однако при таком подходе появляется новая задача: нужно поддерживать синхронизацию данных и UI-состояния. Изменение данных, например, удаление компании из списка, должно приводить к соответствующим изменениям в UI-состоянии, иначе возможны ошибки и некорректное отображение.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">function removeCompany(companyId) {
// Удаляем компанию
state.companies = state.companies.filter(c => c.id !== companyId)
// Синхронизируем UI-состояние
delete state.uiState.accordion[companyId]
}
// Пример
removeCompany(2)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p><a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://codepen.io/hexlet/pen/OPVpOVb" rel="noopener noreferrer" target="_blank">Полный пример</a></p>
<p>Для автоматизации таких задач применяют:</p>
<ul>
<li>Общую функцию-обработчик, которая управляет и данными, и UI-состоянием.</li>
<li>Реактивные инструменты управления состоянием (Redux, Zustand, MobX), автоматически синхронизирующие изменения.</li>
</ul>
<h2 id="heading-2-3">Итог</h2>
<p>Разделение UI-состояния и данных позволяет снизить сложность и повысить управляемость приложения, упрощает работу с сервером и облегчает тестирование. Однако требует продуманной синхронизации между состоянием интерфейса и основными данными. Применение подходящих инструментов и практик помогает эффективно решать эти задачи.</p></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/frontend?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">10 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">С нуля</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Фронтенд-разработчик</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите HTML, CSS, JavaScript и React</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzcyNywicHVyIjoiYmxvYl9pZCJ9fQ==--2d5cbbf5c3b4a73ae4b2c50632305d78f5872e4d/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-rafiki.png" alt="Фронтенд-разработчик" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 6 792 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/fullstack-javascript?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">12 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">С нуля</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Fullstack-разработчик на Node.js</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Освоите JavaScript, Node.js, Fastify и React для фронтенда и бэкенда.</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDA0MywicHVyIjoiYmxvYl9pZCJ9fQ==--e2c6c0775e2308e42fbc5dc592ba2db0470632ca/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-rafiki.png" alt="Fullstack-разработчик на Node.js" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 7 934 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/middle-frontend?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">5 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Middle-фронтенд разработчик</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Научитесь строить сложные интерфейсы и оптимизировать веб-приложения</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTIyNSwicHVyIjoiYmxvYl9pZCJ9fQ==--3c9f823d2a682639c9e5cb055e85378898a46a22/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-rafiki.png" alt="Middle-фронтенд разработчик" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 4 050 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/frontend-architecture?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">1 месяц</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Архитектура фронтенда</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите архитектуру фронтенда: слои, состояние, процессы, тексты и формы</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDc3NiwicHVyIjoiYmxvYl9pZCJ9fQ==--fb9f66ea5309a88440a060f2c88dc8495472a29e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Website%20Creator-bro.png" alt="Архитектура фронтенда" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/js-frontend-architecture/lessons/ui-state/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 11</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/js-frontend-architecture/lessons/ui-state/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-D8AK0-_C.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-DOv3_-Z_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>