<!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 16:49:31 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="OFMv5KMXTv6p-R_ZqMNZ0Yq3Kcv6YqHVQp4OUSJ0EmfXguTTUWnjnh-6O0GkzKmmSr4EYfJVX3f_fpQFcHP1CQ";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>Асинхронные запросы | React: Redux Toolkit</title>
<meta name="description" content="Асинхронные запросы / React: Redux Toolkit: Учимся работать с Ajax и createAsyncThunk внутри Redux Toolkit">
<link rel="canonical" href="https://ru.hexlet.io/courses/js-redux-toolkit/lessons/async-thunks/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Асинхронные запросы">
<meta property="og:title" content="React: Redux Toolkit">
<meta property="og:description" content="Асинхронные запросы / React: Redux Toolkit: Учимся работать с Ajax и createAsyncThunk внутри Redux Toolkit">
<meta property="og:url" content="https://ru.hexlet.io/courses/js-redux-toolkit/lessons/async-thunks/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="0AnCdC5KfDMXKFl4bNO6bPTWyFVAWfbycQVOMKUjc9w_2AlD3DTRU6FrfeBg3EobNN_l_0huCFDM5dRk9ySUsg" />
<script src="/vite/assets/inertia-INZxX8jp.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-nkZBEvfU.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-6pOtQ3OW.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/eyJfcmFpbHMiOnsiZGF0YSI6NDA0OSwicHVyIjoiYmxvYl9pZCJ9fQ==--a6531362dd1f3afb65f5b269e1a23113df7171b1/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Devices-amico.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/eyJfcmFpbHMiOnsiZGF0YSI6NDc3MywicHVyIjoiYmxvYl9pZCJ9fQ==--5c7e03f5c0b090edfd085c95a269f9996b8bb090/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Source%20code-amico.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-26T16:49:31.572Z","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":"JeFRzbrN4VzVri4dQ_Bcb6kyCn1ZRiA2455QupWOiS_KMJr6SLNMPGPtCoVP_6wYaTsn11Fx3pRefsrux4luQQ","topics":[{"id":60151,"title":"Как мне в `reducers` вызвать ф-цию созданную через `createAsyncThunk`?\nЧто-то я подзавис.\nРевью: https://ru.hexlet.io/code_reviews/482979","plain_title":"Как мне в reducers вызвать ф-цию созданную через createAsyncThunk? Что-то я подзавис. Ревью: https://ru.hexlet.io/code_reviews/482979 ","creator":{"public_name":"Andrei Gunderin","id":269996,"is_tutor":false},"comments":[{"creator":{"public_name":"Andrei Gunderin","id":269996,"is_tutor":false},"id":126986,"body":"**Ivan Gagarinov**, большое спасибо за ответ.\n\nПеренес всю необходимую работу с состоянием в экстраредьюсеры.\n\nНо теперь в экстраредьюсерах не работают вызовы ф-ций созданных через `createAsyncThunk`\n\nhttps://ru.hexlet.io/code_reviews/482979\n\nКакой-то ужас этот курс)))","topic_id":60151},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":126933,"body":"**Andrei Gunderin**, здравствуйте! Вам не нужно вызывать функцию в `reducers`. Всю необходимую работу с состоянием нужно выполнять в экстраредьюсерах","topic_id":60151},{"creator":{"public_name":"Andrei Gunderin","id":269996,"is_tutor":false},"id":127037,"body":"Спасибо. Решил.\n\nНо что в вашем решении, что в моем вылазит ошибка:\n\n```\nconsole.error\n Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. \n```","topic_id":60151},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":127039,"body":"**Andrei Gunderin**, ага, спасибо. Про этот варнинг знаю, но пока не дошёл до его исправления","topic_id":60151},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":127031,"body":"**Andrei Gunderin**, в слайсе, при создании таски, сейчас вызывается запрос на получение списка `fetchTasks()`, но делать этого не нужно, мы ведь сразу можем добавить созданный таск в стейт. Тоже самое с удалением","topic_id":60151}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":82789,"title":"Привет. мне кажется, теорию нужно раскрыть поподробнее и поразжеванней. особенно первую половину, до createAsyncThunk мне, например, мало что понятно.","plain_title":"Привет. мне кажется, теорию нужно раскрыть поподробнее и поразжеванней. особенно первую половину, до createAsyncThunk мне, например, мало что понятно. ","creator":{"public_name":"Валентина","id":273986,"is_tutor":false},"comments":[{"creator":{"public_name":"Валентина","id":273986,"is_tutor":false},"id":168519,"body":"**Ivan Gagarinov**, классно! мне еще кажется, редакс толкит понятней разбирать в сравнении с простым редаксом. Например, тема thunk мне стала более понятной, когда я разобралась какая функция что возвращает и как бы это было в простом редаксе. А так, курс очень классный и полезный, спасибо.","topic_id":82789},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":168607,"body":"**Валентина**, спасибо! ","topic_id":82789},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":168393,"body":"**Валентина**, привет! Да, мы планируем доработать этот курс. Пока не могу сказать точные сроки когда будет готово. Но работа уже идет.","topic_id":82789}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":61372,"title":"Здравствуйте!\nЗастрял и не могу добиться того, чтобы таски выводились после первоначальной загрузки.\nВ `Tasks.jsx` не могу заставить selector работать\n\nhttps://ru.hexlet.io/code_reviews/502357","plain_title":"Здравствуйте! Застрял и не могу добиться того, чтобы таски выводились после первоначальной загрузки. В Tasks.jsx не могу заставить selector работать https://ru.hexlet.io/code_reviews/502357 ","creator":{"public_name":"","id":385344,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129565,"body":"**Anton Nazarenko**, спасибо! Я ввел в заблуждение. Метод `addMany` может принимать и массив `entities`, и `action`. Прошу прощения за свою невнимательность! Значит проблема только в селекторах. Сейчас проверил - так и есть","topic_id":61372},{"creator":{"public_name":"Anton Nazarenko","id":382899,"is_tutor":false},"id":129555,"body":"А в теории принимают action.payload :\n\n> .addCase(fetchUserById.fulfilled, (state, action) => {\n // Добавляем пользователя\n usersAdapter.addOne(state, action.payload);\n state.loading = 'idle';\n state.error = null;\n })","topic_id":61372},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129353,"body":"**user-9c6eca2633f3d496**, здравствуйте! При первой загрузке сервер возвращает пустой список. Добавил эту информацию в подсказки","topic_id":61372},{"creator":{"public_name":"","id":385344,"is_tutor":false},"id":129365,"body":"**Ivan Gagarinov**, спасибо за ответ!\nУ меня, видимо, проблема в чем-то другом. Тесты тоже говорят, что не видят нужные элементы (еще до создания таска).","topic_id":61372},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129429,"body":"**user-9c6eca2633f3d496**, у вас неправильно используется методы адаптера. addOne, addMany и т.д. принимают полный `action`, а не `payload`. Кроме того, если вы хотите использовать адаптер, вы можете использовать селектор, который пердаставляет адаптер","topic_id":61372}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":60812,"title":"Не мешало бы в условии испытания уточнить, что не надо создавать стейт с помощью createEntityAdapter. По ходу курса все примеры постепенно перешли на использование адаптеров и в теме перед задачей все примеры приведены с ним. А в задании код этого не ожидает. Я полчаса тупил, пока не посмотрел на реализацию селектора и использование данных в файле Tasks. ","plain_title":"Не мешало бы в условии испытания уточнить, что не надо создавать стейт с помощью createEntityAdapter. По ходу курса все примеры постепенно перешли на использование адаптеров и в теме перед задачей все примеры приведены с ним. А в задании код этого не ожидает. Я полчаса тупил, пока не посмотрел на реализацию селектора и использование данных в файле Tasks. ","creator":{"public_name":"Alex Ivanou","id":229181,"is_tutor":false},"comments":[{"creator":{"public_name":"Артём","id":338241,"is_tutor":false},"id":128565,"body":"согласен стейт в текущем задании путает. \nможно дать больше свободы и пусть ученик сам выбирает как реализовывать стейт и выборку из него.","topic_id":60812},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":128342,"body":"**Alexandr Ivanov**, здравствуйте! Спасибо за замечание. Я изменю испытание","topic_id":60812},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129430,"body":"**Артём**, спасибо за фидбек. Вы правы. Я сделал сейчас таким образом, что вы можете сами выбирать использовать вам адаптеры или нет.","topic_id":60812}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":63644,"title":"Здравствуйте, подскажите пожалуйста, почему если у меня в Tasks.jsx раскомментировать dispatch(fetchTasks()), то в консоли в вебе происходит бесконечный вывод? И в целом, правильным ли путём иду к решению?\nhttps://ru.hexlet.io/code_reviews/536553","plain_title":"Здравствуйте, подскажите пожалуйста, почему у меня если в Tasks.jsx раскомментировать dispatch(fetchTasks()), то в консоли в вебе происходит какая-то рекурсия? И в целом, правильным ли путём иду к решению? https://ru.hexlet.io/code_reviews/536553 ","creator":{"public_name":"Mansur Khusnutdinov","id":317281,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":133915,"body":"**Mansur Khusnutdinov**, здравствуйте. Когда происходит запрос `fetchTasks()`, при завершении, он вызывает редюсер, который меняет состояние. Компонент `Tasks` отслеживает состояние(благодаря useSelector) и перерисовывается с новым состоянием. Соответственно снова вызывается диспатч и всё выполняется по кругу. Чтобы такого не происходило, используется хук `useEffect()`, которые позволяет навешать обработчик только на первый рендер компонента.","topic_id":63644},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":134385,"body":"**Mansur Khusnutdinov**, чтобы ваирант с FormData заработал, в инпут нужно добавить атрибут `name` и указать имя, по которому FormData найдёт поле. С другой стороны вам ничего для получения value делать не надо. У вас же уже сохраняется значение в `name` с помощью `useState()` - просто используйте его.","topic_id":63644},{"creator":{"public_name":"Mansur Khusnutdinov","id":317281,"is_tutor":false},"id":134376,"body":"Спасибо! Почти решил задачу, но ещё такой вопрос: как обратиться к полю, в которое вводится название таски, чтобы передать его в аргумент экшена? Пробовал уже разные варианты: e.target[0].value (в вебе работает, но тесты не проходят), использовал new FormData, но тоже не получается","topic_id":63644}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":65593,"title":"не смог стопроцентно понять код с `useEffect`\n\n```\nuseEffect(() => {\n dispatch(fetchTasks());\n }, [dispatch]);\n```\nвот мы засунули `dispatch` в массив как аргумент, но диспатч ведь это лишь функция, а не значение, которое должно отслеживаться, на сброс эффекта тоже не похоже. Что тут конкретно происходит?","plain_title":"не смог стопроцентно понять код с useEffect useEffect(() => { dispatch(fetchTasks()); }, [dispatch]); вот мы засунули dispatch в массив как аргумент, но диспатч ведь это лишь функция, а не значение, которое должно отслеживаться на сброс эффекта тоже не похоже что тут конкретно происходит? ","creator":{"public_name":"Имя Фамилия","id":235616,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":138116,"body":"**Lost Eko**, в JS все функции являются и значениями (выражениями), поэтому в этом смысле тут всё в порядке. На самом деле dispatch не меняется. Эта запись равнозначна пустому массиву `[]`. А `dispatch` добавлен просто чтобы не было ошибки линтера, так как сам линтер не знает что эта функция не меняет ссылку. Можно ещё просто отключить проверку этого правила, либо настроить линтер чтобы он был в курсе как работает dispatch. Все варианты решения проблемы будут корректными.","topic_id":65593}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":59273,"title":"Здравствуйте! Я немного не понял вот эту фразу:\n\n> Например, когда нам нужно работать с состоянием или глобальными объектами, такими как вебсокет соединение. В этом случае придется использовать redux-thunk, который все это позволяет легко сделать\n\nВ документации [redux](https://redux.js.org/usage/writing-logic-thunks#thunk-use-cases) сказано:\n\n> So, they should not generally be used for initializing persistent connections like websockets, and you can't use them to respond to other actions.\n\nВебсокет нужно в промис оборачивать как-то? \n\nИли там как-то по другому все делается, вебсокет соединение иницируется где-то отдельно, а потом через redux-thunk идет общение?\n\nИли тут более [продвинутые техники](https://redux-toolkit.js.org/rtk-query/overview) осваивать нужно, которые вы указали в дополнительных материалах?","plain_title":"Здравствуйте! Я немного не понял вот эту фразу: Например, когда нам нужно работать с состоянием или глобальными объектами, такими как вебсокет соединение. В этом случае придется использовать redux-thunk, который все это позволяет легко сделать В документации redux (https://redux.js.org/usage/writing-logic-thunks#thunk-use-cases) сказано: So, they should not generally be used for initializing persistent connections like websockets, and you can't use them to respond to other actions. Вебсокет нужно в промис оборачивать как-то? Или там как-то по другому все делается, вебсокет соединение иницируется где-то отдельно, а потом через redux-thunk идет общение? ","creator":{"public_name":"Максим Гурьянов","id":206528,"is_tutor":false},"comments":[{"creator":{"public_name":"Максим Гурьянов","id":206528,"is_tutor":false},"id":125321,"body":"Хорошо, спасибо!","topic_id":59273},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":125317,"body":"Максим, здравствуйте! Я сейчас готовлю практику по этому уроку, она поможет наглядно разрешить ваш вопрос. Обязательно сообщу вам когда она будет готова.","topic_id":59273}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":93603,"title":"Дисклеймер: описанное ниже может быть утрировано на эмоциях, но автор уверен, что основная суть отражает посыл, который он хотел донести.\n\nК концу курса есть ощущение, что завтра я уезжаю в мир без интернета и в меня напоследок постарались затолкать как можно больше, что бы я не говорил потом, что недодали. \n\nЯ уже не представляю, как работать без Redux Toolkit не потому что это настолько крутой и полезный инструмент, а потому что просто не помню. Настолько сильно ногами пришлось заталкивать информацию последней пары курсов, что бы понять хоть что-то.\n\nВ последнем уроке что-то говорится про dispatch() и useEffect(), но для меня это уже далекое прошлое, которого я не помню. Не потому что оно стало не нужно, а потому что сам я ни разу наверное не написал слово dispatch в практических заданиях там, где это было актуально. \n\nВ результате, в ближайшее время я приступлю к проекту, где меня вновь ждет отчаяние и ярость, но не потому что приходится осваивать что-то новое, как на предыдущих проектах, а потому что объем знаний необходимого старого сравним с навыками обработки деревянных изделий, которые я проходил в школе 16 лет назад.","plain_title":"Дисклеймер: описанное ниже может быть утрировано на эмоциях, но автор уверен, что основная суть отражает посыл, который он хотел донести. К концу курса есть ощущение, что завтра я уезжаю в мир без интернета и в меня напоследок постарались затолкать как можно больше, что бы я не говорил потом, что недодали. Я уже не представляю, как работать без Redux Toolkit не потому что это настолько крутой и полезный инструмент, а потому что просто не помню. Настолько сильно ногами пришлось заталкивать информацию последней пары курсов, что бы понять хоть что-то. В последнем уроке что-то говорится про dispatch() и useEffect(), но для меня это уже далекое прошлое, которого я не помню. Не потому что оно стало не нужно, а потому что сам я ни разу наверное не написал слово dispatch в практических заданиях там, где это было актуально. В результате, в ближайшее время я приступлю к проекту, где меня вновь ждет отчаяние и ярость, но не потому что приходится осваивать что-то новое, как на предыдущих проектах, а потому что объем знаний необходимого старого сравним с навыками обработки деревянных изделий, которые я проходил в школе 16 лет назад. ","creator":{"public_name":"Aleksandr Manujlo","id":536195,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":183708,"body":"**Aleksandr Manujlo**, здравствуйте. Вся работа с тулкитом по сути сводится к написанию однотипных слайсов. Обычная практика просто копипастить слайсы и в них указывать нужную логику в редьюсерах и начальный стейт. Подключение тулкита, настройка, вызов экшенов — все это максимально однотипно и мало отличается в каждом проекте. Все сразу запоминать не обязательно, вы всегда можете подсмотреть как правильно настраивать тулкит.","topic_id":93603},{"creator":{"public_name":"Aleksandr Manujlo","id":536195,"is_tutor":false},"id":183811,"body":"Доброго дня. Проблема явно не в настройке тулкита и создании редьюсеров. Это вы правильно написали, что всегда есть возможность подсмотреть как настраивается в чем-то готовом. Проблема в подходе к последним курсам, связанным с реактом. До последнего модуля в программе Фронтенд-разработчик есть четкое ощущение, что ты постепенно узнаешь всю картину по кусочкам. К 3 проекту ты четко уверен, что можешь написать приложение и что важнее, ты можешь представить как оно работает, на каких этапах и куда передаются данные. Здесь же есть ощущение, что в руках 3-5 деталей из 10-детального пазла, а пазл вот УЖЕ надо собирать. Чем дальше в реакт, тем больше вопросов что вообще происходит и тем меньше ответов. Ты уже не видишь, как мигрируют по архитектуре данные, которые ввел пользователь, ты молишься, что бы оно само взлетело, потому что сам ты точно не разберешься. Даже в этом уроке первый час-два уходят на постоянное перечитывание теории что бы понять, на каком этапе что должно происходить, а время, заложенное на курс, кстати, указано всего 12 часов. И это не практика сложная, это теория накидана пошустряку. К примеру, в теории дается пример запроса на получение юзера из БД, но вообще не уделено внимание тому, что в случае успешного запроса мы добавляем его в стейт. Казалось бы, логичная штука и там даже коммент есть в коде, но нет, это неочевидно. И так в каждом уроке, в формате \"Используется где-то там, например в слайсах\" или \"Импортируем где нужно\". Ты не понимаешь, что от чего зависит, когда переписываешь вот так куски кода из теории, как я сейчас делаю в этом упражнении. \nБез негатива, просто грустно. Денег заплатил, с этим точно надо будет дальше работать как-то, но рассчитывать придется на добрых людей на будущей работе","topic_id":93603},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":183847,"body":"**Aleksandr Manujlo**, здравствуйте! Спасибо за фидбек! Я его сохранил. Обсудим с автором доработку этих курсов.","topic_id":93603}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":65034,"title":"Было бы здорово добавить в урок информации, что вот эта строка\n`'users/fetchUserById', // отображается в dev tools`\nв createAsyncThunk нужна не только для отображения в dev tools, а она еще должна быть уникальна у каждого Thunk иначе выдаёт ошибку - `Uncaught Error: addCase cannot be called with two reducers for the same action type`, что как я думал из-за одинаковых типов экшенов в extraReducers, т.е. типа fulfilled у разных Thunk .","plain_title":"Было бы здорово добавить в урок информации, что вот эта строка 'users/fetchUserById', // отображается в dev tools в createAsyncThunk нужна не только для отображения в dev tools, а она еще должна быть уникальна у каждого Thunk иначе выдаёт ошибку - Uncaught Error: addCase cannot be called with two reducers for the same action type, что как я думал из-за одинаковых типов экшенов в extraReducers, т.е. типа fulfilled у разных Thunk . ","creator":{"public_name":"Михаил Колотович","id":178518,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":136755,"body":"Спасибо! Добавил.","topic_id":65034}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}},{"id":61477,"title":"Добрый день! подскажите, что я не так делаю при загрузке списка задач и добавления задачи, не могу понять почему адаптер не меняет стейт.... ни в первом ни во втором случае... \n\nhttps://ru.hexlet.io/code_reviews/503809 \n\nЗа ранее благодарен! ","plain_title":"Добрый день! подскажите, что я не так делаю при загрузке списка задач и добавления задачи, не могу понять почему адаптер не меняет стейт.... ни в первом ни во втором случае... https://ru.hexlet.io/code_reviews/503809 За ранее благодарен! ","creator":{"public_name":"Anton Nazarenko","id":382899,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129665,"body":"На всякий случай я добавил в описание пример данных, которые приходят с сервера","topic_id":61477},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":129660,"body":"**Anton Nazarenko**, здравствуйте! В вашем решении в `extraReducers` передаётся объект, а должна передаваться функция. Сверьтесь с тем, как экстраредьюсеры добавляются в теории. Функция принимает билдер и к нему добавляются `addCase`. То есть вам нужно сразу добавлять к нему обработку всех экшенов, даже если они относятся к другим запросам. Также обратите внимание, что список приходит во вложенном свойстве `items`","topic_id":61477}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Асинхронные запросы","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":1541,"slug":"js_redux_toolkit_async_thunks_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"Допишите добавление и удаление задач, используя *redux-toolkit*.\n\n### src/slices/tasksSlice.js\n\nРеализуйте слайс для работы с задачами. Добавьте методы работы с задачами — добавление новой задачи и удаление задачи.\n\nИспользуйте материал из теории для создания асинхронных запросов. В слайсе уже есть метод получения спис��а задач, используйте его как пример.\n\nВам предстоит создать три запроса:\n\n* GET-запрос `'/api/tasks'`, который возвращает список задач\n* POST-запрос `'/api/tasks'`, который принимает данные в виде `{ name }`, где `name` — это название задачи. Это запрос создает задачу и возвращает `{ name, id }`\n* DELETE-запрос `'/api/tasks/:id'`, который удаляет задачу с таким `id`\n\nПример данных, которые приходят с сервера:\n\n```json\n{\n \"items\": [\n {\n \"name\": \"test1\",\n \"id\": \"1\"\n },\n {\n \"name\": \"test2\",\n \"id\": \"2\"\n }\n ]\n}\n```\n\n### src/components/NewTaskForm.jsx\n\nДобавьте импорт необходимых действий и реализуйте обработку добавления новой задачи.\n\n### src/components/Tasks.jsx\n\nДобавьте импорт необходимых действий и получение списка задач.\n\nРеализуйте обработку удаления задачи. Для этого создайте функцию `handleRemoveTask()`, которая будет принимать идентификатор задачи.\n\n### Подсказки\n\n* Используйте роуты из файла *src/routes.js* для формирования запросов\n* При первой загрузке задач сервер возвращает пустой список\n* При создании задачи сервер сгенерирует `id` и вернет объект с присвоенным идентификатором\n* Тесты могут вывести предупреждение _Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one_. Не обращайте на него внимания\n","prepared_readme":"Допишите добавление и удаление задач, используя *redux-toolkit*.\n\n### src/slices/tasksSlice.js\n\nРеализуйте слайс для работы с задачами. Добавьте методы работы с задачами — добавление новой задачи и удаление задачи.\n\nИспользуйте материал из теории для создания асинхронных запросов. В слайсе уже есть метод получения списка задач, используйте его как пример.\n\nВам предстоит создать три запроса:\n\n* GET-запрос `'/api/tasks'`, который возвращает список задач\n* POST-запрос `'/api/tasks'`, который принимает данные в виде `{ name }`, где `name` — это название задачи. Это запрос создает задачу и возвращает `{ name, id }`\n* DELETE-запрос `'/api/tasks/:id'`, который удаляет задачу с таким `id`\n\nПример данных, которые приходят с сервера:\n\n```json\n{\n \"items\": [\n {\n \"name\": \"test1\",\n \"id\": \"1\"\n },\n {\n \"name\": \"test2\",\n \"id\": \"2\"\n }\n ]\n}\n```\n\n### src/components/NewTaskForm.jsx\n\nДобавьте импорт необходимых действий и реализуйте обработку добавления новой задачи.\n\n### src/components/Tasks.jsx\n\nДобавьте импорт необходимых действий и получение списка задач.\n\nРеализуйте обработку удаления задачи. Для этого создайте функцию `handleRemoveTask()`, которая будет принимать идентификатор задачи.\n\n### Подсказки\n\n* Используйте роуты из файла *src/routes.js* для формирования запросов\n* При первой загрузке задач сервер возвращает пустой список\n* При создании задачи сервер сгенерирует `id` и вернет объект с присвоенным идентификатором\n* Тесты могут вывести предупреждение _Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one_. Не обращайте на него внимания\n","has_solution":true,"entity_name":"Асинхронные запросы"},"units":[{"id":4940,"name":"theory","url":"/courses/js-redux-toolkit/lessons/async-thunks/theory_unit"},{"id":10551,"name":"quiz","url":"/courses/js-redux-toolkit/lessons/async-thunks/quiz_unit"},{"id":5017,"name":"exercise","url":"/courses/js-redux-toolkit/lessons/async-thunks/exercise_unit"}],"links":[{"id":422466,"name":"Как работает Redux Thunk","url":"https://redux.js.org/usage/writing-logic-thunks\n"},{"id":422467,"name":"Документация createAsyncThunk","url":"https://redux.js.org/usage/writing-logic-thunks#using-createasyncthunk\n"},{"id":422468,"name":"Автоматное программирование","url":"https://guides.hexlet.io/ru/usefull-twitter-threads/#автоматное-программирование\n"}],"ordered_units":[{"id":4940,"name":"theory","url":"/courses/js-redux-toolkit/lessons/async-thunks/theory_unit"},{"id":10551,"name":"quiz","url":"/courses/js-redux-toolkit/lessons/async-thunks/quiz_unit"},{"id":5017,"name":"exercise","url":"/courses/js-redux-toolkit/lessons/async-thunks/exercise_unit"}],"id":2218,"slug":"async-thunks","state":"approved","name":"Асинхронные запросы","course_order":600,"goal":"Учимся работать с Ajax и createAsyncThunk внутри Redux Toolkit","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"## Проблематика работы с асинхронными запросами\n\nОдна из самых сложных задач в построении фронтенд-приложений – работа с внешними запросами. Трудности приходят с двух сторон.\n\nС одной стороны, асинхронность сама по себе порождает неоднозначности, перестают работать стандартные механизмы. Redux не умеет работать в асинхронном режиме, поэтому вся обработка запросов происходит снаружи. В таком случае любая нетривиальная логика обработки асинхронных действий будет появляться внутри компонентов React:\n\n```javascript\nconst MyComponent = (props) => {\n const onClick = async (todoId) => {\n const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`)\n const todos // Извлекаем и преобразуем данные из response\n = dispatch(todosLoaded(todos))\n }\n\n // Рендер\n}\n```\n\nС другой стороны, сеть — это вещь ненадежная. Запросы могут выполняться долго или не выполниться вообще, и все это нужно отслеживать для правильной реакции:\n\n* При долгих запросах — показывать спиннер\n* При обрыве запроса — выводить соответствующее предупреждение\n\nДобавим к примеру выше обработку ошибок и отслеживание статуса загрузки:\n\n```javascript\nconst onClick = async (todoId) => {\n // Здесь мы работаем с конечным автоматом по обработке любого http-запроса\n try {\n // Начинаем процесс загрузки\n dispatch(todosLoadingStarted())\n const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`)\n const todos // Извлекаем и преобразуем данные из response\n = dispatch(todosLoaded(todos))\n }\n catch (e) {\n // Все еще сложнее — нужно отслеживать, что конкретно пошло не так\n dispatch(todosLoadingFailed(e.message))\n }\n}\n```\n\nКак мы видим, даже для небольшого числа вызовов придется написать очень много похожего кода. В реальных приложениях количество вызовов может измеряться многими десятками и даже сотнями. Поэтому без готового решения тут не обойтись. Для автоматизации HTTP-запросов к нам на помощь приходят два механизма:\n\n* Мидлвара _redux-thunk_, которая уже включена в Redux Toolkit\n* Механизм `createAsyncThunk()`\n\n## Мидлвара redux-thunk\n\nМидлвара — это код, который встраивается в обработку. Можно представить ее в виде цепочки функций, где каждая функция принимает данные из предыдущего обработчика и передает данные в следующую функцию. Каждая такая функция и будет мидлварой. Самый простой пример — это вывод логов. Такая функция будет выводить в лог данные и передавать дальше, никак их не меняя:\n\n```javascript\nconst loggerMiddleware = store => next => (action) => {\n // Вывод информации о действии в консоль\n console.log('Dispatching action:', action)\n\n // Передача действия дальше по цепочке middleware\n return next(action)\n}\n```\n\nМидлвара _redux-thunk_ добавляется в Redux и позволяет использовать асинхронный код внутри `dispatch()`. С ее помощью выносят логику выполнения запросов и обновления хранилища в отдельные функции (_thunks_). Вот пример такой функции:\n\n```javascript\n// Обратите внимание на вложенную функцию, принимающую dispatch\n// Эта функция будет храниться вне компонента — например, в слайсе\nexport const fetchTodoById = todoId => async (dispatch) => {\n const response = await axios.get(`https://ru.hexlet.io/todos/${todoId}`)\n // Здесь нужно выполнить необходимую нормализацию и обработать ошибки\n dispatch(todosLoaded(response.todos))\n}\n\n// Использование\nconst TodoComponent = ({ todoId }) => {\n const dispatch = useDispatch()\n\n const onFetchClicked = () => {\n // Передали асинхронную функцию\n dispatch(fetchTodoById(todoId))\n }\n\n // Где-то здесь используем onFetchClicked\n}\n```\n\nВообще thunk необязательно должен быть асинхронным. Thunk — это всего лишь функция, которая возвращает другую функцию и принимает dispatch и getState (при необходимости) в качестве параметров. Thunk может выполнять синхронные действия или комбинации синхронных и асинхронных операций, но в этом уроке нам это не так важно.\n\nКод из примера выше можно реализовать и без _redux-thunk_, просто написав асинхронную функцию. Ей на вход мы передадим `dispatch`:\n\n```javascript\nexport const fetchTodoById = async (todoId, dispatch) => {\n try {\n const response = await axios.get(`https://ru.hexlet.io/todos/${todoId}`)\n // Выполнить необходимую нормализацию и обработать ошибки\n dispatch(todosLoaded(response.data.todos))\n }\n catch (error) {\n // Обработка ошибок\n dispatch(todosLoadingFailed(error.message))\n }\n}\n```\n\nРазница проявляется в более продвинутых вариантах использования — например, когда мы работаем с состоянием или глобальными объектами. В этом случае не обойтись без _redux-thunk_:\n\n```javascript\n// (dispatch, getState, extraArgument)\nexport const fetchTodoById = todoId => async (dispatch, getState, extraArgument) => {\n // Любые данные, переданные на этапе конфигурации мидлвары\n const { serviceApi } = extraArgument\n const response = await serviceApi.getTodo(todoId)\n dispatch(todosLoaded(response.todos))\n}\n```\n\nОсновное отличие здесь — это возможность передачи дополнительных параметров в thunk-функцию через extraArgument, а также быстро получить текущее состояние хранилища с помощью функции getState.\n\n\n## Механизм createAsyncThunk()\n\nНесмотря на удобства _redux-thunk_, сами по себе thunks не уменьшают количество кода. Та же обработка ошибок всё еще составляет большую его часть. Здесь на помощь приходит инструмент `createAsyncThunk()`, появившийся вместе с redux-toolkit:\n\n```javascript\nimport { createAsyncThunk, createSlice, createEntityAdapter } from '@reduxjs/toolkit'\n// Чтобы не хардкодить URL-адреса, делаем модуль, в котором они создаются\nimport { getUserUrl } from './routes.js'\n\n// Создаем thunk\nexport const fetchUserById = createAsyncThunk(\n 'users/fetchUserById', // Id отображается в dev tools и должен быть уникальный у каждого thunk\n async (userId) => {\n // Здесь только логика запроса и возврата данных\n // Никакой обработки ошибок\n const response = await axios.get(getUserUrl(userId))\n return response.data\n },\n)\n\nconst usersAdapter = createEntityAdapter()\n\nconst usersSlice = createSlice({\n name: 'users',\n // Добавляем в состояние отслеживание процесса загрузки\n // { ids: [], entities: {}, loadingStatus: 'idle', error: null }\n initialState: usersAdapter.getInitialState({ loadingStatus: 'idle', error: null }),\n reducers: {\n // Любые редьюсеры, которые нам нужны\n },\n extraReducers: (builder) => {\n builder\n // Вызывается прямо перед выполнением запроса\n .addCase(fetchUserById.pending, (state) => {\n state.loadingStatus = 'loading'\n state.error = null\n })\n // Вызывается, если запрос успешно выполнился\n .addCase(fetchUserById.fulfilled, (state, action) => {\n // Добавляем пользователя\n usersAdapter.addOne(state, action)\n state.loadingStatus = 'idle'\n state.error = null\n })\n // Вызывается в случае ошибки\n .addCase(fetchUserById.rejected, (state, action) => {\n state.loadingStatus = 'failed'\n // https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors\n state.error = action.error\n })\n },\n})\n\n// Где-то в приложении\nimport { fetchUserById } from './slices/usersSlice.js'\n\n// Внутри компонента\ndispatch(fetchUserById(123))\n```\n\nКаждый thunk, созданный через `createAsyncThunk()`, содержит внутри себя три события:\n\n* *pending*\n* *fulfilled*\n* *rejected*\n\nОни соответствуют состояниям промиса и вызываются в Redux Toolkit в тот момент, когда промис переходит в одно из этих состояний. Нам не обязательно реагировать на все. Мы сами выбираем, что нам важно в приложении.\n\n## Применение thunk\n\nThunk выходит за рамки обработки асинхронных запросов. Этот механизм можно использовать в различных сценариях, где требуется вынос сложной логики или побочных эффектов из компонентов. Thunk также оказывается полезным для написания логики, зависящей от состояния в Redux. Ещё одним важным аспектом использования Thunk является возможность отправки нескольких действий в определенный момент или в течение заданного времени.\n\nПри этом Thunk дополняет, а не заменяет `useEffect`, который по-прежнему может использоваться для работы с побочными эффектами в компонентах. Если необходимо выделить какую-то логику из компонента, чтобы сделать его более универсальным для повторного использования, эту логику можно перенести в Thunk вместо использования `useEffect`.\n\nНа практике `useEffect` и `createAsyncThunk` часто используются вместе. Например, вызов `dispatch(fetchData())` внутри `useEffect` с пустым массивом зависимостей позволяет подгрузить данные при инициализации компонента, обеспечивая эффективное взаимодействие между асинхронными операциями и жизненным циклом компонента.\n\n## Что дальше\n\nВ современной разработке вместе с React часто используется TypeScript. Вы можете познакомиться с ним в курсе [Основы Typescript](https://ru.hexlet.io/courses/typescript-basics).\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":4934,"name":"theory","url":"/courses/js-redux-toolkit/lessons/intro/theory_unit"}],"links":[],"ordered_units":[{"id":4934,"name":"theory","url":"/courses/js-redux-toolkit/lessons/intro/theory_unit"}],"id":2212,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с курсом и готовим окружение","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"\nReact — это мощный инструмент, но все равно в нем появляются некоторые неудобства при работе с большими приложениями.\n\nОдно из самых раздражающих — подъем состояния наверх через колбеки, которые нужно прокидывать вниз с самого верхнего уровня. Прокидывать приходится не только колбеки, но и любые данные. Получается, что множество промежуточных компонентов выступают в качестве прокси — они пропускают сквозь себя данные, которыми не пользуются.\n\nВторой раздражающий фактор — рендеринг и логика смешаны в одном месте, что быстро раздувает компоненты и усложняет понимание. Сюда же добавляются неконтролируемые побочные эффекты вперемешку с обновлением данных.\n\nДля решения этих проблем появились **менеджеры состояния**, в том числе Redux — официальный менеджер, который поддерживает сама компания Facebook.\n\n## Как работают Redux и Toolkit\n\n**Redux** — это очень простая библиотека, предназначенная исключительно для управления состоянием. Ее разрабатывали под использование в React, но на самом деле от React она не зависит — можно использовать ее с чем угодно.\n\nДля связи Redux с React понадобится **Redux Toolkit**, через который мы произведем всю необходимую интеграцию. Redux Toolkit не просто склеивает Redux и React. Он меняет способ работы с Redux до неузнаваемости, привносит множество полезных абстракций — например, широко используемые мидлвары.\n\nЦель этой библиотеки — сделать хранение, извлечение и использование данных максимально удобными, убрав шаблонный код, характерный для приложений на Redux.\n\nRedux Toolkit вводит много новых понятий, которые тяжело рассматривать в отрыве от общей картины. Поэтому сначала мы без погружения рассмотрим пример, в котором все собрано в одном месте, а уже в следующих уроках разберем по косточкам каждый элемент.\n\n## Зачем нужны классы, функции, хуки и Redux Toolkit\n\nЗачем так усложнять эту тему? Сначала мы изучаем React на классах, потом на функциональных компонентах и хуках, затем в связке с Redux. В итоге мы заменяем Redux на Redux Toolkit. К счастью, это финальная точка в этом путешествии.\n\nReact развивался именно так: сначала одно, потом другое, потом третье, а теперь пришел к Toolkit. Все это нужно знать, потому что под капотом у Toolkit все еще работает Redux. Да и хуки не заменяют классы — они останутся навсегда.\n"},"id":248,"slug":"js-redux-toolkit","challenges_count":5,"name":"React: Redux Toolkit","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите Redux Toolkit. Вы узнаете больше об организации и управлении сложным состоянием в React. В итоге вы научитесь проектировать React-приложения, которые будут просты в поддержке.","kind":"advanced","updated_at":"2026-01-20T11:37:21.979Z","language":"javascript","duration_cache":48120,"skills":["Строить фронтенд-приложения, которые легко поддерживать","Организовывать данные и доступ к ним внутри хранилища","Автоматизировать работу с HTTP-запросами"],"keywords":[],"lessons_count":11,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTY3OCwicHVyIjoiYmxvYl9pZCJ9fQ==--efe2b18fa5374934980a581ecd8ecf7b3f1d03ed/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":23,"slug":"js-react-development","title":"React","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":350,"duration_in_months":2},"id":34,"slug":"js-react-developer","title":"React","subtitle":"Навык разрабатывать быстрые и удобные интерфейсы, открывающий доступ к интересным вакансиям в крупных компаниях","subtitle_for_lists":"Освоите React и создание быстрых интерфейсов","locale":"ru","current":true,"duration_in_months_text":"2 месяца","stack_slug":"js-react-development","price_text":"от 3 900 ₽","duration_text":"2 месяца","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDA0OSwicHVyIjoiYmxvYl9pZCJ9fQ==--a6531362dd1f3afb65f5b269e1a23113df7171b1/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Devices-amico.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":421,"slug":"js-redux-toolkit","title":"Redux Toolkit","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"not_finished","order":null,"duration_in_months":1},"id":584,"slug":"js-redux-toolkit","title":"Redux Toolkit","subtitle":"Изучите Redux Toolkit, слайсы, нормализацию данных, асинхронные запросы и RTK Query","subtitle_for_lists":"Изучите Redux Toolkit, слайсы, нормализацию данных, асинхронные запросы и RTK Query","locale":"ru","current":true,"duration_in_months_text":"1 месяц","stack_slug":"js-redux-toolkit","price_text":"от 3 900 ₽","duration_text":"1 месяц","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDc3MywicHVyIjoiYmxvYl9pZCJ9fQ==--5c7e03f5c0b090edfd085c95a269f9996b8bb090/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Source%20code-amico.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/js-redux-toolkit/lessons/async-thunks/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","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">React: Redux Toolkit</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":"React: Redux Toolkit"},"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"><h2 id="heading-2-1">Проблематика работы с асинхронными запросами</h2>
<p>Одна из самых сложных задач в построении фронтенд-приложений – работа с внешними запросами. Трудности приходят с двух сторон.</p>
<p>С одной стороны, асинхронность сама по себе порождает неоднозначности, перестают работать стандартные механизмы. Redux не умеет работать в асинхронном режиме, поэтому вся обработка запросов происходит снаружи. В таком случае любая нетривиальная логика обработки асинхронных действий будет появляться внутри компонентов React:</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 MyComponent = (props) => {
const onClick = async (todoId) => {
const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`)
const todos // Извлекаем и преобразуем данные из response
= dispatch(todosLoaded(todos))
}
// Рендер
}</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>
<ul>
<li>При долгих запросах — показывать спиннер</li>
<li>При обрыве запроса — выводить соответствующее предупреждение</li>
</ul>
<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 onClick = async (todoId) => {
// Здесь мы работаем с конечным автоматом по обработке любого http-запроса
try {
// Начинаем процесс загрузки
dispatch(todosLoadingStarted())
const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`)
const todos // Извлекаем и преобразуем данные из response
= dispatch(todosLoaded(todos))
}
catch (e) {
// Все еще сложнее — нужно отслеживать, что конкретно пошло не так
dispatch(todosLoadingFailed(e.message))
}
}</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>Как мы видим, даже для небольшого числа вызовов придется написать очень много похожего кода. В реальных приложениях количество вызовов может измеряться многими десятками и даже сотнями. Поэтому без готового решения тут не обойтись. Для автоматизации HTTP-запросов к нам на помощь приходят два механизма:</p>
<ul>
<li>Мидлвара <em>redux-thunk</em>, которая уже включена в Redux Toolkit</li>
<li>Механизм <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">createAsyncThunk()</code></li>
</ul>
<h2 id="heading-2-2">Мидлвара redux-thunk</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 loggerMiddleware = store => next => (action) => {
// Вывод информации о действии в консоль
console.log('Dispatching action:', action)
// Передача действия дальше по цепочке middleware
return next(action)
}</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>Мидлвара <em>redux-thunk</em> добавляется в Redux и позволяет использовать асинхронный код внутри <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">dispatch()</code>. С ее помощью выносят логику выполнения запросов и обновления хранилища в отдельные функции (<em>thunks</em>). Вот пример такой функции:</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">// Обратите внимание на вложенную функцию, принимающую dispatch
// Эта функция будет храниться вне компонента — например, в слайсе
export const fetchTodoById = todoId => async (dispatch) => {
const response = await axios.get(`https://ru.hexlet.io/todos/${todoId}`)
// Здесь нужно выполнить необходимую нормализацию и обработать ошибки
dispatch(todosLoaded(response.todos))
}
// Использование
const TodoComponent = ({ todoId }) => {
const dispatch = useDispatch()
const onFetchClicked = () => {
// Передали асинхронную функцию
dispatch(fetchTodoById(todoId))
}
// Где-то здесь используем onFetchClicked
}</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>Вообще thunk необязательно должен быть асинхронным. Thunk — это всего лишь функция, которая возвращает другую функцию и принимает dispatch и getState (при необходимости) в качестве параметров. Thunk может выполнять синхронные действия или комбинации синхронных и асинхронных операций, но в этом уроке нам это не так важно.</p>
<p>Код из примера выше можно реализовать и без <em>redux-thunk</em>, просто написав асинхронную функцию. Ей на вход мы передадим <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">dispatch</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">export const fetchTodoById = async (todoId, dispatch) => {
try {
const response = await axios.get(`https://ru.hexlet.io/todos/${todoId}`)
// Выполнить необходимую нормализацию и обработать ошибки
dispatch(todosLoaded(response.data.todos))
}
catch (error) {
// Обработка ошибок
dispatch(todosLoadingFailed(error.message))
}
}</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>Разница проявляется в более продвинутых вариантах использования — например, когда мы работаем с состоянием или глобальными объектами. В этом случае не обойтись без <em>redux-thunk</em>:</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">// (dispatch, getState, extraArgument)
export const fetchTodoById = todoId => async (dispatch, getState, extraArgument) => {
// Любые данные, переданные на этапе конфигурации мидлвары
const { serviceApi } = extraArgument
const response = await serviceApi.getTodo(todoId)
dispatch(todosLoaded(response.todos))
}</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>Основное отличие здесь — это возможность передачи дополнительных параметров в thunk-функцию через extraArgument, а также быстро получить текущее состояние хранилища с помощью функции getState.</p>
<h2 id="heading-2-3">Механизм createAsyncThunk()</h2>
<p>Несмотря на удобства <em>redux-thunk</em>, сами по себе thunks не уменьшают количество кода. Та же обработка ошибок всё еще составляет большую его часть. Здесь на помощь приходит инструмент <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">createAsyncThunk()</code>, появившийся вместе с redux-toolkit:</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">import { createAsyncThunk, createSlice, createEntityAdapter } from '@reduxjs/toolkit'
// Чтобы не хардкодить URL-адреса, делаем модуль, в котором они создаются
import { getUserUrl } from './routes.js'
// Создаем thunk
export const fetchUserById = createAsyncThunk(
'users/fetchUserById', // Id отображается в dev tools и должен быть уникальный у каждого thunk
async (userId) => {
// Здесь только логика запроса и возврата данных
// Никакой обработки ошибок
const response = await axios.get(getUserUrl(userId))
return response.data
},
)
const usersAdapter = createEntityAdapter()
const usersSlice = createSlice({
name: 'users',
// Добавляем в состояние отслеживание процесса загрузки
// { ids: [], entities: {}, loadingStatus: 'idle', error: null }
initialState: usersAdapter.getInitialState({ loadingStatus: 'idle', error: null }),
reducers: {
// Любые редьюсеры, которые нам нужны
},
extraReducers: (builder) => {
builder
// Вызывается прямо перед выполнением запроса
.addCase(fetchUserById.pending, (state) => {
state.loadingStatus = 'loading'
state.error = null
})
// Вызывается, если запрос успешно выполнился
.addCase(fetchUserById.fulfilled, (state, action) => {
// Добавляем пользователя
usersAdapter.addOne(state, action)
state.loadingStatus = 'idle'
state.error = null
})
// Вызывается в случае ошибки
.addCase(fetchUserById.rejected, (state, action) => {
state.loadingStatus = 'failed'
// https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors
state.error = action.error
})
},
})
// Где-то в приложении
import { fetchUserById } from './slices/usersSlice.js'
// Внутри компонента
dispatch(fetchUserById(123))</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>Каждый thunk, созданный через <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">createAsyncThunk()</code>, содержит внутри себя три события:</p>
<ul>
<li><em>pending</em></li>
<li><em>fulfilled</em></li>
<li><em>rejected</em></li>
</ul>
<p>Они соответствуют состояниям промиса и вызываются в Redux Toolkit в тот момент, когда промис переходит в одно из этих состояний. Нам не обязательно реагировать на все. Мы сами выбираем, что нам важно в приложении.</p>
<h2 id="heading-2-4">Применение thunk</h2>
<p>Thunk выходит за рамки обработки асинхронных запросов. Этот механизм можно использовать в различных сценариях, где требуется вынос сложной логики или побочных эффектов из компонентов. Thunk также оказывается полезным для написания логики, зависящей от состояния в Redux. Ещё одним важным аспектом использования Thunk является возможность отправки нескольких действий в определенный момент или в течение заданного времени.</p>
<p>При этом Thunk дополняет, а не заменяет <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">useEffect</code>, который по-прежнему может использоваться для работы с побочными эффектами в компонентах. Если необходимо выделить какую-то логику из компонента, чтобы сделать его более универсальным для повторного использования, эту логику можно перенести в Thunk вместо использования <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">useEffect</code>.</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">useEffect</code> и <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">createAsyncThunk</code> часто используются вместе. Например, вызов <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">dispatch(fetchData())</code> внутри <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">useEffect</code> с пустым массивом зависимостей позволяет подгрузить данные при инициализации компонента, обеспечивая эффективное взаимодействие между асинхронными операциями и жизненным циклом компонента.</p>
<h2 id="heading-2-5">Что дальше</h2>
<p>В современной разработке вместе с React часто используется TypeScript. Вы можете познакомиться с ним в курсе <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/courses/typescript-basics" rel="noopener noreferrer" target="_blank">Основы Typescript</a>.</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/js-react-developer?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">2 месяца</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">React</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Освоите 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/eyJfcmFpbHMiOnsiZGF0YSI6NDA0OSwicHVyIjoiYmxvYl9pZCJ9fQ==--a6531362dd1f3afb65f5b269e1a23113df7171b1/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Devices-amico.png" alt="React" 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="/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/js-redux-toolkit?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">Redux Toolkit</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите Redux Toolkit, слайсы, нормализацию данных, асинхронные запросы и RTK Query</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/eyJfcmFpbHMiOnsiZGF0YSI6NDc3MywicHVyIjoiYmxvYl9pZCJ9fQ==--5c7e03f5c0b090edfd085c95a269f9996b8bb090/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Source%20code-amico.png" alt="Redux Toolkit" 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-redux-toolkit/lessons/async-thunks/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-redux-toolkit/lessons/async-thunks/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-CdBlNCiQ.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-nkZBEvfU.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>