React Native / Asynchronous and data ownership / Lesson 8
Async results, errors, and freshness
Reject obsolete completions on every publication path and distinguish cleanup from actual transport cancellation.
On this page
Lesson goals
- Completion order can differ from input order
- Guard success and failure from an Effect instance
- Model ownership independently of React scheduling
Completion order can differ from input order
A search for Maps can start before a search for Rivers and finish afterward. Neither success nor failure is automatically entitled to replace the current interface. Define the policy: live search usually publishes only the current input’s result, while a transactional save may need reconciliation rather than silent replacement. Await does not freeze the user’s navigation or account state. Preserve which query and session started the operation, then verify ownership at publication. A component remaining mounted does not prove an old response still belongs to its current input or current user.
Guard success and failure from an Effect instance
The first module gives each Effect setup an active publication right. Cleanup removes that right before replacement or unmount, so both later success and error are ignored. The repository and query are dependencies; their identities must be stable when the relationship is unchanged. Promise.resolve().then also converts a synchronous repository throw into the same failure path. This gate does not cancel underlying work or guarantee an HTTP request stopped. It is an illustrative owner policy requiring a compatible React Native host and an injected repository; no real endpoint is supplied or contacted.
import {useEffect, useState} from 'react';
import {Text} from 'react-native';
export default function SearchResult({query, repository}) {
const [state, setState] = useState({phase: 'loading'});
useEffect(() => {
let active = true;
setState({phase: 'loading'});
Promise.resolve().then(() => repository.search(query)).then(
items => { if (active) setState({phase: 'ready', items}); },
() => { if (active) setState({phase: 'failed'}); },
);
return () => { active = false; };
}, [query, repository]);
if (state.phase === 'loading') return <Text>Loading</Text>;
if (state.phase === 'failed') return <Text>Search failed</Text>;
return <Text>{state.items.join(', ') || 'No matches'}</Text>;
}
Only the currently active Effect may publish Loading, Search failed, or its accepted results.
The example assumes repository results are already validated string arrays. Cleanup revokes publication; it does not claim transport cancellation.
Model ownership independently of React scheduling
The second program represents request identity with a monotonically increasing ticket. B completes before A, and A’s later publication is rejected. Passing a result-shaped object through the same gate handles errors as well as success. This deterministic model tests the policy without relying on timers, React scheduling, or a network. A real session switch needs another identity dimension or an invalidation event so data from a previous account cannot publish into the next. Keep generation state under the owner of the operation rather than a mutable global shared across unrelated screens.
let generation = 0;
let visible = null;
function begin() { return ++generation; }
function publish(ticket, result) {
if (ticket === generation) visible = result;
}
const a = begin();
const b = begin();
publish(b, {phase: 'ready', title: 'Rivers'});
publish(a, {phase: 'failed'});
console.log(JSON.stringify(visible));
{"phase":"ready","title":"Rivers"}
The stale failure cannot cover the newer success. The model establishes a commit rule, not actual request cancellation or authorization.
Separate pending state, empty success, and failure
A successful empty result is different from a rejected request. Do not catch every failure and return an empty list, since that hides service problems as no matches. Decide whether refresh keeps previous useful data or replaces it with a loading screen, and identify which operation may clear pending state. An obsolete finally callback can incorrectly clear a newer operation’s busy flag. Timeouts limit waiting but may not stop underlying work. For uncertain writes, a missing response can follow server acceptance, so use the service’s operation identity and reconciliation policy instead of blind retries.
Control completion order in tests
Use a fake repository whose pending responses can be resolved in a chosen order. Start A and B, complete B, then A; repeat with A failing and with ownership ending before completion. Assert stale work cannot alter either result or current pending state. Add account change before a response when the model is session-scoped. Arbitrary sleeps can pass without exercising the dangerous ordering. Node-only models verify publication logic, while a configured renderer must separately verify Effect replacement and native UI behavior. No request, renderer, transport abort, or device execution is performed here.
Common errors and fixes
An old failure covers new results
- Why it happens
- Only the successful completion checked freshness.
- How to fix it
- Apply ownership checks to success, error, and pending-state cleanup.
- Prevent it next time
- Complete the stale request with both success and failure.
An Effect restarts on every parent render
- Why it happens
- An injected repository object changes identity unnecessarily.
- How to fix it
- Keep the intended dependency owner stable or replace it deliberately.
- Prevent it next time
- Rerender without changing query or repository semantics.
A timeout is reported as a server rollback
- Why it happens
- Local waiting policy was confused with remote transaction outcome.
- How to fix it
- Represent uncertain completion and reconcile under the service contract.
- Prevent it next time
- Test lost responses separately from explicit rejections.
Check your understanding
1. Why does the model reject A’s failure after B succeeds?
Show worked solution
Beginning B advances the current generation. A’s ticket no longer matches that owner, regardless of whether its completion is success or failure. The gate prevents obsolete work from changing the current result; it does not pretend A stopped executing.
2. Is component lifetime alone sufficient during an account switch?
Show worked solution
No. The same component or shared model may survive while account ownership changes. Capture session identity and invalidate old publication rights and private caches. Server authorization is another boundary and must still validate the current operation; a local generation check cannot replace it.
Key takeaways
- Completion order can differ from input order
- Control completion order in tests