Course outline

React Native / Asynchronous and data ownership / Lesson 7

Effects, subscriptions, and resource ownership

Synchronize with native event sources through a symmetric setup/cleanup contract rather than performing work during render.

About 4 min readContent reviewed
On this page

Lesson goals

  • An Effect synchronizes an external relationship
  • Remove the subscription created by the same setup
  • Model listener lifetime without pretending it is native evidence

An Effect synchronizes an external relationship

An Effect is appropriate when a component must synchronize with something outside React, such as a native event subscription. A value derived from current props does not need an Effect just to be copied into another state variable. A deliberate user action belongs in its event handler rather than an Effect watching a flag that indirectly represents the action. Ask which resource is created, which inputs identify the relationship, and when it ends. This makes cleanup reviewable and avoids a render that registers another listener every time an unrelated label changes.

Remove the subscription created by the same setup

The first module reads an initial AppState value and subscribes to later changes. The returned subscription supplies remove, and cleanup removes exactly that subscription. A null initial value is represented as unknown instead of being mistaken for background or active. AppState events describe application visibility transitions; they do not confirm account permission or create new user intent. Native delivery and platform differences still require the selected host. The expected text is whichever accepted status the source reports, not a fixed event sequence promised across iOS and Android.

jsx Illustrative React Native module; requires the declared compatible application host.
import {useEffect, useState} from 'react';
import {AppState, Text} from 'react-native';
export default function StatusLabel() {
  const [status, setStatus] = useState(AppState.currentState ?? 'unknown');
  useEffect(() => {
    const subscription = AppState.addEventListener('change', setStatus);
    return () => subscription.remove();
  }, []);
  return <Text>App state: {status}</Text>;
}
Expected output
The label reflects the initial status and subsequent accepted AppState changes.

Cleanup targets the returned native subscription. No native event source or React Effect was executed in the authoring environment.

Model listener lifetime without pretending it is native evidence

The second program uses a tiny local event source to demonstrate registration and removal. After cleanup, the later event no longer reaches the observer and no listener remains. This is useful policy evidence under Node, but it bypasses AppState registration, React scheduling, and native delivery. A production subscription API can have different reentrancy and exception rules, so do not copy this small bus as a general replacement. Use it as a deterministic fake to verify which owner attaches and detaches, then test actual platform integration separately in an authorized compatible host.

javascript Standalone JavaScript model with synthetic local inputs.
function createSource() {
  const listeners = new Set();
  return {
    subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
    emit(value) { for (const listener of listeners) listener(value); },
    size() { return listeners.size; },
  };
}
const source = createSource();
const observed = [];
const remove = source.subscribe(value => observed.push(value));
source.emit('active');
remove();
source.emit('background');
console.log(JSON.stringify(observed));
console.log(source.size());
Expected output
["active"]
0

The fake demonstrates that ownership ends at remove. It establishes neither native event timing nor device lifecycle behavior.

Dependencies describe the relationship, not a desired schedule

An Effect’s dependencies include reactive values it reads that identify its synchronization. Omitting a changing identifier can leave a listener connected to an old record. Adding an unstable object created during every render can recreate the relationship unnecessarily. Solve that ownership problem rather than disabling dependency checks or assuming an empty array means the screen never changes. Development Strict Mode can probe setup and cleanup with an additional cycle. A correct symmetric subscription should survive that probe; a ref that suppresses setup while leaving cleanup behavior unclear can hide the actual defect.

Verify replacement, unmount, and obsolete callbacks

Test attaching once, receiving an event, changing the relationship identifier, and removing the owner. Verify the old source cannot update current state after replacement. Some APIs can already have a callback queued when unsubscribe occurs; where that is possible, add a generation or active-owner check at publication as well as detachment. Keep resource cancellation separate from result freshness. A fake listener count verifies local cleanup policy but does not certify every native event path. Report which source implementation was exercised and leave platform lifecycle claims to actual target evidence.

Common errors and fixes

One event causes several state updates

Why it happens
Repeated setup left older listeners attached.
How to fix it
Return cleanup that removes the subscription created by that setup.
Prevent it next time
Repeat setup/cleanup with a counted fake source.

A listener keeps using an old identifier

Why it happens
A reactive dependency was omitted from the synchronization contract.
How to fix it
Include the relevant dependency and replace the old relationship safely.
Prevent it next time
Change identifiers while events are pending.

Strict Mode is disabled to hide duplicate setup

Why it happens
The development probe exposed asymmetric resource ownership.
How to fix it
Repair setup and cleanup symmetry before changing diagnostics.
Prevent it next time
Treat repeated mount-like cycles as useful lifecycle tests.

Check your understanding

1. Should a subtotal computed from selected rows be copied into state by an Effect?

Show worked solution

Usually derive it directly from authoritative rows. An Effect copying the calculation creates another mutable value that can lag or diverge. Use a cache only when measured cost justifies it and all dependencies are represented; external synchronization is a different responsibility.

2. Why might unsubscribe need an additional freshness gate?

Show worked solution

An API can already have scheduled a callback before detachment. Removing future registration may not retract that callback. Capture relationship identity and reject publication if the owner changed, while still removing the resource to stop later work. The exact guarantee must come from the source API.

Key takeaways

  • An Effect synchronizes an external relationship
  • Verify replacement, unmount, and obsolete callbacks

Official references