Бұл жазба автоматты түрде аударылған. Бастапқы тіл: Ағылшын
Шынымызды айтсақ — React қолданбаларын дебагтау әрқашан оңай бола бермейді. Сіз жылтыраған компонент жасайсыз, бәрі дұрыс сияқты, ал сосын… бум! Қолданба баяулайды, консоліңіз түсініксіз қателер шығарады немесе одан да жаманы — бірдеңе үнсіз бұзылады. Әрі бұл, әрине, тек біреуге көрсеткен кезде болады.
React тамаша. Ол декларативті, қуатты және икемді. Бірақ сол қуатпен бірге күрделілік те келеді — әсіресе қолданбаңыз үлкейген сайын. Компоненттер түсініксіз себептермен қайта рендерленеді. State өз бетінше әрекет еткендей болады. Хукстар «еркінсіп» кетеді. Асинхронды код ше? Нағыз джунгли.
Сонымен, неге тиімді дебагтау қазіргі React дамуы үшін соншалық маңызды?
Себебі багтар сөзсіз — бірақ нашар пайдаланушы тәжірибесі міндетті емес.
Қазіргі қолданбаларда өнімділік — бұл фича. Пайдаланушылар лезде жауап беруді, мінсіз навигацияны және нөлдік құлауды күтеді. Ал егер біз дұрыс дебаг жасамасақ, «жұмыс істейтін… бірақ бір сәтте тоқтайтын» нәрсені жеткізуіміз мүмкін. Сондықтан React-ті дұрыс дебагтауды меңгеру — «жақсы болса екен» емес, қажет.
Бұл мақалада біз әзірлеушілер React-пен жұмыс істегенде жиі кездесетін мәселелерді талдаймыз:
- қажетсіз қайта рендерлер (сәлем, баяу UI)
- жадтың ағып кетуі (қолданбаңыз RAM-ды кәмпиттей «жейді»)
- хукстардың дұрыс жұмыс істемеуі (мына қате не өзі?)
- асинхронды багтар (неге бұл дерек екі рет жаңарады?)
Бірақ уайымдамаңыз — біз тек проблемаларды ғана емес, шешімдерді де қарастырамыз. React-тің «капотының астында» не болып жатқанын түсінеміз, әрі React DevTools, why-did-you-render және логтау сервистері сияқты құралдарды қолдана отырып, қателерді пайдаланушылар байқамай тұрып қалай табуға болатынын көрсетеміз.
Кофе алыңыз да, дебагтау әлеміне plonge жасайық. Сіздің React қолданбаларыңыз бұрынғыдан да таза, жылдам және сенімді болады.
А-а, қайта рендерлер. Кейде олар React-тің пассивті-агрессивті хабарындай сезіледі: «Сен бірдеңе жасадың… бірақ не екенін айтпаймын».
Алдымен React қайта рендерлерді қалай орындайтынын түсініп алайық.
React UI-ды жаңарту үшін «reconciliation» алгоритмін қолданады. Ол алдыңғы виртуалды DOM-ды жаңасымен салыстырады да, нақты DOM-ға тек минимал өзгерістерді қолданады. Тиімді естіледі, солай ма? Иә — бірақ компоненттер шынында қажет емес кезде қайта рендерлене бастағанда ғана емес.
Елестетіңіз: батырма parent жаңарған сайын қайта рендерленеді, ал оның props-тары өзгермейді. Мұны 50 компонентке көбейтіңіз — қолданбаңыз «картопқа» айналады.
Ендеше, мұны қалай болдырмаймыз?
Егер компонент бірдей props алса және әрдайым бірдей нәтижені шығарса — оны React.memo() ішіне ораңыз. Бұл React-ке: «Шын өзгеріс болмаса, тиме» деумен бірдей.
import React from 'react';
import { FC, memo } from 'react';
type ButtonProps = {
label: string;
}
export const Button: FC<ButtonProps> = memo(({label}) => {
console.log('Button rendered');
return <button>{label}</button>;
})
Бұл хуктар әр рендер сайын мәндерді қайта есептеуді немесе функцияларды қайта жасауды болдырмайды.
- useMemo қымбат есептеулердің нәтижесін кэштайды
- useCallback функция сілтемесін кэштайды, сондықтан бала компоненттер қажетсіз қайта рендерленбейді
import React from 'react';
import { FC, useMemo, useCallback, useState } from 'react';
export const ExpensiveComponent: FC = () => {
const [count, setCount] = useState(0);
const expensiveValue = useMemo(() => count * 2, [count]);
const handleClick = useCallback(() => setCount(prev => prev + 1), []);
return (
<div>
<p>{expensiveValue}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
Бірақ міне мәселе: оларды артық қолданбаңыз. Олар сиқырлы өнімділік күшейткіштері емес — өздерінің құны бар, мысалы, жадты көбірек тұтыну және кэшті жүргізу шығыны. Оларды нақты өнімділік мәселесін байқағанда немесе профайлинг құралдарында қайталанатын қайта рендерлерді көргенде ғана қолданыңыз.
Қажетсіз қайта рендерлерді анықтауға арналған ең жақсы dev-құралдардың бірі. Бұл кітапхана қай компонент қайта рендерленгенін және не себепті екенін дәл айтады — көбіне көз ашатын дерек.
import React from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React);
}
Оны development режимінде қосып қойыңыз — ол консольге мынадай детальдар шығарады:
[why-did-you-render] <MyComponent> re-rendered because props.changed
Қолданбаңыз түсініксіз бәсеңдеп кеткенде және себебін таба алмай жүргенде — аса пайдалы.
Егер сіз әлі React DevTools қолданбасаңыз, сіз React-ті көз жұмып дебагтап жүрсіз. Бұл құрал — компоненттер әлеміне терезе: қолданбаңызға арналған «рентген».
Тереңірек қарастырайық.
Components қойындысы толық компонент ағашын көрсетеді. Компоненттерді шертіп, нақты уақыттағы props пен state-ті көресіз. Тіпті мәндерді жылдам өңдей аласыз — қолданбаңызбен «құдай режимінде» ойнағандай боласыз.
Компоненттің неге солай әрекет ететінін білгіңіз келе ме? Оның ағымдағы props/state-ін қараңыз. Бұл — бірінші із.
ℹ️ Пайдалы кеңес: компонентті оң жақ батырмамен шертіп, “Store as global variable” таңдауға болады. Сонда браузер консолінен оны $r ретінде тікелей басқара аласыз.
$r.setState({open: true})
Дереу — тест үшін күйді ауыстыру.
Қайта рендерлерді дебагтағанда — нағыз алтын. DevTools баптауларында (тісше белгішесі) мынаны қосыңыз:
- Highlight updates when components render.
Енді компонент әр қайта рендерленгенде, қысқа уақытқа жыпылықтайды. Егер іздеу жолағына тергенде футер жыпылықтаса — бұл дұрыс емес. Тексеретін уақыт келді.
Profiler қойындысына өтіп, жазуды қосып, қолданбаңызбен әрекеттесіңіз. Сіз қандай компоненттер рендерленгенін, қанша уақыт алғанын және неге рендер болғанын көрсететін «flame graph» аласыз.
Мынадай себептерді көресіз:
- props өзгерді
- context өзгерді
- hook мәні өзгерді
Бұл — тар жерлер мен босқа жасалған рендерлерді табуда ең жақын досыңыз.
ℹ️ Қосымша кеңес: profiler қайталанатын рендерлерді байқаса, кей компоненттерді React.memo() ішіне орауды да ұсынады.
Хукстар React-ке қуат пен сымбат әкелді — бірақ жаңа класстағы багтарды да қосты. Хук бір рет іске қосылады деп ойлап едіңіз — екі рет атылды ма? Әлде useEffect unmount кезінде біртүрлі әрекет етті ме? Иә, мұның бәрі таныс.
Ең жиі кездесетін «хук бас ауруларын» тарқатып, оларды профи сияқты қалай дебагтауға болатынын көрейік.
Егер custom hook жазып, оның ішінде шын мәнінде не боп жатқанын білмей жүрсеңіз — useDebugValue қолданып көріңіз.
import React from 'react';
import { useDebugValue, useState, useEffect } from 'react';
export const useAuthStatus = (): boolean => {
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => setLoggedIn(true), 1000);
return () => clearTimeOut(timeout);
}, []);
useDebugValue(loggedIn ? 'Logged In' : 'Logged Out');
return loggedIn;
}
Енді React DevTools ішінде, хук атауының жанында «Logged In» немесе «Logged Out» деген жапсырманы көресіз. Хуктың ішкі күйін консольді «лазамай» әдемі көрсетудің жолы.
Бұл продакшнда шықпайды, сондықтан dev-режимде қолдану қауіпсіз.
Мойындайық: useEffect қуатты, бірақ оңай шатасады. Назар аударатын нәрселер:
- dependency массивінің болмауы — эффект күтпегеннен көп жұмыс істейді.
- stale closures — асинхронды кері шақыру ішінде ескі state/props қолданылып қалғанда.
- race conditions — әсіресе fetch не setTimeout сияқты сценарийлерде.
Компонент unmount болса, fetch-ті қалай тоқтатамыз? AbortController қолданыңыз:
import { useEffect, useState } from 'react';
const useUserData = (): unknown => {
const [data, setData] = useState<unknown>(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async (): Promise<void> => {
try {
const res = await fetch('/api/user', { signal: controller.signal });
const result = await res.json();
setData(result);
} catch (error: unknown) {
if ((error as Error).name !== 'AbortError') {
console.error(error);
}
}
}
fetchData();
return () => controller.abort();
}, []);
return data;
};
Бұл өте жиі кездесетін багты болдырмайды: unmount болған компоненттің state-ін жаңартуға тырысу.
Хуктарыңыз модульді және қадағалана алатын болса, дебаг әлдеқайда оңай. Бір алып useEffect жазғанша, бөліп тастаңыз:
useEffect(() => {
// subscribe to service
}, []);
useEffect(() => {
// update on prop change
}, [prop]);
useEffect(() => {
// cleanup or side-effect
}, [state]);
Бұл әсіресе hot reload немесе жылдам жаңартулар кезінде «қайсысы не істеп тұр» дегенді бақылауды жеңілдетеді.
React көп нәрсені кешіреді… бірақ әрқашан емес. Бір бұзылған компонент бүкіл UI-ды құлатуы мүмкін. Осы жерде error boundaries көмекке келеді — бәрі қисайғанда қолданбаңыздың қауіп торы.
Error boundary — компоненттер үшінгі try...catch іспетті. Олар рендер уақытындағы және lifecycle-қателерді ұстап, қосымшаның бір бөлігі құласа да, тұтас қолданбаның құлауына жол бермейді.
Негізгі орнату:
import React from 'react';
import { Component, ErrorInfo, PropsWithChildren } from 'react';
type ErrorBoundaryState = {
hasError: boolean;
};
class ErrorBoundary extends Component<PropsWithChildren, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false };
static getDerivedStateFromError(): ErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}
export default ErrorBoundary;
Сосын қолданбаңыздың «қауіпті» бөліктерін былай ораңыз:
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
Дайын — егер UserProfile құласа, пайдаланушы бос экранды емес, достық хабарламаны көреді.
Бәрін түгел орай берудің қажеті жоқ. Бүкіл қолданбаны орау — жақсы «соңғы қамал», бірақ нағыз күш — шектелген аймақтарды орауда:
- қашық деректерге тәуелді компоненттердің айналасында (API істемей қалды ма? Фолбэк көрсетіңіз);
- фича-эксперименттер айналасында (A/B тест сәтсіз болды ма? Сайт құламасын);
- күрделі рендер логикасы бар бөліктерде (көзбен сызу/чарттар, динамикалық UI және т.б.).
Қателікті жергілікті ұстау — істің жартысы ғана. Енді оны пайдалы жерге жолдау керек.
componentDidCatch ішінде немесе getDerivedStateFromError блоктарында қатені Sentry, LogRocket, Replay.io сияқты логтау құралдарына жіберіңіз:
componentDidCatch(error, info) {
logErrorToService(error, info);
}
Қателерді локалда ұстау — жақсы. Ал продакшнда нақты не болғанын білу ше? Бұл — келесі деңгей. Логтау құралдарымен біріктіру пайдаланушы көрген багтарды және оның контекстін көруге мүмкіндік береді.
Sentry — «суперқабілеті бар» краш-репортер. Ол ерекшеліктерді қадағалайды, стек-трейстерді көрсетеді және бұзылған нақты код жолдарын байланыстырады.
React қолданбасына Sentry қосудың жолы:
npm install @sentry/react @sentry/tracing
Сосын қолданбаның түбінде бір рет баптаңыз:
import as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";
Sentry.init({
dsn: "https://your-dsn-url",
integrations: [new BrowserTracing()],
tracesSampleRate: 1.0,
});
Error boundary-ді Sentry-дің өз өңдегішіне орағыңыз келе ме?
const SentryBoundary = Sentry.withErrorBoundary(MyComponent, {
fallback: <p>Oops, something went wrong.</p>,
});
Кейде логтар жеткіліксіз. Пайдаланушы не істеді — шерту, жылжыту, формаға жазу — крашқа дейін барлығын көргіңіз келеді.
Міне, LogRocket және Replay.io осы жерде жарқырайды. Олар сессияларды этикалық және қауіпсіз түрде жазып алады — қолданушының иығынан қарап тұрғандай боласыз.
LogRocket интеграциясының мысалы:
npm install logrocket
Қолданбада инициализациялаңыз:
import LogRocket from 'logrocket';
LogRocket.init('your-app-id');
Сондай-ақ LogRocket-ті Sentry-мен байланыстырып, қате болған нақты сессияны көруге болады.
Дебагтау — тек қателерді шешу емес; ол контексті түсіну туралы. Sentry және LogRocket сияқты құралдар сізге толық көрініс береді.
- құпия деректерді логтамаңыз: email, токен, парольдерді алып тастаңыз;
- продакшнда логтарды «сиретіңіз»: маңызды сигналдарды «су басудан» сақтаңыз;
- орта бойынша сүзгілер қолданыңыз: dev/staging-те қажет болмаса, жазба жүргізбеңіз;
- қателерді белгілеңіз және топтаңыз: user ID, қолданба нұсқасы сияқты метадеректермен триажды жеделдетіңіз.
Асинхронды код қиын. Батырманы басасыз, жауап күтесіз… және бум — бірдеңе дұрыс емес. Бәлкім, ештеңе болмаған шығар. Немесе екі нәрсе бірден орындалған шығар. Не болмаса, бірдеңе жол ортасында тоқтап қалған шығар.
React-та асинхронды хаосты қалай ауыздықтаймыз — тарқатайық.
Ең алдымен — браузердің Network қойындысын қолданыңыз. Ол мына нәрселерді көрсетеді:
- сұраныс шығып кеткен-кетпегенін
- жауап қандай болғанын
- қате болса — неліктен екенін
Кодта, қателерді анық көру үшін fetch/axios-ты try/catch ішіне ораңыз:
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch("/api/data");
const json = await res.json();
setData(json);
} catch (error) {
console.error("API error:", error);
}
};
fetchData();
}, []);
Айталық, пайдаланушы іздеу өрісіне жылдам теріп жатыр — бірнеше fetch қатар ұшты. Бізге соңғысы ғана маңызды — қалғаны тоқтауы тиіс.
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(`/api/search?q=${query}`, { signal })
.then (res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== "AbortError") {
console.error("Search error:", err);
}
});
return () => controller.abort();
});
Бұл ескі сұраныстардың жаңа деректерді басып кетпеуін қамтамасыз етеді — өте жиі кездесетін баг.
Mock Service Worker (MSW) сияқты құралдар әзірлеу мен тестілеу кезінде API мінезін симуляциялауға көмектеседі. Осылайша, қолданбаның жүктелу, қате және кідіріс жағдайларына реакциясын сынай аласыз.
jest және @testing-library/react арқылы мінез-құлықты растаңыз:
test("displays loading then data", async () => {
render(<MyComponent />);
expect (screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText(/data loaded/i)).toBeInTheDocument();
});
Шынайы әлемдегі шеткі жағдайларды симуляциялай алғанда, асинхронды багтарға орын қалмайды.
useState, useReducer, useContext сияқты хукстарды бәріміз жақсы көреміз. Бірақ оларды дебагтау — «айғақсыз жұмбақ шешкендей».
Мұны түзетейік.
Ең қарапайым бастама — state өзгерістерін логтау:
useEffect(() => {
console.log("Current state: ", myState);
}, [myState]);
useReducer үшін логтауды тікелей редьюсердің ішінде қосыңыз:
function reducer(state, action) {
console.log("Dispatching:", action);
console.log("Previous state:", state);
const newState = { ...state, ...action.payload };
console.log("New state:", newState);
return newState;
}
Бұл — не, қашан өзгеріп жатқанын бақылаудың «тез әрі лас» жолы.
React DevTools-та context тұтынатын компонентті таңдасаңыз — «Context» деген арнайы бөлімде ағымдағы мәндерді көресіз.
Бұл — терең орналасқан провайдерлер немесе сәйкес келмейтін тұтынушыларды дебагтағанда өте пайдалы.
Сонымен қатар context-ті дебагқа ыңғайлы ету үшін ат беруге болады:
export const ThemeContext = React.createContect('light');
ThemeContext.displayName = 'ThemeContext';
DevTools-тағы «Context.Provider» деген «құпия нысандар» енді түсінікті атпен көрінеді.
- байланысты мәндерді бір объектіге топтаңыз (useState({ name, age }))
- логика күрделі болса, useReducer қолданыңыз
- артық ұялатпаңыз — тым терең объектілерді тексеру/жаңарту қиын
- үлкен деректер жиындарын «нормализациялаңыз» (Redux-стилі) — жаңартуды жеңілдетеді
Тестер тек регрессияны болдырмау үшін ғана емес — «бүркенген» дебаг құралдары. Тест құласа, дәл қай жерде, неге бұзылғанын көрсетеді.
- unit-тесттер — хук, редьюсер, компонент логикасындағы багтарды ұстайды;
- end-to-end (E2E) тесттер (мысалы, Cypress немесе Playwright) шынайы пайдаланушы мінезін симуляциялайды — шерту, теру, жіберу.
Тесттегі сәтсіздік — көбіне қолмен байқалмай қалған логикалық ақаудың белгісі.
Testing Library пайдаланушы әрекетіне жақын тесттерді ынталандырады — сондықтан онда бұзылса, реалда да бұзылуы ықтимал.
test("button submits form data", async () => {
render(<Form />);
userEvent.type(screen.getByLabelText("Name"), "John")
userEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/thank you/i)).toBeInTheDocument();
});
Егер бұл тест құласа, батырма табылды ма, state жаңарды ма, эффект атылды ма — бәрін айтады; бұл — кіріктірілген «дебаггер» сияқты.
React қолданбаларын дебагтау кейде бас айналдырады — бірақ дұрыс құралдар, ойлау тәсілі және құрылым арқылы багтар қорқынышты түстен гөрі үйрететін сәттерге айналады.
- компоненттерді, state-ті және қайта рендерлерді тексеру үшін React DevTools пайдаланыңыз
- асинхронды эффектілерді AbortController арқылы тазалаңыз
- хуктардың (әсіресе useEffect, useReducer) мінезін логтаңыз немесе визуализациялаңыз
- қолданбаны ErrorBoundary-лермен ораңыз және қателерді Sentry-ге жіберіңіз
- пайдаланушы сессияларын көру үшін LogRocket немесе Replay.io қолданыңыз
- тест жазыңыз — олар әрі «баг ұстағыш», әрі «түсіндіргіш»
Заманауи React сізге «профи» сияқты дебагтауға қажет нәрсенің бәрін береді — енді оларды қолдану ғана қалды.
Бұл аударма автоматты түрде жасалды және қателіктер болуы мүмкін. Мазмұнды дәлірек түсіну үшін түпнұсқа мақаланы ағылшын тілінде қарауды ұсынамыз.
Let’s be honest — debugging React apps isn’t always straightforward. You build a shiny component, everything seems fine, and then… boom! The app gets sluggish, your console throws cryptic errors, or worse — something just silently breaks. And of course, it only happens when you show it to someone.
React is awesome. It’s declarative, powerful, and flexible. But with that power comes complexity — especially as your application grows. Components rerender for mysterious reasons. State behaves like it’s got a mind of its own. Hooks go rogue. Async code? It’s basically a jungle.
So, why does effective debugging matter so much in modern React development?
Because bugs are inevitable — but bad user experiences don’t have to be.
In modern apps, performance is a feature. Users expect snappy interactions, flawless navigation, and zero crashes. And if we don’t debug properly, we risk shipping something that works… until it doesn’t. That’s why getting good at debugging React isn’t just nice to have — it’s essential.
In this article, we’ll break down the most common issues developers face when working with React:
- unnecessary re-renders (hello laggy UI)
- memory leaks (your app eats RAM like candy)
- hook misbehavior (what even is this error?)
- async bugs (why does this data update twice?)
But don’t worry — we’re not just here to point out problems. We’ll walk through how React really works under the hood, and how to use tools like React DevTools, why-did-you-render, and logging services to track down issues before your users do.
Grab a coffee and let’s dive into the world of debugging. Your React apps are about to become cleaner, faster, and more reliable than ever.
Ah, re-renders. Sometimes they feel like React’s passive-aggressive way of telling you: “You did something… but I won’t say what.”
Let’s start by understanding how React re-renders work.
React uses something called the reconciliation algorithm to update the UI. It compares the previous virtual DOM with the new one, and only applies the minimal set of changes to the actual DOM. Sounds efficient, right? It is — until components start re-rendering when they really don’t need to.
Imagine a button re-rendering every time the parent updates, even though its props never change. Multiply that by 50 components, and your app starts to feel like it’s running on a potato.
So how do we avoid this?
If a component receives the same props and always renders the same output — wrap it in React.memo(). It’s like telling React: “Hey, don’t touch this unless something really changed.”
import React from 'react';
import { FC, memo } from 'react';
type ButtonProps = {
label: string;
}
export const Button: FC<ButtonProps> = memo(({label}) => {
console.log('Button rendered');
return <button>{label}</button>;
})
These hooks help avoid recalculating values or re-creating functions on every render.
- useMemo caches the result of expensive computations
- useCallback caches a function reference, so child components don’t re-render unnecessarily
import React from 'react';
import { FC, useMemo, useCallback, useState } from 'react';
export const ExpensiveComponent: FC = () => {
const [count, setCount] = useState(0);
const expensiveValue = useMemo(() => count * 2, [count]);
const handleClick = useCallback(() => setCount(prev => prev + 1), []);
return (
<div>
<p>{expensiveValue}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
But here’s the deal: don’t overuse them. They’re not magic performance boosts — they have their own costs, like added memory usage and the overhead of maintaining the cache. Use them when you actually face performance issues or see repeated re-renders in profiling tools.
One of the best dev tools for spotting unnecessary re-renders. This library tells you exactly which components re-rendered and why — and often it’s eye-opening.
import React from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React);
}
Add it in development mode, and it’ll log detailed info in the console, like:
[why-did-you-render] <MyComponent> re-rendered because props.changed
Super useful when your app is mysteriously slow and you can’t figure out why.
If you’re not using React DevTools yet, you’re basically debugging React with your eyes closed. This tool is your window into the component world — like X-ray vision for your app.
Let’s break it down.
The components tab shows the full component tree. Click around and you’ll see props and state in real-time. You can even edit values on the fly — it’s like playing God with your app.
Want to know why a component behaves a certain way? Just look at the props/state it currently has. That’s your first clue.
ℹ️Pro tip: you can right-click a component and select “Store as global variable.” This gives you direct access to it from the browser console as $r.
$r.setState({open: true})
Boom — instant state change for testing.
This is gold when debugging re-renders. In the DevTools settings (gear icon), turn on:
Highlight updates when components render.
Now every time a component re-renders, it flashes briefly. If your footer is blinking when you type in a search bar — that’s not okay. Time to investigate.
Switch to the Profiler tab, hit record, and interact with your app. You’ll get a flame graph showing which components were rendered, how long they took, and why they were rendered.
You’ll see things like:
- props changed
- context changed
- hook value changed
This is your best friend for finding bottlenecks and wasted renders.
ℹ️Bonus tip: profiler even suggests you wrap some components in React.memo() when it sees repetitive renders.
Hooks brought power and elegance to React — but also a new class of bugs. Ever had a hook fire twice when you expected it once? Or a useEffect do something weird on unmount? Yeah, we’ve all been there.
Let’s unpack the most common hook headaches and how to debug them like a pro.
If you’ve built a custom hook and have no idea what it’s doing internally — try useDebugValue.
import React from 'react';
import { useDebugValue, useState, useEffect } from 'react';
export const useAuthStatus = (): boolean => {
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => setLoggedIn(true), 1000);
return () => clearTimeOut(timeout);
}, []);
useDebugValue(loggedIn ? 'Logged In' : 'Logged Out');
return loggedIn;
}
Now in React DevTools, next to the hook name, you’ll see the label “Logged In” or “Logged Out”. It’s a clean way to surface hook internals without dumping them into the console.
It won’t show up in production, so it’s a safe dev-only tool.
Let’s face it: useEffect is powerful but easy to mess up. Here’s what you need to watch out for:
- missing dependency array – causes effects to run more than expected.
- stale closures – when you're using old state or props inside an async callback.
- race conditions – especially in fetches or timeouts.
Want to cancel a fetch if the component unmounts? Use AbortController:
import { useEffect, useState } from 'react';
const useUserData = (): unknown => {
const [data, setData] = useState<unknown>(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async (): Promise<void> => {
try {
const res = await fetch('/api/user', { signal: controller.signal });
const result = await res.json();
setData(result);
} catch (error: unknown) {
if ((error as Error).name !== 'AbortError') {
console.error(error);
}
}
}
fetchData();
return () => controller.abort();
}, []);
return data;
};
This avoids a very common bug: trying to update state on an unmounted component.
Debugging is way easier when your hooks are modular and traceable. Instead of writing one massive useEffect, split it:
useEffect(() => {
//subscribe to service
}, []);
useEffect(() => {
//update on prop change
}, [prop]);
useEffect(() => {
//cleanup or side-effect
}, [state]);
This makes it easier to track what does what, especially during hot reloads or rapid updates.
React is pretty forgiving… until it’s not. One broken component can take down your entire UI. That’s where error boundaries come in — your app’s safety net when things go sideways.
Think of error boundaries as React’s version of try...catch — but for components. They catch render-time and lifecycle errors, so your app doesn’t crash entirely when one part fails.
Here’s the basic setup:
import React from 'react';
import { Component, ErrorInfo, PropsWithChildren } from 'react';
type ErrorBoundaryState = {
hasError: boolean;
};
class ErrorBoundary extends Component<PropsWithChildren, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false };
static getDerivedStateFromError(): ErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}
export default ErrorBoundary;
Then wrap vulnerable parts of your app like this:
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
Boom — if UserProfile crashes, users won’t see a blank screen. Instead, they’ll get a friendly message.
Not everywhere. Wrapping your entire app is a good fallback, but the real power comes from using scoped boundaries:
- around remote data components (API failure? Show fallback).
- around feature experiments (A/B test gone wrong? Don’t break the site).
- around custom rendering logic (complex charts, dynamic UIs, etc).
Capturing the error is only half the job — now let’s send it somewhere useful.
In your componentDidCatch, or inside getDerivedStateFromError, forward the error to logging tools like Sentry, LogRocket, or Replay.io:
componentDidCatch(error, info) {
logErrorToService(error, info);
}
Catching errors locally is good. But knowing what actually happened in production? That’s next-level debugging. Integrating logging tools gives you visibility into the bugs your users are seeing — and the context around them.
Sentry is like a crash reporter with superpowers. It tracks exceptions, shows stack traces, and connects them to the actual lines of code that broke.
How to add Sentry in a React app:
npm install @sentry/react @sentry/tracing
Then configure it once at the root of your app:
import as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";
Sentry.init({
dsn: "https://your-dsn-url",
integrations: [new BrowserTracing()],
tracesSampleRate: 1.0,
});
Want to wrap an error boundary with Sentry’s own handler?
const SentryBoundary = Sentry.withErrorBoundary(MyComponent, {
fallback: <p>Oops, something went wrong.</p>,
});
Sometimes logs aren’t enough. You want to see what the user did — clicks, scrolls, form entries — all leading up to the crash.
That’s where tools like LogRocket and Replay.io shine. They record sessions as if you’re watching over the user’s shoulder — ethically and securely.
Example LogRocket integration:
npm install logrocket
Then configure it in your app:
import LogRocket from 'logrocket';
LogRocket.init('your-app-id');
You can also link LogRocket to Sentry to watch the session where the error occurred.
Debugging isn’t just about solving errors — it’s about understanding context. Tools like Sentry and LogRocket give you that full picture.
- don’t log sensitive data: strip emails, tokens, passwords from logs.
- throttle logs in production: avoid log floods that can hide real issues.
- use environment filters: don’t record in development or staging unless necessary.
- tag and group errors: use metadata (like user ID, app version) for faster triage.
Asynchronous code is tricky. You click a button, wait for a response… and boom — something goes wrong. Maybe nothing will happen. Or maybe two things happen at once. Or maybe something was canceled halfway through.
Let’s break down how to tame the async chaos in React.
First things first — use the browser’s network tab to track API calls. It shows:
- if the request went out
- what the response was
- if it failed and why
In code, wrap fetch/axios in try/catch to surface errors cleanly:
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch("/api/data");
const json = await res.json();
setData(json);
} catch (error) {
console.error("API error:", error);
}
};
fetchData();
}, []);
Let’s say a user types quickly in a search input, triggering multiple fetches. Only the last one matters — the rest should be canceled.
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(`/api/search?q=${query}`, { signal })
.then (res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== "AbortError") {
console.error("Search error:", err);
}
});
return () => controller.abort();
});
This avoids outdated requests from overriding newer data — a super common bug.
Tools like mock service worker (MSW) help simulate API behavior during development and testing. This lets you test how your app reacts to loading, errors, and delays.
Use jest with @testing-library/react to assert behavior:
test("displays loading then data", async () => {
render(<MyComponent />);
expect (screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText(/data loaded/i)).toBeInTheDocument();
});
Async bugs don’t stand a chance when you can simulate real-world edge cases.
We all love hooks like useState, useReducer, and useContext. But debugging them can feel like solving a mystery without clues.
Let’s fix that.
The best first step is to log state changes:
useEffect(() => {
console.log("Current state: ", myState);
}, [myState]);
For useReducer, add logging inside the reducer itself:
function reducer(state, action) {
console.log("Dispatching:", action);
console.log("Previous state:", state);
const newState = { ...state, ...action.payload };
console.log("New state:", newState);
return newState;
}
That’s a quick and dirty way to trace what’s changing — and when.
In React DevTools, select a component that consumes a context — you’ll see a special “Context” section with current values.
This is huge when debugging deeply nested providers or mismatched consumers.
Also, you can make context more debug-friendly by giving it a name:
export const ThemeContext = React.createContect('light');
ThemeContext.displayName = 'ThemeContext';
No more “Context.Provider” mystery blobs in DevTools.
- group related values in one object (useState({ name, age }))
- use useReducer when logic is complex
- don’t over-nest — deep objects are hard to inspect and update
- normalize large datasets (like Redux-style) for easier updates
Tests aren’t just about preventing regressions — they’re fantastic for debugging in disguise. When a test fails, it shows you exactly what broke, where, and why.
- unit tests catch logic bugs in hooks, reducers, and components.
- end-to-end (E2E) tests (e.g. with cypress or playwright) simulate real user behavior — clicking, typing, submitting.
When something fails in a test, it’s often a sign of a logic issue you missed manually.
Testing Library encourages tests that match what users do — so if something breaks there, it likely breaks in the real world too.
test("button submits form data", async () => {
render(<Form />);
userEvent.type(screen.getByLabelText("Name"), "John")
userEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/thank you/i)).toBeInTheDocument();
});
If this test fails, it tells you whether the button was found, whether the state updated, and whether the effect fired — it’s like a built-in debugger.
Debugging React apps can feel overwhelming — but with the right tools, mindset, and structure, you can turn bugs into learning moments instead of nightmares.
- use React DevTools to inspect components, state, and re-renders
- clean up async effects with AbortController
- log or visualize hook behavior (especially useEffect, useReducer)
- wrap app in ErrorBoundaries and send errors to Sentry
- use LogRocket or Replay.io to replay user sessions
- write tests — they double as bug catchers and explainers
Modern React gives you everything you need to debug like a pro — now you just need to use it.