React Native component lifecycle: Complete guide

Most React Native lifecycle bugs ship from screens that passed QA: subscriptions that never clean up, timers that fire after a screen unmounts, AppState listeners registered twice because a focus event re-ran setup. They surface weeks later as production memory leaks, and every one of them traces back to the component lifecycle.

For senior engineers, lifecycle isn't about memorizing method order; it's about understanding the contract between React's reconciler, the JavaScript thread, and the native layer, and knowing exactly where that contract gets violated. This guide covers that contract end to end: class lifecycle methods and their Hooks equivalents, the React Native-specific layers (AppState, screen focus), and the cleanup patterns that stop the leaks above before they reach production.

TL;DR, lifecycle in 90 seconds

React Native component lifecycle has three phases: mounting, updating, and unmounting, and failing to handle the third one correctly is the top source of memory leaks in production apps. Patterns for custom React component architecture in high-traffic apps show that disciplined lifecycle management is one of the clearest differentiators between codebases that scale and those that degrade.

After reviewing 30+ React Native codebases between 2023 and 2025, our engineering team found subscription teardown bugs and setState-on-unmounted-component warnings in the majority of them. The fix is almost always the same: return a cleanup function from useEffect, the functional equivalent of componentWillUnmount. In class components, componentDidMount is where subscriptions and timers are created; componentWillUnmount is where they must be destroyed.

In functional components, a single useEffect with a correctly structured dependency array handles both, the callback runs on mount (and on listed dependency changes), and its return function executes on unmount. Get those three things right and you have solved roughly 80% of React Native lifecycle bugs before they reach production.

Why lifecycle matters: Memory leaks, stale state, and cleanup

Skipping componentWillUnmount cleanup is the single most reliable way to ship a memory leak into a React Native production app. Subscriptions stay alive after a component unmounts, setState gets called on a dead component tree, and native event listeners accumulate with every re-render.

The warning `Warning: Can't perform a React state update on an unmounted component` is the visible symptom. The invisible one is the heap growing by a few hundred kilobytes per navigation cycle: imperceptible in development, catastrophic after 20 minutes of use on a low-memory Android device (Android Developers (Nutrient blog quoting isLowRamDevice behavior)). We saw this in practice with Sportano.pl: iOS and Android app launched using React Native.

In our audits, the most common pattern was a component that registered a BackHandler event subscription in componentDidMount or a useEffect without a cleanup function.

BackHandler is a native module-backed API, its listeners are not garbage-collected when the JavaScript component unmounts. The subscription keeps executing, calling setState on a component that no longer exists in the tree. Every navigation away and back multiplies the count of orphaned listeners.

The AppState API produces the same failure mode when developers initialize it without proper configuration. A component subscribes to `AppState.addEventListener('change', handler)` to pause video or cancel network requests when the app backgrounds. Without a matching `remove()` call in componentWillUnmount (or a useEffect return function), the handler fires on every app state transition for the lifetime of the process, not just when the component is mounted.

Understanding the full lifecycle diagram: mounting, updating, and unmounting phases, and how React Navigation focus and blur events interact with them, is what separates components that behave correctly from components that behave correctly in isolation.

The three lifecycle phases: Mounting, updating, unmounting

Every React Native component moves through three lifecycle phases in a fixed execution order: mounting (the component is inserted into the tree), updating (props or state change triggers a re-render), and unmounting (the component is removed). Understanding this order is the mental model that separates reliable cleanup from the subscription teardown bugs our engineering team finds in production audits.

MOUNTING
  constructor()
  static getDerivedStateFromProps(props, state)   ← runs here too
  render()
  [React commits to native UI]
  componentDidMount()

UPDATING  (props change, setState, or forceUpdate)
  static getDerivedStateFromProps(props, state)   ← runs on EVERY render
  shouldComponentUpdate(nextProps, nextState)
  render()
  getSnapshotBeforeUpdate(prevProps, prevState)
  [React commits diff to native UI]
  componentDidUpdate(prevProps, prevState, snapshot)

UNMOUNTING
  componentWillUnmount()

Three facts from this diagram deserve attention.

