In a React Native app, how does code outside components, such as a Headless JS task or notification handler, read or update the cart store?
answer
- Context needs a rendered tree
- an external store is a module-level object
- getState and dispatch, or setState
- Jotai: the store the Provider uses
- a cold-started task sees initial state
basics
~10 sContext is readable only by rendered components, so code outside the tree needs an importable external store: Redux's getState and dispatch, a Zustand store's getState and setState, or a Jotai store's get and set.
solid answer
~50 sReact Native runs a lot of code that is not a component: handlers registered in `index.js`, a Headless JS task registered with `AppRegistry.registerHeadlessTask`, connectivity or deep-link listeners, and API client interceptors. None of them can call a hook or read Context, because Context only exists inside a rendered React tree. An external store is a plain JavaScript object in a module, so any code can import it: `store.getState()` and `store.dispatch()` in Redux Toolkit, `useCart.getState()` and `useCart.setState()` on a Zustand hook, and `get` and `set` on a Jotai store, the default one or the one passed to its `Provider`. Components subscribed to that store re-render when outside code changes it. The React Native trap: when Android starts a Headless JS task while the app process is dead, the JavaScript starts fresh and the store holds its initial state, so a persisted cart must be rehydrated before the task trusts it.
code
typescript · 10 lines// index.ts - wiring outside any component
import { AppRegistry } from 'react-native';
import { useCart } from './cartStore'; // a Zustand store created at module scope
AppRegistry.registerHeadlessTask('CartSync', () => async (data: { sku?: string }) => {
// Fresh process? The cart is at its initial state until the persisted copy is restored.
if (data.sku) {
useCart.getState().add(data.sku); // the same action a button calls
}
});go deeper
Recall that Context and hooks work only inside rendered components, while an external store can be imported anywhere.
Name each library's out-of-component read and write, and explain why the Jotai store must be the one the Provider uses.
Reason about which JavaScript runtime outside code runs in, and handle a cold-started Headless JS task with an unhydrated store.
Treat out-of-tree access as a requirement when choosing a store, and define where app-level handlers live and which actions they may call.
## Code in a React Native app that is not a component A React Native app runs JavaScript in places where no component is rendering: - handlers registered at module scope in `index.js`, before or beside `AppRegistry.registerComponent`; - **Headless JS** tasks on Android, registered with `AppRegistry.registerHeadlessTask`, which the React Native docs describe as a way to run JavaScript in the background, for example to sync data or handle push notifications, without touching UI; - listeners for connectivity changes, incoming links or app state that live in plain modules; - an API client module that attaches the cart id to requests. These need the cart too: a notification action "add to cart" should update it, a background sync should read it. ## Why Context cannot serve them A Context value lives in the **rendered React tree**. Reading it needs a component, rendered under the provider, calling `use` or `useContext`. A module-level handler is not a component, and a Headless JS task has no UI at all, so there may be no tree to read from. Workarounds such as copying the Context value into a module variable from an effect produce a second, stale copy of the state. ## Reading and writing an external store External stores are ordinary objects created in a module, which is why outside code can import them. The access points differ by library: | Library | Read outside React | Write outside React | |---|---|---| | Redux Toolkit | `store.getState()` | `store.dispatch(action)` | | Zustand | `useCart.getState()` on the hook `create` returns | `useCart.setState(...)` or an action read from `getState()` | | Jotai | `store.get(atom)` | `store.set(atom, value)` | For Jotai, `store` is the one components actually use: the default store from `getDefaultStore()` when there is no `Provider`, or the store created with `createStore()` and passed to the `Provider`. Reading the default store while components use another gives a different, empty state. Writes from outside are ordinary store updates: subscribed components re-render with the same selector rules as any other change, and calling an existing action keeps business rules in one place instead of duplicating them in a handler. ## The React Native trap: which JavaScript runtime you are in A store module holds state **for the lifetime of one JavaScript runtime**. That matters on a phone in a way it does not in a browser tab: 1. **App in the foreground or backgrounded but alive.** A Headless JS task or handler runs in the same runtime as the UI, imports the same module and sees the live cart. 2. **Process killed, then woken for a task.** Android can start a Headless JS task while no UI exists. The JavaScript bundle is loaded fresh, every module is initialised again, and the store holds its **initial state**. A cart persisted to storage is not in memory until rehydration completes. 3. **Consequence.** A task that reads the cart must first wait for, or trigger, rehydration of the persisted store, and a task that writes must make sure that write is persisted before it resolves, since React Native pauses once the task's promise settles. ## Practical rules - Keep one store instance per app, created in a module, and import it from components and outside code alike. - Expose **actions** for outside callers rather than raw setters, so a push handler and a button run the same logic. - Do not start a store per Headless JS task; share the module so a live app and a task agree. - Keep fast-changing server data out of this store; a server-state cache has its own out-of-component API.
- A Jotai app reads getDefaultStore().get(cartAtom) in a notification handler and always gets an empty cart. Why?The app wraps its tree in a `Provider` with its own store, so components read and write that store, not the default one. The handler is reading a different store that nobody updates. Create the store with `createStore()` in a module, pass it to the `Provider`, and import the same object in the handler.
- Why must a Headless JS task finish persisting its cart change before it resolves?React Native treats a resolved task promise as the end of the work and goes into a paused mode unless other tasks or a foreground app are running, and the process may then be killed. A write still in flight, or held only in memory, can be lost. Await the write that persists the change before returning.
saying these in an interview costs you the question
- Any module can read React Context by importing the Context object
- A Headless JS task always sees the same in-memory cart as the UI
- Jotai's default store is always the one the components are using
- Outside code should set raw state rather than call the store's actions
- Copying Context into a module variable from an effect keeps it in sync