🍝 Готовим React Context
React Context - полноценный инструмент для хранения и чтения глобального состояния. Обычно в контексте хранится набор значений и их методов. Однако он имеет проблему с производительностью: когда меняется любое поле в контексте, все подписчики контекста ререндерятся. Так происходит потому, что объект контекста сравнивается по ссылке, и если меняется поле в контексте, то потребитель получает новую ссылку на контекст, что приводит к ререндеру - даже в том случае, если поле, которое поменялось, не используется в этом компоненте вообще.
✍️ Несколько рецептов, которые помогут вам использовать удобно контекст и снизить проблемы с производительностью:
1️⃣ Разделяйте один контекст на несколько маленьких. Например, вместо одного
2️⃣ Кешируйте через useCallback и useMemo значения, которые возвращаете из контекста - в таком случае в компоненте-потребителе вы сможете безопасно передавать их в дочерние компоненты, не опасаясь, что каждое изменение контектста приведет к замене ссылки и перерендеру дочерних компонентов.
3️⃣ Создавайте хук для взаимодействия с контекстом. Вместо того, чтобы в каждом компоненте писать
#react
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 - то же самое, но для пропсов. Рассмотрим на примере, как это происходит.
Мы имеем React компонент
1. Состояние
В анонимной функции-колбеке, которую планирует таймер, никакого идентификатора
#react
📌 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
🔥7❤2🤡1
Хуки эффектов, в которых есть запросы с последующей установкой ответа в 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 вместо 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);
});
};
Полный код примера можно посмотреть здесь
Вот так можно переписать это при помощи 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
1❤23🔥3❤🔥1👍1
💥 Как React рендерит UI. Часть 1
Если вам когда-либо было сложно разобраться в порядке вызова эффектов в компоненте или вы просто хотите подготовиться к собеседованию, то это серия постов для вас.
📎 Рендеринг UI в React состоит из двух фаз: render и commit.
📌 Render фаза
Во время render фазы React вызывает функции компонентов. Ваши компоненты — это же обычные javascript функции, верно? Вот React эти функции и вызывает с переданными аргументами. В результате получается новый VDOM. После этого React React сравнивает старый VDOM с новым (этот процесс называется reconciliation) и определяет, какие изменения нужно внести (какие узлы добавить, какие узлы удалить и т.д.)
📌 Commit фаза
React вносит изменения в реальный DOM при помощи js методов вроде🙂 . После внесения изменения в DOM выполняются layout эффекты (
Рассмотрим пример и разберемся по шагам, что здесь происходит:
В этом примере:
*️⃣ Render фаза
1️⃣ Вызывается функция App. Вычисляется VDOM и изменения, которые нужно закоммитить в реальный DOM. Мы увидим лог “render”, потому что функция App была вызвана.
*️⃣ Commit фаза
2️⃣ React обновляет реальный DOM
3️⃣ Выполняется useLayoutEffect. Мы увидим лог “useLayoutEffect”
4️⃣ Изменения отобразятся на экране, и пользователь увидит Hello world.
5️⃣ Выполнится useEffect, и мы увидим лог “useEffect”
В следующих постах посмотрим, как изменится этот флоу, если эффекты вносят изменения в DOM или состояние.
#react
Если вам когда-либо было сложно разобраться в порядке вызова эффектов в компоненте или вы просто хотите подготовиться к собеседованию, то это серия постов для вас.
Во время render фазы React вызывает функции компонентов. Ваши компоненты — это же обычные javascript функции, верно? Вот React эти функции и вызывает с переданными аргументами. В результате получается новый VDOM. После этого React React сравнивает старый VDOM с новым (этот процесс называется reconciliation) и определяет, какие изменения нужно внести (какие узлы добавить, какие узлы удалить и т.д.)
React вносит изменения в реальный DOM при помощи js методов вроде
appendChild, removeChild и т.п. Но это не приводит к их немедленному отображению на экране — js однопоточный и этот поток занят выполнением js кода useLayoutEffect). Они уже могут использовать реальный DOM (например, для вычисления размеров элементов) и вносить в него правки. Только после этого браузер отрисовывает новый UI на экране, после чего выполняются эффекты useEffect.Рассмотрим пример и разберемся по шагам, что здесь происходит:
function App() {
useLayoutEffect(() => console.log('useLayoutEffect'));
useEffect(() => console.log('useEffect'));
console.log('render');
return <div>hello world</div>;
}В этом примере:
В следующих постах посмотрим, как изменится этот флоу, если эффекты вносят изменения в DOM или состояние.
#react
Please open Telegram to view this post
VIEW IN TELEGRAM
1❤29🔥10🤮6💩3👎2💘2
💥 Как React рендерит UI. Часть 2
В прошлой серии мы поговорили с вами о том, что рендеринг в React состоит из двух фаз — render и commit. В render фазе вычисляются новые VDOM и Fiber tree, в commit фазе сначала обновляется DOM, затем выполняются useLayoutEffect, после чего следует отрисовка и, наконец, выполняются useEffect.
Сегодня мы рассмотрим useLayoutEffect подробнее.
Многие из вас знают, что в useLayoutEffect можно подшаманить UI перед отрисовкой и тогда верстка не будет прыгать. Например, в следующем кейсе пользователь сразу увидит красивый нежно-розовый прямоугольник без скучного серого перед ним:
Это происходит за счет того, что useLayoutEffect выполняется перед отрисовкой. Браузер построил DOM с серым прямоугольником, затем выполнил js код с useLayoutEffect, в котором содержится смена цвета. После этого браузер внес изменения в DOM и затем наконец отрисовал интерфейс.
А что произойдет, если мы в useLayoutEffect будем менять состояние? Помним, что обновление состояния происходит асинхронно — новое значение появится только в следующем рендере. Как думаете: в примере ниже пользователь сначала увидит серый прямоугольник или сразу розовый?
Ответ на этот вопрос я напишу позже, а пока вы можете делиться вашими догадками в комментариях :)
#react
В прошлой серии мы поговорили с вами о том, что рендеринг в 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
🔥15❤11🤮6👍3👎2💩2
💥 Как React рендерит UI. Часть 3
Мнения разделились. Правы оказались те, кто выбрал романтичный розовый цвет 💗 (пруф). Но подождите… ведь переменная состояния обновляется только в следующем рендере, верно?
Так и есть. При вызове setState внутри useLayoutEffect React прерывает текущую commit фазу и сразу запускает новую render фазу. Таким образом, после первого рендера пользователь ничего не увидит — до стадии отрисовки React не дошел. А вот после второго увидит розовый прямоугольник🌸
В следующей серии поговорим о том, что будет, если в дело вступит useEffect💫
#react
Мнения разделились. Правы оказались те, кто выбрал романтичный розовый цвет 💗 (пруф). Но подождите… ведь переменная состояния обновляется только в следующем рендере, верно?
Так и есть. При вызове 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
В прошлый раз мы с вами убедились в том, что
Ок, а что насчет
#react
В прошлый раз мы с вами убедились в том, что
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🥰5❤3👎3💩3👍1
💥 Как React рендерит UI. Часть 5
Вопреки ожиданиям голосующих, в примере выше React зависнет на две секунды, прежде чем что-то покажет на экране. Пруф здесь. Давайте разберемся, почему так.
Ранее мы выяснили, что при изменении состояния в
Но тут появляется важная деталь: прежде чем начать новый рендер, React обязан выполнить эффекты предыдущего. Это официальная гарантия:
Именно поэтому в нашем примере после
#react
Вопреки ожиданиям голосующих, в примере выше 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💩4❤3☃2👎2🫡2🤨1💘1
💥 Как React рендерит UI. Часть 6
Вдогонку к вчерашнему посту: а вот такой кейс сработает ожидаемо — мы сразу увидим розовый прямоугольник, и только потом интерфейс подвиснет. Все потому, что в этом случае рендер будет только один и useEffect сработает после отрисовки.
#react
Вдогонку к вчерашнему посту: а вот такой кейс сработает ожидаемо — мы сразу увидим розовый прямоугольник, и только потом интерфейс подвиснет. Все потому, что в этом случае рендер будет только один и 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👍10❤7💩4👎3🔥3