Кодовая база
1.28K subscribers
16 photos
1 video
32 links
База во фронтенд разработке.

Написать в личку: https://t.iss.one/devmargooo
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👍195🤡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
123🔥3❤‍🔥1👍1
💥 Как React рендерит UI. Часть 1

Если вам когда-либо было сложно разобраться в порядке вызова эффектов в компоненте или вы просто хотите подготовиться к собеседованию, то это серия постов для вас.

📎 Рендеринг UI в React состоит из двух фаз: render и commit.

📌 Render фаза

Во время render фазы React вызывает функции компонентов. Ваши компоненты — это же обычные javascript функции, верно? Вот React эти функции и вызывает с переданными аргументами. В результате получается новый VDOM. После этого React React сравнивает старый VDOM с новым (этот процесс называется reconciliation) и определяет, какие изменения нужно внести (какие узлы добавить, какие узлы удалить и т.д.)

📌 Commit фаза

React вносит изменения в реальный DOM при помощи js методов вроде appendChild, removeChild и т.п. Но это не приводит к их немедленному отображению на экране — js однопоточный и этот поток занят выполнением js кода 🙂. После внесения изменения в DOM выполняются layout эффекты (useLayoutEffect). Они уже могут использовать реальный DOM (например, для вычисления размеров элементов) и вносить в него правки. Только после этого браузер отрисовывает новый UI на экране, после чего выполняются эффекты useEffect.

Рассмотрим пример и разберемся по шагам, что здесь происходит:

function App() {
useLayoutEffect(() => console.log('useLayoutEffect'));
useEffect(() => console.log('useEffect'));
console.log('render');
return <div>hello world</div>;
}


В этом примере:

*️⃣Render фаза

1️⃣Вызывается функция App. Вычисляется VDOM и изменения, которые нужно закоммитить в реальный DOM. Мы увидим лог “render”, потому что функция App была вызвана.

*️⃣Commit фаза

2️⃣ React обновляет реальный DOM
3️⃣ Выполняется useLayoutEffect. Мы увидим лог “useLayoutEffect”
4️⃣ Изменения отобразятся на экране, и пользователь увидит Hello world.
5️⃣ Выполнится useEffect, и мы увидим лог “useEffect”

В следующих постах посмотрим, как изменится этот флоу, если эффекты вносят изменения в DOM или состояние.

#react
Please open Telegram to view this post
VIEW IN TELEGRAM
129🔥10🤮6💩3👎2💘2
💥 Как React рендерит UI. Часть 2

В прошлой серии мы поговорили с вами о том, что рендеринг в React состоит из двух фаз — render и commit. В render фазе вычисляются новые VDOM и Fiber tree, в commit фазе сначала обновляется DOM, затем выполняются useLayoutEffect, после чего следует отрисовка и, наконец, выполняются useEffect.
Сегодня мы рассмотрим useLayoutEffect подробнее.

Многие из вас знают, что в useLayoutEffect можно подшаманить UI перед отрисовкой и тогда верстка не будет прыгать. Например, в следующем кейсе пользователь сразу увидит красивый нежно-розовый прямоугольник без скучного серого перед ним:

// index.css

.rect { width: 300px; height: 200px; background: grey; }

// App.tsx

export default function App() {
const rect = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
if (!rect.current) return;
rect.current.style.backgroundColor = "pink";
}, []);

return <div className="rect" ref={rect}></div>;
}


Это происходит за счет того, что useLayoutEffect выполняется перед отрисовкой. Браузер построил DOM с серым прямоугольником, затем выполнил js код с useLayoutEffect, в котором содержится смена цвета. После этого браузер внес изменения в DOM и затем наконец отрисовал интерфейс.

А что произойдет, если мы в useLayoutEffect будем менять состояние? Помним, что обновление состояния происходит асинхронно — новое значение появится только в следующем рендере. Как думаете: в примере ниже пользователь сначала увидит серый прямоугольник или сразу розовый?

function sleep() {
const start = Date.now();
while (Date.now() - start < 2000) {}
}

export default function App() {
const [color, setColor] = useState("gray");

useLayoutEffect(() => {
sleep(); // спим 2 секунды
setColor("pink");
console.log(color); // серый - обновление произойдет перед следующим рендером
}, []);

return (
<div
style={{
width: "300px",
height: "200px",
background: color,
}}
></div>
);
}


Ответ на этот вопрос я напишу позже, а пока вы можете делиться вашими догадками в комментариях :)

#react
🔥1511🤮6👍3👎2💩2
💥 Как React рендерит UI. Часть 3

Мнения разделились. Правы оказались те, кто выбрал романтичный розовый цвет 💗 (пруф). Но подождите… ведь переменная состояния обновляется только в следующем рендере, верно?

Так и есть. При вызове setState внутри useLayoutEffect React прерывает текущую commit фазу и сразу запускает новую render фазу. Таким образом, после первого рендера пользователь ничего не увидит — до стадии отрисовки React не дошел. А вот после второго увидит розовый прямоугольник 🌸

В следующей серии поговорим о том, что будет, если в дело вступит useEffect💫

#react
Please open Telegram to view this post
VIEW IN TELEGRAM
20👍10🔥7👎4🤯4🤮2🤡1
💥 Как React рендерит UI. Часть 4

В прошлый раз мы с вами убедились в том, что useLayoutEffect действительно блокирует рендер. Если в useLayoutEffect есть “тяжелый” код (в нашем примере это была задержка в две секунды), то интерфейс зависнет, а затем покажет обновленный UI — с правками, внесенными в useLayoutEffect.

Ок, а что насчет useEffect? Мы знаем, что он выполняется после отрисовки. Как думаете, код ниже зависнет на 2 секунды прежде чем отобразить прямоугольник на экране или нет?

function sleep() {
const start = Date.now();
while (Date.now() - start < 2000) {}
}

export default function App() {
const [color, setColor] = useState("gray");

useLayoutEffect(() => { setColor("pink"); }, []);

useEffect(() => { sleep(); // спим 2 секунды }, []);

return (
<div
style={{
width: "300px",
height: "200px",
background: color,
}}
></div>
);
}

#react
🤡10🔥6🥰53👎3💩3👍1
💥 Как React рендерит UI. Часть 5

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

Ранее мы выяснили, что при изменении состояния в useLayoutEffect React пропустит браузерную отрисовку и перейдет в рендер фазу. В нашем примере мы как раз меняем состояние в useLayoutEffect, именно поэтому мы должны пропустить отрисовку и перейти к рендер фазе.

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

Even in cases where useEffect is deferred until after the browser has painted, it’s guaranteed to fire before any new renders. React will always flush a previous render’s effects before starting a new update.


Именно поэтому в нашем примере после useLayoutEffect React вызывает useEffect. В нашем случае useEffect содержит блокирующую операцию, поэтому интерфейс подвиснет. Затем React доходит до ререндера, и только после этого UI мы наконец увидим наш UI на экране🌸


#react
🔥12👍8🤮8😱5💩432👎2🫡2🤨1💘1
💥 Как React рендерит UI. Часть 6

Вдогонку к вчерашнему посту: а вот такой кейс сработает ожидаемо — мы сразу увидим розовый прямоугольник, и только потом интерфейс подвиснет. Все потому, что в этом случае рендер будет только один и useEffect сработает после отрисовки.

function sleep() {
const start = Date.now();
while (Date.now() - start < 2000) {}
}

function App() {
useEffect(() => {
sleep(); // спим 2 секунды
}, []);

return (
<div
style={{
width: '300px',
height: '200px',
background: 'pink',
}}
></div>
);
}


#react
🤮12👍107💩4👎3🔥3