getDerivedStateFromProps runs on every render, mounting and every update, not only when props change. This surprises most developers who reach for it to sync external props into state. Per react.dev's lifecycle reference, the method is intentionally static (no this access) to prevent side effects, but its every-render execution means derived state calculations fire far more often than expected. Our team's view: getDerivedStateFromProps is almost always the wrong tool; componentDidUpdate comparing prevProps to this.props, or moving the derivation into the render method itself, is cleaner in The React team’s "You Probably Don’t Need Derived State" / "You Might Not Need Derived State" guidance states that in the majority of class component use cases where getDerivedStateFromProps or componentWillReceiveProps are used, simpler patterns (computing from props during render, componentDidUpdate, memoization, controlled components, or key-based resets) can replace them, implying that more than 50% of such use cases do not truly need getDerivedStateFromProps (react.dev (Component reference, derived state guidance, plus linked blog "You Probably Don’t Need Derived State"), 2023).

componentDidMount is where the native UI tree is ready. Network requests, native module subscriptions, and BackHandler listeners belong here, not in the constructor, where the native view does not yet exist.

componentWillUnmount is the only safe place to cancel those subscriptions. Every resource claimed in componentDidMount should have a matching teardown here. Skipping it is exactly what produces the stale-state warning described in the previous section.

Core lifecycle method signatures and triggers

Each lifecycle method fires at a specific point in the component tree reconciliation, and getting the signature wrong, especially on componentDidUpdate, is where most class component bugs originate.

`componentDidMount()` executes once, immediately after the component is mounted into the native view hierarchy. This is the correct place for subscriptions, network requests, and native module setup. Its functional equivalent is `useEffect(() => { / setup / }, [])`, an empty dependency array tells React to run the effect after the first render only.

componentDidMount() {
  this.subscription = AppState.addEventListener('change', this.handleAppStateChange);
}

`componentDidUpdate(prevProps, prevState, snapshot)` runs after every re-render except the first. The snapshot parameter receives whatever getSnapshotBeforeUpdate returned, usually null unless you're preserving scroll position. Always guard against infinite loops by comparing prevProps or prevState before calling setState.

componentDidUpdate(prevProps, prevState, snapshot) {
  if (prevProps.userId !== this.props.userId) {
    this.fetchUserData(this.props.userId);
  }
}

`componentWillUnmount()` is the teardown hook: cancel subscriptions, clear timers, and remove native module listeners here. Missing this method is the source of the setState on unmounted component warning our engineering team encounters most frequently in React Native audits: a network callback fires after navigation pops the screen, and the component is already gone.

componentWillUnmount() {
  this.subscription?.remove();
}

`getDerivedStateFromProps(props, state)` is a static method, it has no access to this, that returns a partial state object or null. The React team's own documentation at react.dev describes it as intended for rare cases where state depends on changes in props over time. In practice, it is almost always the wrong tool: it runs before every render, including updates triggered by setState, which means derived logic silently re-executes even when props haven't changed. Prefer memoization or computing derived values inline during render.

static getDerivedStateFromProps(props, state) {
  if (props.counter !== state.lastCounter) {
    return { displayCount: props.counter, lastCounter: props.counter };
  }
  return null;
}

A component that extends React.Component has access to all four methods above. PureComponent shallow comparison skips componentDidUpdate when neither props nor state count references have changed, useful for reducing render count, covered in the performance section below.

Advanced methods: shouldComponentUpdate, getSnapshotBeforeUpdate, and when getDerivedStateFromProps is the wrong tool

GetDerivedStateFromProps is almost always the wrong tool. The method exists for one narrow case: a component whose internal state must be a pure function of incoming props, with no async operations, no side effects, and no memory of previous prop values. In practice, that use case is so rarely invoked that it's vanishingly uncommon.

The typical mistake we see in code review is engineers reaching for getDerivedStateFromProps to reset a counter or filter list when a prop changes. The safer pattern in React is lifting state up or using a key prop to remount the component, both approaches are more explicit and easier to test. React's own documentation at react.dev/learn/you-might-not-need-an-effect covers the full decision tree, and the conclusion is consistent: if you're deriving state from props, first ask whether the parent should own that state instead.

`shouldComponentUpdate(nextProps, nextState)` has a sharper, more defensible use case: preventing expensive re-renders in list cells or chart components where props change reference but not value. The risk is a stale-closure equivalent, returning false when you should return true. PureComponent shallow comparison handles the common case automatically and is harder to get wrong.

`getSnapshotBeforeUpdate(prevProps, prevState)` is the rarest method in production React Native code. It fires synchronously before the native view commits, letting you capture scroll position or layout measurements. The return value becomes the third argument, snapshot, in `componentDidUpdate(prevProps, prevState, snapshot)`. Outside of animated scroll restoration, we rarely see a justified use for it.

Error boundary lifecycle: Catching crashes without crashing

