<!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:28:39 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="nfgwaiPAlNjDiumE8asTe3ERNwjSE2a4S8783T10Z6lyKftd0b45uHXJzRz9pOMMsRgaotokmBr2LmaJb3OAxw";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>Нормализация данных | JS: Архитектура фронтенда</title>
<meta name="description" content="Нормализация данных / JS: Архитектура фронтенда: Знакомимся с базовыми принципами нормализации данных">
<link rel="canonical" href="https://ru.hexlet.io/courses/js-frontend-architecture/lessons/shape-of-state/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Нормализация данных">
<meta property="og:title" content="JS: Архитектура фронтенда">
<meta property="og:description" content="Нормализация данных / JS: Архитектура фронтенда: Знакомимся с базовыми принципами нормализации данных">
<meta property="og:url" content="https://ru.hexlet.io/courses/js-frontend-architecture/lessons/shape-of-state/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="WmVJtcnLfaqjXv9T-6OIclPonzdDbtjNdCGGLqErt061tIKCO7XQyhUd28v3rHgFk-GynUtZJm_JwRx68yxQIA" />
<script src="/vite/assets/inertia-DfXos102.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-BrRXra1y.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:28:39.735Z","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":"iC8T_-YGf-Wk6GnD1WeYBqUTnoOCkNfK2pmeeOSY5hhn_tjIFHjShRKrTVvZaGhxZRqzKYqnKWhneQQstp8Bdg","topics":[{"id":48953,"title":"Три дня работал с этим заданием!!! (с предыдущими 2 и 1 день соответственно) \nПоловину времени ушло на понимание задания, что от меня хотят получить в итоге, как это должно работать, чтобы прошли все тесты и было самому приятно кликать по функционалу результата:) \nПомогло чтение `Обсуждения`,где описаны проблемы, с которыми столкнулись другие ребята и ответы на них менторов. \nВ начале выполнения задания все подрывает сделать с наскока, писать код, не разобравшись хорошо в сути задания и не имея представления вообще с какой стороны подойти к его решению. \n Когда в голове сложилась хоть какая-то правильная архитектура решения, код пишется и отлаживается живей. ","plain_title":"Три дня работал с этим заданием!!! (с предыдущими 2 и 1 день соответственно) Половину времени ушло на понимание задания, что от меня хотят получить в итоге, как это должно работать, чтобы прошли все тесты и было самому приятно кликать по функционалу результата:) Помогло чтение Обсуждения,где описаны проблемы, с которыми столкнулись другие ребята и ответы на них менторов. В начале выполнения задания все подрывает сделать с наскока, писать код, не разобравшись хорошо в сути задания и не имея представления вообще с какой стороны подойти к его решению. ","creator":{"public_name":"Sergey Kirillov","id":302329,"is_tutor":false},"comments":[{"creator":{"public_name":"Sergei Melodyn","id":162475,"is_tutor":true},"id":105100,"body":"**Sergey Kirillov**, приветствую.\n\nЗдорово что всё получилось!\n\n> Половину времени ушло на понимание задания, что от меня хотят получить в итоге, как это должно работать\n\nВы можете написать что было непонятным? Тогда мы сможем переписать задание на более очевидное.","topic_id":48953}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":40806,"title":"Добрый день, вот [ревью](https://ru.hexlet.io/code_reviews/245351)\nВроде все работает, но есть несколько вопросов:\n1) почему, когда я использовал метод innerText дял добавления названий элементов списка, мои тесты не проходились, хотя в веб-окружении было все норм, в снепшотах были пустые элементы такого типа <li><b></b></li>\n2) и второй вопрос, правильный ли я выбрал подход для передачи данных через состояние, возможно следовало их передавать через другие параметры?\nPS: предыдущее задание написал настолько криво, нарушив кучу концепций, судя по замечаниям ментора, что это обдумывал около недели перед тем как сесть и сделать, скажите что я исправился хотя бы чуть чуть :)\nвот предыдущие [замечания](http://prntscr.com/rviqth)","plain_title":"Добрый день, вот ревью (https://ru.hexlet.io/code_reviews/245351) Вроде все работает, но есть несколько вопросов: 1) почему, когда я использовал метод innerText дял добавления названий элементов списка, мои тесты не проходились, хотя в веб-окружении было все норм, в снепшотах были пустые элементы такого типа 2) и второй вопрос, правильный ли я выбрал подход для передачи данных через состояние, возможно следовало их передавать через другие параметры? PS: предыдущее задание написал настолько криво, нарушив кучу концепций, судя по замечаниям ментора, что это обдумывал около недели перед тем как сесть и сделать, скажите что я исправился хотя бы чуть чуть :) вот предыдущие замечания enter image description here http://prntscr.com/rviqth ","creator":{"public_name":"Сулейман Алиев","id":167506,"is_tutor":false},"comments":[{"creator":{"public_name":"Сулейман Алиев","id":167506,"is_tutor":false},"id":89230,"body":"**Kirill Mokevnin**, спасибо за ответ!, режимы решил ввести для того что в процессе рендеринга не перерисовывать всю страницу, когда мы например добавляем канал новый или таску, а работать только с тем контейнером куда нужно добавить, ну или в случае переключения каналов, перерисовываем и каналы и задания","topic_id":40806},{"creator":{"public_name":"Сулейман Алиев","id":167506,"is_tutor":false},"id":89323,"body":"ааа, спасибо, точно помнил там есть сдвоенная буква, а какая именно забыл :D","topic_id":40806},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":89297,"body":"Слова Chanell не существует\n\n> chHtml\n\nПро подобные сокращения https://ru.hexlet.io/blog/posts/naming-errors-1\n\n\n","topic_id":40806},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":89189,"body":"> почему, когда я использовал метод innerText дял добавления названий элементов списка, мои тесты не проходились, хотя в веб-окружении было все норм, в снепшотах были пустые элементы такого типа <li><b></b></li>\n\nСложно сказаь, можете показать пример ошибки на каком нибудь сервисе типа codepen?\n\nПо поводу кода. Сразу бросаются в глаза орфографические ошибки в названиях. \n\nПо архитектуре вижу усложнение в создании понятия \"режим\" (mode). Но у нас фактически нет никаких режимов в приложении. То есть это искуственное усложнение.\n\nЧто касается передачи состояния, то это ок. \n\nВ остальном очень важно изучить решение учителя и понять почему у вас получилось настолько больше кода и он сложнее. Прогресс все равно хороший!\n\n","topic_id":40806},{"creator":{"public_name":"Сулейман Алиев","id":167506,"is_tutor":false},"id":89274,"body":"**Kirill Mokevnin**, \n> По поводу кода. Сразу бросаются в глаза орфографические ошибки в названиях.\n\nа можно пожалуйста примеры. хоть один, а то пишу в полной уверенности что правильно именую функции","topic_id":40806}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":49855,"title":"Здравствуйте! В Веб доступе все работает, но в тестах такая ошибка: \nTestingLibraryElementError: Unable to find an accessible element with the role \"link\" and name \"Random\"\n\nТо есть, у меня должен присутствовать элемент \"Random\", который попадает с список листов тогда, когда пользователь в браузере добавляет такой (любой) новый элемент в список? Не понимаю, как это реализовать. Спасибо.\n\nhttps://ru.hexlet.io/code_reviews/345481\n","plain_title":"Здравствуйте! В Веб доступе все работает, но в тестах такая ошибка: TestingLibraryElementError: Unable to find an accessible element with the role \"link\" and name \"Random\" То есть, у меня должен присутствовать элемент \"Random\", который попадает с список листов тогда, когда пользователь в браузере добавляет такой (любой) новый элемент в список? Не понимаю, как это реализовать. Спасибо. https://ru.hexlet.io/code_reviews/345481 ","creator":{"public_name":"Оксана Дуга","id":186305,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":106937,"body":"Приветствую, Оксана!\n\nВ целом с вашим решением всё хорошо. Сейчас тесты падают потому, что тег `a` должен содержать обязательный атрибут [href](https://developer.mozilla.org/ru/docs/Web/HTML/Element/A). Добавьте его и тесты пройдут, используйте метод `setAttribute`.","topic_id":49855}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":41212,"title":"[Сделал](https://ru.hexlet.io/code_reviews/249743). И опять столкнулся с интересным поведением `innerText` vs `innerHTML`. В версии 3 ревью использую `innerText` для установки имени листа внутрь тега. При запуске тестов текст внутри отсутствует. Заменяю на `innerHTML` — работает. Какая-то странная семантика, я ведь не HTML устанавливаю, а именно текст. Или я чего-то не понимаю?","plain_title":"Сделал (https://ru.hexlet.io/code_reviews/249743). И опять столкнулся с интересным поведением innerText vs innerHTML. В версии 3 ревью использую innerText для установки имени листа внутрь тега. При запуске тестов текст внутри отсутствует. Заменяю на innerHTML — работает. Какая-то странная семантика, я ведь не HTML устанавливаю, а именно текст. Или я чего-то не понимаю? ","creator":{"public_name":"Artem Kalachian","id":144,"is_tutor":false},"comments":[{"creator":{"public_name":"Artem Kalachian","id":144,"is_tutor":false},"id":90018,"body":"**Kirill Mokevnin**, гм, действительно. Ну, в доке же написано:\n> innerText is easily confused with Node.textContent\n\nвот я и оконфузился)","topic_id":41212},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":89990,"body":"Только непонятно почему innerText? Мы ведь в курсе рассказывали про textContent. Нужно использовать именно его.","topic_id":41212}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":44237,"title":"Здравствуйте! \nНекоторые тесты не проходят, при этом на странице всё отрабатывает корректно. \nМое решение: https://ru.hexlet.io/code_reviews/280334 \nВ снепшотах заметил остатки value в полях ввода, в моей реализации они очищаются после отправки формы. Возможно из-за этого тесты не проходят, хотя в выводе указывается, что ожидается вторая задача в списке, но она нормально добавляется на странице. \nЧто-то, у кого-то не так. Прошу помощи. ","plain_title":"Здравствуйте! Некоторые тесты не проходят, при этом на странице всё отрабатывает корректно. Мое решение: https://ru.hexlet.io/code_reviews/280334 В снепшотах заметил остатки value в полях ввода, в моей реализации они очищаются после отправки формы. Возможно из-за этого тесты не проходят, хотя в выводе указывается, что ожидается вторая задача в списке, но она нормально добавляется на странице. Что-то, у кого-то не так. Прошу помощи. ","creator":{"public_name":"Владимир Коченов","id":174562,"is_tutor":false},"comments":[{"creator":{"public_name":"Sergei Melodyn","id":162475,"is_tutor":true},"id":95738,"body":"**Владимир Коченов**, приветствую.\n\nТесты опираются на снепшоты, которые требуют конкретной реализации, то есть если в веб-доступе работает, а тесты падают, то проблема в реализации. Лучший путь к решению - это открыть снепшоты, воспользоваться отладочной печатью и довести своё решение до соответствующего ожидаемому. Чтобы было проще отлаживать свой код и находить в нём ошибки, ознакомьтесь с нашим гайдом: https://help.hexlet.io/article/7-how-to-debug-code","topic_id":44237}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":42433,"title":"Здравствуйте, код, конечно на костылях, но в WEBе отрабатывает. Логику проверял по-всякому, все работатет. А вот тесты сыпятся. \nhttps://ru.hexlet.io/code_reviews/262609\nПроверяю действия тестов в ручном режиме - все норм, HTML-код совпадает. А вот тесты запарываются. Видимо, как в анекдоте - есть нюанс?","plain_title":"Здравствуйте, код, конечно на костылях, но в WEBе отрабатывает. Логику проверял по-всякому, все работатет. А вот тесты сыпятся. https://ru.hexlet.io/code_reviews/262609 Проверяю действия тестов в ручном режиме - все норм, HTML-код совпадает. А вот тесты запарываются. Видимо, как в анекдоте - есть нюанс? ","creator":{"public_name":"Максим Колотушкин","id":252539,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":92600,"body":"Приветствую, Максим!\n\nДля доступа к элементам формы нужно использовать свойство [elements](https://developer.mozilla.org/ru/docs/Web/API/HTMLFormElement/elements). То есть, на примере вашего решения вот так: `taskForm.elements.name`.","topic_id":42433}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":43216,"title":"У меня получилось все иначе: https://ru.hexlet.io/code_reviews/271233\nна сколько это верно?","plain_title":"У меня получилось все иначе: https://ru.hexlet.io/code_reviews/271233 на сколько это верно? ","creator":{"public_name":"","id":241738,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":94102,"body":"**defaktor**, приветствую!\n\nМенторы помогают справиться с трудностями в процессе обучения, но не занимаются оценкой кода. Для проверки в упражнении есть тесты и линтер. Если тесты проходят и замечаний линтер не даёт, то решение как минимум неплохое.\n\nСравнивайте своё решение с решением учителя. Если оно работает иначе, разберитесь как, и задавайте вопросы, если что-то будет непонятно.\n\nА ещё недавно у нас появился премиум-план. Там у вас будет персональный наставник, которого, в том числе можно попросить разобрать код.\n\nПро премиум - https://help.hexlet.io/article/46-what-is-premium-plan. На какие вопросы отвечают менторы - https://help.hexlet.io/article/39-mentors","topic_id":43216}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":37240,"title":"https://ru.hexlet.io/code_reviews/202375\n```\nВ решении учителя нет кода по обработкам ошибок, это секретная информация?\nИли она не нужна в принципе? \n```","plain_title":"https://ru.hexlet.io/code_reviews/202375 В решении учителя нет кода по обработкам ошибок, это секретная информация? Или она не нужна в принципе? ","creator":{"public_name":"Юрий Шошин","id":188408,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":81619,"body":"Обработка ошибок не просто нужна, это само по себе очень важно! В данном примере я намеренно их убрал, иначе бы решение получилось слишком сложным (там и так не просто).","topic_id":37240}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":43872,"title":"Добрый день. Возник вопрос по тестам: в последнем снепшоте ожидается состояние по-умолчанию? Тесты [падают](https://ru.hexlet.io/code_reviews/278461) предсказуемо во всех тестах, кроме последнего. Если в этом снепшоте ожидается состояние по-умолчанию, то как его можно получить?","plain_title":"Добрый день. Возник вопрос по тестам: в последнем снепшоте ожидается состояние по-умолчанию? Тесты падают (https://ru.hexlet.io/code_reviews/278461) предсказуемо во всех тестах, кроме последнего. Если в этом снепшоте ожидается состояние по-умолчанию, то как его можно получить? ","creator":{"public_name":"Никита Узаков","id":245848,"is_tutor":false},"comments":[{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":95495,"body":"Отлично Никита, что разобрались.\n\n> state был объявлен в модуле глобально, что вело к его мутации тоже на глобальном уровне\n\nАга верно, так делать не нужно. ","topic_id":43872},{"creator":{"public_name":"Никита Узаков","id":245848,"is_tutor":false},"id":95308,"body":"Немного [переделал](https://ru.hexlet.io/code_reviews/278461), стало лучше, но вот главный вопрос остался не решенным.","topic_id":43872},{"creator":{"public_name":"Никита Узаков","id":245848,"is_tutor":false},"id":95483,"body":"Разобрался в чем была проблема: state был объявлен в модуле глобально, что вело к его мутации тоже на глобальном уровне, так делать не надо.","topic_id":43872},{"creator":{"public_name":"Никита Узаков","id":245848,"is_tutor":false},"id":95457,"body":"**Станислав Дзисяк**, спасибо, немного прояснилось в голове. Только не могу понять с чем может быть связана такая ошибка, можете подсказать в какую сторону посмотреть?","topic_id":43872},{"creator":{"public_name":"Stanislav Dzisiak","id":212236,"is_tutor":true},"id":95452,"body":"Приветствую, Никита!\n\nЯ так понимаю вы спрашиваете о тесте `'application 2'`. Обратите внимание на вызов функции beforeEach() в начале тестов. Она запускается перед каждым тестом, и учитывая что в данном тесте никаких манипуляций с DOM не происходит, поэтому ожидается состояние по умолчанию.","topic_id":43872}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}},{"id":41046,"title":"Добрый день.\n\nhttps://ru.hexlet.io/code_reviews/248266\n\nВ веб-доступе все работает так как написано в задании. Но тесты не проходят.\n\nПодскажите, пожалуйста, в чем дело? Такая же ситуация была с предыдущим заданием, пришлось смотреть решение учителя.\n\nИ если вынести функции `createGeneral()`, `render()` и переменные `lists` и `tasks`, тогда ни один тест не проходит, хотя опять же в веб-доступе все работает. \nПришлось все это запихнуть в одну функцию. Почему так происходит?","plain_title":"Добрый день. https://ru.hexlet.io/code_reviews/248045 В веб-доступе все работает так как написано в задании. Но тесты не проходят. Подскажите, пожалуйста, в чем дело. Такая же ситуация была с предыдущим заданием, пришлось смотреть решение учителя. ","creator":{"public_name":"Андрей Мягков","id":251038,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":89572,"body":"> так и пытался отлаживать. Проходился по действиям из тестов и сверял со своим кодом, вывод получался тот, который требуют от этого тесты.\n\nЭТо только первая часть отладки, а выше я писал и про вторую. Чтобы отладить до конца, вам нужно понаставить выводов и запускать тесты и смотреть что код правильно все делает не только в браузерах, но и в тесте. А, так как, он в тестах работает неверно, то вы увидите в какой момент состояние не такое как ожидается.","topic_id":41046},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":89565,"body":"Как я уже писал в прошлом топике. Невозможно просто взять посмотреть на код и сказать что не так, тут он сложный и его нужно отлаживать. Вот смотрите:\n\n```\n - <li><a href=\"#general\">General</a></li>\n + <li><b>General</b></li>\n - <li><b>Random</b></li>\n + <li><a href=\"#random\">Random</a></li>\n```\n\nВ первую очередь надо понять что ожидали тесты и что получилось в вашем коде. Дальше нужно понять как пришли к этому тесты, и что они для этого делали. Потом посмотреть почему ваш код в какой-то момент не делает то что надо. Для этого надо все выводить на экран и следить за тем как меняется состояние. ","topic_id":41046},{"creator":{"public_name":"Андрей Мягков","id":251038,"is_tutor":false},"id":89568,"body":"**Kirill Mokevnin**, так и пытался отлаживать. Проходился по действиям из тестов и сверял со своим кодом, вывод получался тот, который требуют от этого тесты.\nК сожалению, придется глупо скопировать решение учителя, чтобы прошли тесты)\nТестирование фронтенда - это, конечно, то еще дело.","topic_id":41046}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Нормализация данных","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":1056,"slug":"js_frontend_architecture_shape_of_state_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"_Эта задача не сложная алгоритмически, но довольно объемная. На решение потребуется время и это хорошая прокачка_\n\nВ этой задаче вам предстоит сделать список задач похожий на [Reminders из MacOS](https://bit.ly/38GX3wI). Reminder позволяет планировать задачи и создавать списки задач.\n\n\n\nПо умолчанию, в нашей реализации сразу должен быть создан список General. Начальный HTML доступен в _public/index.html_. После инициализации js он становится таким (туда добавляется General):\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><b>General</b></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\"></div>\n </div>\n</div>\n```\n\nПосле добавления первой задачи в список General:\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><b>General</b></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\">\n <ul>\n <li>My First Task</li>\n </ul>\n </div>\n </div>\n</div>\n```\n\nПосле создания нового списка (но до переключения на него):\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><b>General</b></li>\n <li><a href=\"#random\">Random</a></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\">\n <ul>\n <li>My First Task</li>\n </ul>\n </div>\n </div>\n</div>\n```\n\nПосле переключения на список Random (клик по имени):\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><a href=\"#general\">General</a></li>\n <li><b>Random</b></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\"></div>\n </div>\n</div>\n```\n\nСписки должны иметь уникальные имена. Добавление списка с уже существующим именем не должно производить никакого эффекта.\n\n### src/application.js\n\nЭкспортируйте функцию по умолчанию, которая реализует всю необходимую логику.\n","prepared_readme":"_Эта задача не сложная алгоритмически, но довольно объемная. На решение потребуется время и это хорошая прокачка_\n\nВ этой задаче вам предстоит сделать список задач похожий на [Reminders из MacOS](https://bit.ly/38GX3wI). Reminder позволяет планировать задачи и создавать списки задач.\n\n\n\nПо умолчанию, в нашей реализации сразу должен быть создан список General. Начальный HTML доступен в _public/index.html_. После инициализации js он становится таким (туда добавляется General):\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><b>General</b></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\"></div>\n </div>\n</div>\n```\n\nПосле добавления первой задачи в список General:\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><b>General</b></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\">\n <ul>\n <li>My First Task</li>\n </ul>\n </div>\n </div>\n</div>\n```\n\nПосле создания нового списка (но до переключения на него):\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><b>General</b></li>\n <li><a href=\"#random\">Random</a></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\">\n <ul>\n <li>My First Task</li>\n </ul>\n </div>\n </div>\n</div>\n```\n\nПосле переключения на список Random (клик по имени):\n\n```html\n<div class=\"row\">\n <div class=\"col\">\n <h3>Lists</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-list-form\">\n <label for=\"new-list-name\" class=\"sr-only\">New list name</label>\n <input\n type=\"text\"\n id=\"new-list-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add List\" />\n </form>\n <div data-container=\"lists\">\n <ul>\n <li><a href=\"#general\">General</a></li>\n <li><b>Random</b></li>\n </ul>\n </div>\n </div>\n <div class=\"col\">\n <h3>Tasks</h3>\n <form class=\"form-inline mb-2\" data-container=\"new-task-form\">\n <label for=\"new-task-name\" class=\"sr-only\">New task name</label>\n <input\n type=\"text\"\n id=\"new-task-name\"\n class=\"form-control mr-2\"\n name=\"name\"\n required\n />\n <input type=\"submit\" class=\"btn btn-primary\" value=\"Add Task\" />\n </form>\n <div data-container=\"tasks\"></div>\n </div>\n</div>\n```\n\nСписки должны иметь уникальные имена. Добавление списка с уже существующим именем не должно производить никакого эффекта.\n\n### src/application.js\n\nЭкспортируйте функцию по умолчанию, которая реализует всю необходимую логику.\n","has_solution":true,"entity_name":"Нормализация данных"},"units":[{"id":3527,"name":"theory","url":"/courses/js-frontend-architecture/lessons/shape-of-state/theory_unit"},{"id":13373,"name":"quiz","url":"/courses/js-frontend-architecture/lessons/shape-of-state/quiz_unit"},{"id":3606,"name":"exercise","url":"/courses/js-frontend-architecture/lessons/shape-of-state/exercise_unit"}],"links":[],"ordered_units":[{"id":3527,"name":"theory","url":"/courses/js-frontend-architecture/lessons/shape-of-state/theory_unit"},{"id":13373,"name":"quiz","url":"/courses/js-frontend-architecture/lessons/shape-of-state/quiz_unit"},{"id":3606,"name":"exercise","url":"/courses/js-frontend-architecture/lessons/shape-of-state/exercise_unit"}],"id":1649,"slug":"shape-of-state","state":"approved","name":"Нормализация данных","course_order":350,"goal":"Знакомимся с базовыми принципами нормализации данных","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"При разработке приложения часто возникают сложности с удобством доступа к вложенным данным. Например, сложно быстро извлечь последние комментарии к вопросам в Q&A (Вопросы и Ответы), если комментарии вложены внутрь самих вопросов. В таком случае необходимо обойти все вопросы, собрать комментарии и отсортировать их по времени.\n\nВозьмем для примера [Q&A Хекслета](https://ru.hexlet.io/qna). У каждого вопроса есть количество лайков и комментарии, у которых в свою очередь тоже есть лайки. Эти данные можно представить так:\n\n```javascript\nconst posts = [\n {\n id: 1,\n text: 'Как писать код?',\n likesCount: 2,\n likedBy: ['user1', 'user3'],\n comments: [\n {\n answer: 'Открой редактор!',\n likesCount: 1,\n createdAt: '11-12-2022',\n },\n {\n answer: 'Сидя!',\n likesCount: 3,\n createdAt: '11-12-2022',\n },\n ],\n },\n {\n id: 2,\n text: 'Что лучше: vim или emacs?',\n likesCount: 2,\n likedBy: ['user2', 'user3'],\n comments: [\n {\n answer: 'FAR зе бест!',\n likesCount: 100,\n createdAt: '11-12-2022',\n },\n ],\n },\n]\n\nconst state = { posts }\n```\n\nИерархическое представление данных хорошо отражает их структуру. Сразу видно, что к чему относится. Данные удобно выводить и достаточно удобно изменять. Особенно если вывод на экране совпадает с их структурой, и данные между собой не пересекаются. Вопросы и Ответы Хекслета как раз такой пример. Каждый вопрос живет своей независимой жизнью (кое-какие зависимости есть, но они не касаются самих данных).\n\nОднако, если данные связаны, то иерархическая структура усложняет сквозные выборки. Представьте себе, что надо выводить 10 последних комментариев. Как это сделать? Придется ходить по всем топикам, брать все комментарии, объединять и искать самые свежие.\n\n```javascript\nconst comments = posts.flatMap(p => p.comments)\nconst sortedComments = comments.sort(\n (c1, c2) => new Date(c2.createdAt) - new Date(c1.createdAt),\n)\nconst lastComments = sortedComments.slice(0, 10)\n```\n\nТут стоит сказать, что большая часть нужных выборок должна делать на бекенде и отправляться клиенту в готовом виде. Если же нужно делать такие расчеты на клиенте, то к организации объекта состояния можно подойти с другой стороны.\n\n## Создание индексов\n\nОдин из способов выйти из этой ситуации — начать дублировать данные. Создавать дополнительные структуры, оптимизированные под конкретные задачи. И хотя, в общем, это не лишено смысла, все же ручной способ поддерживать эти структуры ничего хорошего не принесет. В тех же базах данных за формирование индексов отвечает сама база данных. Нам как программистам не надо об этом заботиться. А здесь придется внедрять дополнительную синхронизацию во все этапы: добавление, изменение и удаление.\n\nНиже пример создания объекта, оптимизированного под лайкнутые посты конкретными пользователями. Выборка вопросов, которые лайкнул конкретный пользователь (`user1`), превращается в простую операцию извлечения данных из объекта по ключу.\n\n```javascript\n// Отдельная структура под хранение id постов, которые лакйнул каждый пользователь\nconst userLikesIndex = {\n user1: [1, 2],\n user2: [1, 2],\n user3: [1],\n user4: [1, 2],\n}\n\nconst postsLikedByUser1 = userLikesIndex['user1']\nconsole.log(postsLikedByUser1)\n```\n\nНо тут возникает новая проблема: синхронизация данных. При изменении лайков (например, пользователь убрал лайк), нужно обновить оба объекта:\n\n```javascript\nfunction removeLike(userId, postId) {\n const post = state.posts.find(p => p.id === postId)\n post.likes = post.likes.filter(uId => uId !== userId)\n\n userLikesIndex[userId] = userLikesIndex[userId].filter(\n pId => pId !== postId,\n )\n}\n\n// Пример удаления лайка:\nremoveLike('user1', 'post2')\n```\n\nЭти действия нужно выполнять при каждом изменении (добавление, удаление, изменение), и это легко приводит к ошибкам и рассогласованности данных.\n\nИменно поэтому клиентское приложение редко занимается такими вещами вручную. Подобные структуры (индексы) автоматически создаются и поддерживаются на бэкенде или с помощью специальных библиотек управления состоянием. А сейчас активно развиваются полноценные клиентские базы данных, которые умеют с этим работать.\n\n## Нормализация данных\n\nДругой способ – нормализовать данные, прямо как в реляционных базах данных. Представить их плоскими массивами. Например, так:\n\n```javascript\nconst posts = [\n {\n id: 1,\n text: 'Как писать код?',\n },\n {\n id: 2,\n text: 'Что лучше: vim или emacs?',\n },\n]\n\n// Для работы с нормализованными данными необходимо\n// использовать идентификаторы для связи сущностей\n\nconst comments = [\n {\n id: 1,\n postId: 1,\n answer: 'Открой редактор!',\n likesCount: 1,\n createdAt: '11-12-2022',\n },\n {\n id: 2,\n postId: 1,\n answer: 'Сидя!',\n likesCount: 3,\n createdAt: '11-12-2022',\n },\n {\n id: 3,\n postId: 2,\n answer: 'FAR зе бест!',\n likesCount: 100,\n createdAt: '11-12-2022',\n },\n]\n\nconst postLikes = [\n { postId: 1, userId: 'user1' },\n { postId: 1, userId: 'user3' },\n { postId: 2, userId: 'user2' },\n { postId: 2, userId: 'user3' },\n]\n```\n\nНормализованные данные существенно упрощают выполнение разнообразных выборок. Например, получение последних комментариев ко всем постам теперь выглядит проще и быстрее:\n\n```javascript\nconst sortedComments = state.comments.sort(\n (a, b) => new Date(b.createdAt) - new Date(a.createdAt),\n)\nconst lastComments = sortedComments.slice(0, 10)\n```\n\nТакже упрощается получение постов, лайкнутых конкретным пользователем:\n\n```javascript\nconst postsLikedByUser1 = state.postLikes.filter(\n like => like.userId === 'user1',\n)\n```\n\nНормализованные данные тоже требуют синхронизации, но в основном для удаления. В нашем примере удаление поста должно приводить к удалению комментариев и лайков.\n\n```javascript\nfunction removePost(postId, state) {\n return {\n posts: state.posts.filter(post => post.id !== postId),\n comments: state.comments.filter(comment => comment.postId !== postId),\n postLikes: state.postLikes.filter(like => like.postId !== postId),\n }\n}\n\n// Пример удаления поста с id = 1\nconst newState = removePost(1, state)\nconsole.log(newState)\n```\n\nИ нормализованные данные требуют чуть более сложного кода при простых запросах типа «все комментарии конкретного поста»:\n\n```javascript\nconst postId = 1\nconst commentsForPost = state.comments.filter(c => c.postId === postId)\n```\n\nНо в целом это небольшая цена за преимущества, которые вы получите: простоту поддержки, отсутствие дублирования и легкость масштабирования. Именно поэтому большинство современных фронтенд-фреймворков и библиотек управления состоянием рекомендуют использовать именно нормализованный подход к хранению данных.\n\nЧто по производительности? Является это проблемой или нет — вопрос открытый. Как правило, нет. Фронтенд очень редко оперирует большими количествами, например, десятками и сотнями тысяч. Чаще всего размеры коллекций ограничиваются сотней элементов. Но если производительность важна, то для этого используют разнообразные механизмы от кеширования до использования полноценных баз данных.\n\n## Какой подход выбрать?\n\nВо многом ответ зависит от задачи и механизма управления состоянием. Как правило, в этих библиотеках есть документация, которая раскрывает как оптимальнее хранить данные внутри.\n\nЕсли же данные хранятся в обычных объектах без использования библиотек, то можно пойти следующим путем:\n\n- Для простоты всегда делать все плоским. Привыкнув к этому подходу, потом проще работать с любыми данными.\n- Использовать вложенные структуры там где нет связи между данными и использовать плоские, там где нужно постоянно анализировать весь набор.\n\n## Итог\n\nБольшинство фронтенд-приложений лучше всего работают с нормализованными данными, поскольку такой подход упрощает поддержку и масштабирование. Используйте иерархию только тогда, когда ваши данные полностью независимы друг от друга, и вы уверены, что это не изменится со временем.\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/shape-of-state/theory_unit","version":"8f286f6358a90a7bef2263b3a6edf5a90a94fa42","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">Теория: Нормализация данных</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Нормализация данных","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>При разработке приложения часто возникают сложности с удобством доступа к вложенным данным. Например, сложно быстро извлечь последние комментарии к вопросам в Q&A (Вопросы и Ответы), если комментарии вложены внутрь самих вопросов. В таком случае необходимо обойти все вопросы, собрать комментарии и отсортировать их по времени.</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://ru.hexlet.io/qna" rel="noopener noreferrer" target="_blank">Q&A Хекслета</a>. У каждого вопроса есть количество лайков и комментарии, у которых в свою очередь тоже есть лайки. Эти данные можно представить так:</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 posts = [
{
id: 1,
text: 'Как писать код?',
likesCount: 2,
likedBy: ['user1', 'user3'],
comments: [
{
answer: 'Открой редактор!',
likesCount: 1,
createdAt: '11-12-2022',
},
{
answer: 'Сидя!',
likesCount: 3,
createdAt: '11-12-2022',
},
],
},
{
id: 2,
text: 'Что лучше: vim или emacs?',
likesCount: 2,
likedBy: ['user2', 'user3'],
comments: [
{
answer: 'FAR зе бест!',
likesCount: 100,
createdAt: '11-12-2022',
},
],
},
]
const state = { posts }</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>Однако, если данные связаны, то иерархическая структура усложняет сквозные выборки. Представьте себе, что надо выводить 10 последних комментариев. Как это сделать? Придется ходить по всем топикам, брать все комментарии, объединять и искать самые свежие.</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 comments = posts.flatMap(p => p.comments)
const sortedComments = comments.sort(
(c1, c2) => new Date(c2.createdAt) - new Date(c1.createdAt),
)
const lastComments = sortedComments.slice(0, 10)</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>
<h2 id="heading-2-1">Создание индексов</h2>
<p>Один из способов выйти из этой ситуации — начать дублировать данные. Создавать дополнительные структуры, оптимизированные под конкретные задачи. И хотя, в общем, это не лишено смысла, все же ручной способ поддерживать эти структуры ничего хорошего не принесет. В тех же базах данных за формирование индексов отвечает сама база данных. Нам как программистам не надо об этом заботиться. А здесь придется внедрять дополнительную синхронизацию во все этапы: добавление, изменение и удаление.</p>
<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">user1</code>), превращается в простую операцию извлечения данных из объекта по ключу.</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">// Отдельная структура под хранение id постов, которые лакйнул каждый пользователь
const userLikesIndex = {
user1: [1, 2],
user2: [1, 2],
user3: [1],
user4: [1, 2],
}
const postsLikedByUser1 = userLikesIndex['user1']
console.log(postsLikedByUser1)</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>
<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 removeLike(userId, postId) {
const post = state.posts.find(p => p.id === postId)
post.likes = post.likes.filter(uId => uId !== userId)
userLikesIndex[userId] = userLikesIndex[userId].filter(
pId => pId !== postId,
)
}
// Пример удаления лайка:
removeLike('user1', 'post2')</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>Именно поэтому клиентское приложение редко занимается такими вещами вручную. Подобные структуры (индексы) автоматически создаются и поддерживаются на бэкенде или с помощью специальных библиотек управления состоянием. А сейчас активно развиваются полноценные клиентские базы данных, которые умеют с этим работать.</p>
<h2 id="heading-2-2">Нормализация данных</h2>
<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">const posts = [
{
id: 1,
text: 'Как писать код?',
},
{
id: 2,
text: 'Что лучше: vim или emacs?',
},
]
// Для работы с нормализованными данными необходимо
// использовать идентификаторы для связи сущностей
const comments = [
{
id: 1,
postId: 1,
answer: 'Открой редактор!',
likesCount: 1,
createdAt: '11-12-2022',
},
{
id: 2,
postId: 1,
answer: 'Сидя!',
likesCount: 3,
createdAt: '11-12-2022',
},
{
id: 3,
postId: 2,
answer: 'FAR зе бест!',
likesCount: 100,
createdAt: '11-12-2022',
},
]
const postLikes = [
{ postId: 1, userId: 'user1' },
{ postId: 1, userId: 'user3' },
{ postId: 2, userId: 'user2' },
{ postId: 2, userId: 'user3' },
]</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>
<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 sortedComments = state.comments.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
)
const lastComments = sortedComments.slice(0, 10)</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>
<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 postsLikedByUser1 = state.postLikes.filter(
like => like.userId === 'user1',
)</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>
<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 removePost(postId, state) {
return {
posts: state.posts.filter(post => post.id !== postId),
comments: state.comments.filter(comment => comment.postId !== postId),
postLikes: state.postLikes.filter(like => like.postId !== postId),
}
}
// Пример удаления поста с id = 1
const newState = removePost(1, state)
console.log(newState)</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>
<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 postId = 1
const commentsForPost = state.comments.filter(c => c.postId === postId)</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>Что по производительности? Является это проблемой или нет — вопрос открытый. Как правило, нет. Фронтенд очень редко оперирует большими количествами, например, десятками и сотнями тысяч. Чаще всего размеры коллекций ограничиваются сотней элементов. Но если производительность важна, то для этого используют разнообразные механизмы от кеширования до использования полноценных баз данных.</p>
<h2 id="heading-2-3">Какой подход выбрать?</h2>
<p>Во многом ответ зависит от задачи и механизма управления состоянием. Как правило, в этих библиотеках есть документация, которая раскрывает как оптимальнее хранить данные внутри.</p>
<p>Если же данные хранятся в обычных объектах без использования библиотек, то можно пойти следующим путем:</p>
<ul>
<li>Для простоты всегда делать все плоским. Привыкнув к этому подходу, потом проще работать с любыми данными.</li>
<li>Использовать вложенные структуры там где нет связи между данными и использовать плоские, там где нужно постоянно анализировать весь набор.</li>
</ul>
<h2 id="heading-2-4">Итог</h2>
<p>Большинство фронтенд-приложений лучше всего работают с нормализованными данными, поскольку такой подход упрощает поддержку и масштабирование. Используйте иерархию только тогда, когда ваши данные полностью независимы друг от друга, и вы уверены, что это не изменится со временем.</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/shape-of-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/shape-of-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-Bukl1lYy.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-BrRXra1y.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>