Фронтенд кухня🥘
934 subscribers
10 photos
1 video
18 links
Вкусно готовим фронтенд, добавляем библиотеки по вкусу, делимся рецептами дебага

Написать в личку: https://t.iss.one/devmargooo

Менторство по фронтенду: https://devmargooo.ru/mentoring

Канал про жизнь и айти: https://t.iss.one/radical_idea
Download Telegram
Конкурентный режим в React

Не далее как вчера меня угораздило пожаловаться в одной из соцсетей на диффузность современных технических статей и документации, приведя в пример статью о конкурентном режиме в React, после чего меня попросили изложить свое понимание этого режима. Постараюсь избежать недостатка, который я вчера критиковала, и быть предельно четкой в своих определениях.

🟢 Конкуретный режим в React - это режим, который позволяет нескольким задачам одновременно иметь состояние “в работе”. Таким образом, для того, чтобы начать делать следующую задачу, нам необязательно завершать текущую. При этом конкурентный режим не означает, что React будет выполнять задачи одновременно: он отложит выполнение одной задачи, чтобы начать другую, и потом вернется к ней. Это происходит, когда во время выполнения задачи появляется более высокоприоритетная: React приостанавливает работу над менее приоритетной задачей, чтобы сделать более приоритетную.

Конкурентный режим включается, когда мы используем ReactDOM.createRoot вместо ReactDOM.render для рендера нашего приложения. В React приложениях с клиентским рендером конкурентный режим предоставляет нам две возможности в виде хуков useTranstiion и useDefferedValue. Первый позволяет пометить нам экшен, меняющий какое-то состояние, как низкоприоритетный, чтобы сделать наш интерфейс более отзывчивым. Второй с той же целью позволяет пометить нам наше значение как “низкоприоритетное”, что приведет к тому, что React понизит приоритет обновления этого значения.

От теории к делу: Ваша покорная слуга извращалась с React и так, и сяк, пытаясь найти кейс, в котором useTranstion и useDefferedValue сработают лучше, чем обычный setState. Я ставила счетчики, увеличивающиеся каждую милисекунду, загружала компоненты с огромными джейсонами, добавляла на страницу видео. Визуально мне не удалось найти никакой разницы между обновлением данных через setState и useTransition/useDefferedValue. Но если мне наконец удастся найти такой кейс, вы будете первыми, кто об этом узнает❤️

#react
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍194🤡1
🍝 Готовим React Context

React Context - полноценный инструмент для хранения и чтения глобального состояния. Обычно в контексте хранится набор значений и их методов. Однако он имеет проблему с производительностью: когда меняется любое поле в контексте, все подписчики контекста ререндерятся. Так происходит потому, что объект контекста сравнивается по ссылке, и если меняется поле в контексте, то потребитель получает новую ссылку на контекст, что приводит к ререндеру - даже в том случае, если поле, которое поменялось, не используется в этом компоненте вообще.

✍️ Несколько рецептов, которые помогут вам использовать удобно контекст и снизить проблемы с производительностью:

1️⃣ Разделяйте один контекст на несколько маленьких. Например, вместо одного AppContext с множеством свойств создайте отдельные контексты UserContext, ThemeContext и т.д. В таком случае при изменении свойства в UserContext будут перерендерены только те подписчики, которые подписаны на UserContext, а те, которые подписаны на ThemeContext, не будут - ведь он не изменился.

2️⃣ Кешируйте через useCallback и useMemo значения, которые возвращаете из контекста - в таком случае в компоненте-потребителе вы сможете безопасно передавать их в дочерние компоненты, не опасаясь, что каждое изменение контектста приведет к замене ссылки и перерендеру дочерних компонентов.

3️⃣ Создавайте хук для взаимодействия с контекстом. Вместо того, чтобы в каждом компоненте писать const mydata = useContext(MyContext), сделайте такой простой хук:

export const useMyContext = (): MyContextProps => {
const context = useContext(MyContext);
if (!context) {
throw new Error(‘MyContext not found :(’);
}
return context;
};



#react
🤗10👍4🔥4😱2
Stale props и stale state

📌 Stale state — это ситуация, когда внутри замыкания используется устаревшее значение состояния. Stale props - то же самое, но для пропсов. Рассмотрим на примере, как это происходит.

export const Example = () => {
const [count, setCount] = useState(0);

useEffect(() => {
setTimeout(() => {
console.log(count);
}, 1000);
}, [])

return (
<button
onClick={() => setCount((c) => c + 1)}
>
{count}
</button> // кликнем несколько раз
)
}


Мы имеем React компонент Example, в котором имеется:
1. Состояние counter. 2. Эффект, который единожды сработает после монтирования и который выводит наше состояние в лог. Что будет, если за секунду мы уже успеем кликнуть по кнопке несколько раз и counter изменится? Выведется исходное состояние - то, которое было на момент срабатывания эффекта и установки таймера. Вы можете увидеть это на скриншоте выше. Давайте посмотрим подробнее, как так происходит.

В анонимной функции-колбеке, которую планирует таймер, никакого идентификатора counter не существует. Откуда javascript возьмет его при вызове? Он возьмет его из родительского лексического окружения, то есть из лексического окружения функции Example, которая одновременно является реакт компонентом. Клик на кнопке приведет к изменению состояния, а это, в свою очередь, вызовет ререндер - то есть новый вызов функции Example. Каждый новый вызов создаст свой объект лексического окружения. Однако нашей анонимной функции с логом до этого нет дела - она надежно запомнила, из какого лексического окружения ей нужно брать counter: из того объекта лексического окружения, которое было создано на момент вызова useEffect. Таким образом, когда наш таймер сработал и вывелся лог, мы увидели там устаревшее состояние.

#react
🔥72🤡1
🏎Что такое race conditions в React хуках?

Хуки эффектов, в которых есть запросы с последующей установкой ответа в state могут приводить к race conditions. Race conditions возникает, когда хук выполняется слишком часто, и поскольку очередность ответов от сервера не гарантирована, то более ранние запросы могут завершиться после более поздних и тогда в state будут записаны неверные данные.

Например, вот такой код может привести к проблеме:

useEffect(() => {
fetch(`/api/data?query=${query}`)
.then((res) => res.json())
.then((data) => setData(data));
}, [query]);


Этот эффект будет выполняться при каждом изменении query. Таким образом, при вводе query отправится несколько вопросов, но порядок ответа от сервера и тем самым вызов setState не гарантирован.

Вот здесь я собрала песочницу для демонстрации проблемы.

В этом примере мы видим строку поиска товаров. Я добавила искусственную задержку: чем короче строка поиска, тем дольше выполняется запрос. Попробуйте ввести в поиск слово “dog”. Вы увидите, что сначала отображается один результат (”dog food”), а затем больше и больше результатов.

Почему? Потому что при вводе слова “dog” мы отправляем подряд целых три запроса: запрос с поиском значения “d”, запрос с поиском значения “do” и наконец запрос с поиском значения “dog”. Я реализовала искусственную задержку - чем больше букв в поисковой строке, тем быстрее выполнится запрос. Поэтому ответы нам будут приходить в обратном порядке: сначала ответ для “dog”, потом для “do” и наконец для “d”. Хук setData сработает в порядке возвращения ответов от сервера: сначала установятся значения для “dog”, потом для “do” и наконец для “d”. В этом примере задержка искусственная, но реальный сервер тоже не гарантирует порядок ответов и более ранний запрос может завершится после более позднего. В таком мы установим в state устаревшее состояние.

🪄Как предотвратить race conditions?

Проблему решает AbortController: с его помощью можно отменить предыдущий ответ таким образом, что браузер будет дожидаться ответа только для последнего запроса. Это нативное решение и оно подходит в абсолютном большинстве случаев.

useEffect(() => {
const controller = new AbortController();

fetch(`/api/data?query=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});

return () => controller.abort(); // отменяем прошлый запрос
}, [query]);

#react
Please open Telegram to view this post
VIEW IN TELEGRAM
21
👊 Битва useReducer vs useState: где хранить состояние компонента?

Вы наверняка встречали советы использовать useReducer вместо useState, когда состояние компонента становится "слишком большим и сложным". Но на самом деле дело не только в размере.

Я использую useReducer, когда разные части состояния зависят друг от друга. В таких случаях удобно моделировать поведение компонента как конечный автомат - где редюсер отвечает за допустимые переходы между состояниями.

Представим форму с таким поведением:
- При отправке показывается лоадер.
- Сервер отвечает либо ошибкой, либо успешными данными.
- Ошибка или данные отображаются на странице.
- Повторная отправка формы сбрасывает старое состояние (ошибку и данные)

Интуитивно многие используют следующий код для хранения состояния:

const [error, setError] = useState(false); 
const [data, setData] = useState("");
const [loading, setLoading] = useState(false);

const reset = () => {
setError("");
setData("");
};


const onSubmit = (e) => {
reset();
setLoading(true);
sendData(value).then((result) => {
if (result.type === "error") {
setError(result.iss.onessage);
}
if (result.type === "success") {
setData(result.data);
}
setLoading(false);
});
};

Полный код примера можно посмотреть здесь

🌀 Но с таким кодом легко запутаться и привести систему в неконсистентное состояние: одновременно может быть loading: true, заполненное data и ненулевой error.

Вот так можно переписать это при помощи useReducer:

function reducer(state: FullState, action: Action): FullState {
switch (action.type) {
case "SET_LOADING":
return { loading: true, error: "", data: "" };
case "SET_SUCCESS":
return { loading: false, error: "", data: action.payload };
case "SET_ERROR":
return { loading: false, error: action.payload, data: "" };
}
}

Полный код примера смотреть здесь

Теперь все переходы управляются единой централизованной системой, а в коде компонента нам остается только вызвать нужный экшен - о том, чтобы привести систему в консистентное состояние, позаботится редюсер.

#react
Please open Telegram to view this post
VIEW IN TELEGRAM
119🔥3❤‍🔥1👍1