An error boundary component catches JavaScript errors anywhere in its child component tree, preventing a single unhandled exception from unmounting your entire React Native view hierarchy. Two lifecycle methods do this work: getDerivedStateFromError and componentDidCatch.

`getDerivedStateFromError(error)` is a static method that runs during the render phase. Its only job is to return state updates that trigger a fallback UI: no side effects, no async calls. `componentDidCatch(error, info)` runs in the commit phase and is the right place to forward the error to your crash reporting module (Crashlytics, Sentry, etc.).

A minimal, production-ready implementation:

import React from 'react';

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // Forward to native crash reporting module
    crashReporter.log(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? null;
    }
    return this.props.children;
  }
}

Two constraints matter in React Native specifically. First, error boundaries do not catch errors thrown inside useEffect, those escape to the JavaScript thread's unhandled rejection handler. Second, they do not catch errors in async callbacks or native module calls, only synchronous errors that surface during render or lifecycle methods.

Wrap each major screen rather than the entire app. Per-screen boundaries let unaffected views stay mounted and re-rendered normally while one screen recovers, which reduces user-facing disruption without requiring a full app restart.

Hooks-based lifecycle: Class methods mapped to useEffect and friends

Functional components map every class lifecycle method to useEffect and its variants: once you see the correspondence, the mental model clicks into place. The three most common mappings cover mount, update, and unmount, and each has a direct Hook equivalent.

Class method → hook equivalents

Class method Hook equivalent Key difference
componentDidMount `useEffect(() => {}, [])` Empty dependency array means executed once after mount
`componentDidUpdate(prevProps, prevState)` `useEffect(() => {}, [dep1, dep2])` Runs after each render where listed deps changed
componentWillUnmount Cleanup function returned from useEffect Return a function; React calls it before the component is unmounted
componentDidMount + componentWillUnmount Single useEffect with both setup + cleanup Co-location is the win, teardown logic lives next to setup

Side-by-side: Subscription setup and teardown

Class component

class LocationTracker extends React.Component {
  componentDidMount() {
    this.subscription = LocationModule.watch(this.handleUpdate);
  }
  componentWillUnmount() {
    this.subscription.remove();
  }
}

Functional component

function LocationTracker() {
  useEffect(() => {
    const subscription = LocationModule.watch(handleUpdate);
    return () => subscription.remove(); // cleanup on unmount
  }, []); // empty useEffect dependency array = componentDidMount semantics
}

The cleanup function is componentWillUnmount, same contract, co-located. In our engineering team's 2024 audits of React Native apps, the leading source of native module memory leaks was a class component whose componentWillUnmount was removed during a refactor but whose componentDidMount subscription was not. Co-location makes that mistake structurally harder.

useLayoutEffect: When to reach for it, and when not to

UseLayoutEffect fires synchronously after DOM mutations but before the browser paints, the equivalent of componentDidMount/componentDidUpdate in synchronous mode. Use it only when you need to measure a rendered node or prevent a visual flash (e.g., repositioning a View before it appears). In React Native, blocking the JS thread here causes jank on the main thread; our team treats any useLayoutEffect that takes more than ~2ms as a regression. For data fetching, subscriptions, and logging, useEffect is always the right call.

React navigation: useFocusEffect is not componentDidMount

UseFocusEffect from React Navigation fires each time a screen receives focus, on mount and on every return from a child screen. Treating it as componentDidMount means re-fetching data on every back-navigation, which burns unnecessary network calls and resets scroll position. Per the React Navigation documentation, wrap the callback in React.useCallback and include its deps to avoid stale closures.

useFocusEffect(
  React.useCallback(() => {
    fetchData(); // runs on focus, not just mount
    return () => cancelFetch(); // runs on blur
  }, [userId]) // dep controls when the callback re-binds
);

According to Dan Abramov's 'A Complete Guide to useEffect', every value used inside useEffect that participates in the React data flow belongs in the dependency array, omitting deps doesn't suppress re-runs, it just creates stale closures that are harder to debug than the re-runs would have been.

Class vs. Functional component lifecycle: Comparison table and decision guide

Functional components with the useEffect dependency array are the right default for all new React Native code. Class components remain valid when you need `componentDidUpdate(prevProps, prevState, snapshot)` with a snapshot, or when an existing codebase's error boundary component requires a class, getDerivedStateFromProps and componentDidCatch still have no Hook equivalents.

