Данный топик частично основан на докладе Тёмы Синюкова c HolyJS.
- Введение
- throttle/debounce
- Вынос состояния
- Children Prop
- useContext
- Правильный условный рендеринг
- Использование key
Для начала надо понять, что не каждый рендер в React вызывает отрисовку DOM в браузере. Например, компонент ниже будет рендериться каждую секунду и каждую секунду будет выполняться отрисовка браузером:
export const WithChangeView = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => setCount((count) => count + 1), 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
};
А в примере ниже, компонент так же будет рендериться каждую секунду, но отрисовка в браузере произойдёт лишь один раз:
const WithoutChangeView = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => setCount((count) => count + 1), 1000);
return () => clearInterval(interval);
}, []);
return <div>Hello</div>;
};
Если вы столкнулись с проблемами производительности, то в первую очередь необходимо бороться с лишними рендерами, когда эти рендеры приводят к отрисовке браузером (как в первом примере). А уже потом со всеми остальными.
В случае, если вы подписываетесь на событие, которое может срабатывать слишком часто (время, скролл, ресайз, движение мыши и т.д.), например несколько раз в секунду и вам это явно не нужно, воспользуйтесь throttle или debounce. В чём разница можно почитать тут.
Один из примеров, это подписка на событие scroll
, которое будет срабатывать крайне часто. И вряд ли вам вам нужно реагировать на это событие так же часто. Решение - обернуть обработчик в функцию throttle
:
export const ScrollWithThrottle = () => {
const [scroll, setScroll] = useState(0);
useEffect(() => {
const handleWindowScroll = () => {
setScroll(window.scrollY);
};
const throttledHandleWindowScroll = throttle(100, handleWindowScroll);
window.addEventListener('scroll', throttledHandleWindowScroll);
return () => window.removeEventListener('scroll', throttledHandleWindowScroll);
}, []);
return <div>{scroll}</div>;
};
Допустим у вас есть компонент с собственным состоянием, который при этом подключает какой-то тяжёлый <SlowComponent />
. При каждом изменении состояния, <SlowComponent />
будет перерендериваться:
const Component = () => {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount((count) => count + 1)}>Plus</button>
<SlowComponent />
</div>
);
};
Решение - вынести состояние в отдельный компонент:
const Component = () => {
return (
<div>
<TriggerComponent />
<SlowComponent />
</div>
);
};
const TriggerComponent = () => {
const [count, setCount] = useState(0);
return (
<>
<span>{count}</span>
<button onClick={() => setCount((count) => count + 1)}>Plus</button>
</>
);
};
Теперь изменение состояние не приводит к перерендеру <SlowComponent />
.
У вас есть компонент, подключащий множество других компонентов и содержащий логику, которая заставляет этот компонент часто перерендериваться. Как итог, все его потомки будут тоже часто перерендериваться:
const Page = () => {
const [isHovered, setIsHovered] = useState(false);
const [scroll, setScroll] = useState();
// Logic
return (
<div
onScroll={setScroll}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Header />
<Content />
<Footer />
</div>
);
};
Решение - вынести рендер компонентов на уровень выше, передавая результат рендера через пропсы.
const Layout = ({ top, center, bottom }) => {
const [isHovered, setIsHovered] = useState(false);
const [scroll, setScroll] = useState();
// Logic
return (
<div
onScroll={setScroll}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div>{top}</div>
<div>{center}</div>
<div>{bottom}</div>
</div>
);
};
const Page = () => {
return (
<Layout
top={<Header />}
center={<Content />}
bottom={<Footer />}
/>
);
};
Теперь компоненты Header
, Content
и Footer
не являются прямыми потомками в Layout
, и не будут перерендериваться при изменении состояний в Layout
.
При работе с useContext
есть сразу несколько подводных камней.
-
В примере ниже, все элементы внутри
Provider
являются его прямыми потомками. Это приведёт к тому, что любое изменение состояниеtheme
приведёт к перерендеру всех дочерних элементов. Даже тех, кто не использует значение из контекста.const Context = React.createContext(); const Component = () => { const [theme, setTheme] = useState("default"); return ( <Context.Provider value={{ theme, setTheme }}> <div> <Child /> И еще очень много дочерних </div> </Context.Provider> ); };
Что бы этого избежать, всегда выносите
Provider
в отдельный компонент и передавайте потомков через пропchildren
.const Context = React.createContext(); const Component = () => { return ( <ThemeProvider> <div> <Child /> И еще очень много дочерних </div> </ThemeProvider> ); }; const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState("default"); return ( <Context.Provider value={{ theme, setTheme }}> {children} </Context.Provider> ); };
-
В предыдущем примере есть другая важная проблема - это хранение объекта в
value
. При каждом изменении состояния и перерендереThemeProvider
, объект будет создаваться заново и все компоненты, которые читают контекст, будут перерендериваться. Что бы этого избежать, можно обернуть значение вuseMemo
.const Context = React.createContext(); const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState("default"); const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]); return ( <Context.Provider value={value}> {children} </Context.Provider> ); };
-
Но это не решит всех проблем, ведь все компоненты, которые используют только
setTheme
будут перерендериваться при измененииtheme
, хотя сами ониtheme
никак не используют. Это решается разнесением контекста. Один контекст для состояния, другой для сеттера.const Context = React.createContext(); const SetterContext = React.createContext(); const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState("default"); return ( <Context.Provider value={theme}> <SetterContext.Provider value={setTheme}> {children} </SetterContext.Provider> </Context.Provider> ); };
Вот теперь всё хорошо, компоненты использующие
setTheme
не будут перерендериваться при измененииtheme
.А вообще, лучше используйте стейт менеджеры, которые решают проблемы оптимального ререндера компонентов (MobX, Zustand и др.).
Допустим, у вас есть компонент, который должен отображаться только авторизованным пользователям.
const AuthorizedUser = () => {
// здесь много хуков и логики
const isAuthorized = useIsAuthorized();
if (!isAuthorized) {
return null;
}
return <>...</>;
};
Проблема в том, что все хуки и логика, находящиеся перед if
всегда выполнятся. Решение - проверку на рендер компонента AuthorizedUser
делать в родителе. Для переиспользования вы можете вынести проверку в HOC:
const withAuthorize = ({ AuthorizedUser, UnAuthorizedUser }) => {
const Component = function WithAuthorizeComponent({ authProps, unAuthProps }) {
const isAuthorized = useIsAuthorized();
return isAuthorized ? (
<AuthorizedUser {...authProps} />
) : (
<UnAuthorizedUser {...unAuthProps} />
);
};
return Component;
};
Вы можете указать React, что ваш элемент не изменился и его не нужно пересоздавать. В примере ниже, компонент Child
может находится в разных позициях и React по умолчанию назначит ему разный key
:
const Example = ({ isSecondChildVisible }) => {
if (!isSecondChildVisible) {
return <Child />; // здесь key = 1
}
return (
<>
<div>Visible</div> // Здесь key = 1
<Child /> // Здесь key = 2
</>
);
};
При изменении условия, у Child
будет разный key
и React начнёт размонтировать первый Child
и монтировать второй Child
. Что бы этого избежать, можно явно задать одинаковый key
для обоих Child
:
const Example = ({ isSecondChildVisible }) => {
if (!isSecondChildVisible) {
return <Child key="child" />;
return (
<>
<div>Visible</div>
<Child key="child" />
</>
);
};
Теперь при изменении условия, React увидит, что у элементов Child
одинаковый key
и их нужно просто поменять местами, без необходимости размонтирования/монтирования.
Почему это так работает? Читайте тут и смотрите этот таймкод.