Dimension Class component Functional component
Boilerplate extends React.Component, constructor, bind in constructor function or arrow, no ceremony
Render optimization PureComponent shallow comparison via shouldComponentUpdate React.memo + stable useEffect dependency array
Side-effect model Spread across componentDidMount, componentDidUpdate, componentWillUnmount Co-located in one useEffect block per concern
Concurrent mode Partially unsafe, render-phase methods can be called multiple times Fully compatible, no legacy lifecycle contract
Testing Instance methods require `wrapper.instance()` in Enzyme; awkward with RTL Hooks test cleanly with React Testing Library
Error boundaries Required, no Hook equivalent exists yet Not possible without a class wrapper

The PureComponent shallow comparison fires on every props change and re-renders only when a value differs, which is effective but limited: nested objects always pass the shallow check. React.memo offers the same guarantee with a custom comparator as the second argument, giving functional components more control.

In practice, our engineering team migrates class components to Hooks during feature work rather than in dedicated refactor sprints, keeping the error boundary component as a thin class wrapper at the tree boundary while everything mounted below it is functional.

React Native-specific lifecycle: AppState, BackHandler, and navigation events

React Native's component lifecycle extends beyond the JavaScript layer into native OS events that web-focused React guides ignore entirely. Three APIs: AppState, BackHandler, and React Navigation's focus/blur events, each require careful subscription teardown, and getting that teardown wrong is the most common source of memory leaks we find during React Native audits.

AppState: Foreground and background transitions

The AppState API fires change events when the app moves between active, background, and (on Android) inactive states. Subscribe inside useEffect and return the removal function:

useEffect(() => {
  const subscription = AppState.addEventListener('change', handleAppStateChange);
  return () => subscription.remove();
}, []);

The critical detail: AppState.addEventListener returns a subscription object in React Native 0.65+ (Stack Overflow / React Native Docs). Code written before that release called AppState.removeEventListener, which was deprecated and later removed. We still encounter this pattern in audits of apps that have grown incrementally since 2022: the subscription never cleans up, the handler fires on unmounted components, and the setState-on-unmounted-component warning follows.

BackHandler: Android hardware back button

The BackHandler event subscription works on the same principle. Register a handler, return true to consume the event, and remove the subscription in the useEffect cleanup:

useEffect(() => {
  const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
  return () => sub.remove();
}, [onBackPress]);

Missing onBackPress from the useEffect dependency array is a subtle bug: the handler closes over a stale props value, so the component's back-navigation logic never reflects updated state. Per react.dev's hooks reference, every value read inside an effect that can change between renders belongs in the dependency array.

React navigation focus and blur events

React Navigation's focus and blur events solve a different problem: a screen component mounted in a tab navigator stays mounted in the background, so useEffect with an empty dependency array runs once at initial mount, not on each tab visit. useFocusEffect re-executes the callback each time the screen receives focus:

import { useFocusEffect } from '@react-navigation/native';

useFocusEffect(
  React.useCallback(() => {
    startPolling();
    return () => stopPolling();
  }, [])
);

The tradeoff: useFocusEffect triggers a re-render cycle on every focus event. For screens with expensive render trees, prefer subscribing to `navigation.addListener('focus', ...)` inside a useEffect: that approach wires the listener once, avoids repeated execution overhead, and is documented in the React Navigation event system reference. Case in point: Shine hit evolved from a simple messaging bot to a comprehensive well-being platform with iOS and Android apps. Netguru helped prioritize features for Black Friday deadlines and upgraded the React Native app's core to improve performance with Netguru.

The practical rule: use useFocusEffect for data-fetching that must reset on each visit; use navigation.addListener for persistent subscriptions that should only register once.

Performance optimization: PureComponent, react.memo, and render reduction

PureComponent shallow comparison and React.memo cut re-render counts significantly, but both fail silently when prop shapes are complex, and that silent failure is worse than skipping the optimization entirely.

In class components, PureComponent replaces the default shouldComponentUpdate with a shallow equality check across all props and state keys. That works cleanly for primitives and stable object references, but breaks whenever a parent passes an inline object literal (`style=`) or an arrow function: both create new references on every render, so the shallow check always returns false and the memoization never fires (DEV Community (React.memo: Optimizing Performance in React Applications)). Our engineering team found this pattern in a significant portion of audited screens during 2023-2025 engagements; the component looked optimized, yet re-rendered on every parent update.

React.memo is the functional equivalent: it wraps a component and shallowly compares the previous and next props before deciding whether to re-render. The same reference-equality trap applies. Pass a custom comparator as the second argument when props contain derived objects:

const ListRow = React.memo(MyRow, (prev, next) =>
  prev.itemId === next.itemId && prev.selected === next.selected
);

The useEffect dependency array has the same mental model: React uses Object.is comparison on each dependency, so an unstable object reference in the array defeats memoization just as it defeats PureComponent. Stabilize callbacks with useCallback and derived objects with useMemo before passing them as deps or props, otherwise both tools produce overhead without benefit.

Where this breaks down in React Native specifically: list rows inside a FlatList are the highest-value target for React.memo. A 60-item list that re-renders every row on a single state change in the parent View is a common source of frame drops we trace with the Hermes profiler.

Frequently asked questions about React Native component lifecycle

What is the exact execution order of lifecycle methods in React Native?

React Native component lifecycle follows this order: constructor → getDerivedStateFromProps → render → componentDidMount, then on updates: getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate, and finally componentWillUnmount on teardown. Functional components mirror this via useEffect hooks, which execute after the component is mounted and after every re-rendered cycle by default. Understanding this order is essential when debugging state timing bugs or sequencing async calls.

How is useEffect different from componentDidMount in React Native?

UseEffect with an empty dependency array runs after every mount, similar to componentDidMount, but executes asynchronously after the browser paint, not synchronously before it. The key difference: a single useEffect with a cleanup function replaces both componentDidMount and componentWillUnmount combined. Per Dan Abramov's *A Complete Guide to useEffect*, thinking of useEffect as a lifecycle replacement leads to bugs, model it as effect synchronization instead.

How do I replicate componentWillUnmount cleanup in a functional component?

Return a cleanup function from useEffect to replicate componentWillUnmount behavior, React calls that returned function before the component unmounts and before re-running the effect on the next render. For example: `useEffect(() => { const sub = props.eventEmitter.addListener(…); return () => sub.remove(); }, [])`. Skipping this return is the single most common source of subscription teardown bugs our engineering team finds in React Native app audits.

When should I use useLayoutEffect instead of useEffect in React Native?

UseLayoutEffect fires synchronously after React updates the native view tree but before the UI is visible, making it the right choice when you need to measure a component or apply a layout-dependent state change without a visible flicker. Using useLayoutEffect unnecessarily blocks the UI thread on the JavaScript side, which causes jank on low-end Android devices. Reserve it for layout measurements, onLayout callbacks, scroll position resets, and default to useEffect everywhere else.

Should I use shouldComponentUpdate or react.memo in a new React Native project?

Use React.memo in any new React Native project, shouldComponentUpdate only applies to class components, which are effectively legacy in projects targeting React 18+ (React documentation – Describing the UI). React.memo wraps a function component and applies a shallow props comparison by default, or accepts a custom comparator as a second argument for complex prop shapes. The exception: if you maintain a class component that extends PureComponent, shouldComponentUpdate remains valid until that component is migrated.

How does react navigation's useFocusEffect interact with the component lifecycle?

UseFocusEffect runs its callback when a screen gains focus and calls the returned cleanup when the screen loses focus, it does not map to mount/unmount because React Navigation keeps screens mounted during navigation transitions. Per the React Navigation documentation, combine it with useCallback to prevent the effect from re-running on every render. React Navigation focus and blur events are the correct mechanism for refreshing data or pausing animations when screens move in and out of view, not useEffect with an empty array.

How do I handle AppState background and foreground transitions with hooks?

Use the AppState API with a useEffect subscription to detect when your React Native app moves between active, background, and inactive states. The canonical pattern is `AppState.addEventListener('change', handler)` inside a useEffect, with `subscription.remove()` returned as cleanup, omitting that cleanup causes the handler to fire multiple times after hot reload. This matters most for pausing audio playback, flushing analytics, or re-authenticating sessions when the app returns to the foreground.

Build more reliable React Native Apps

Proper componentWillUnmount teardown and a disciplined useEffect dependency array are the two React Native lifecycle practices that prevent the majority of memory leaks, stale-state warnings, and navigator-related bugs we encounter in production audits.

If this guide surfaces a gap in how your team handles component lifecycle, mounting assumptions, subscription cleanup, or the UNSAFE_ migration still on your backlog, these resources cover the next layer:

Our engineering team has helped scale-ups and mid-market product teams resolve lifecycle debt, reduce render count, and ship more reliable React Native apps. Working with UBS, Netguru redesigned payments features and login natively, improved navigation, introduced a new user-centric home screen providing financial insights, refined loading behavior and error handling, and established a native design system with a process for component library management. When building cross-platform products, sharing UI across React platforms is another architectural consideration that affects how component lifecycle patterns are structured and reused.

Talk to our team if you want a second opinion on your component architecture.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business