Stable Values in React: What They Are and Why They Break Your Components

The term "stable value" is unique to the React ecosystem, and it has grown more important since functional components became the norm. A stable value — usually returned from a hook — is one that is guaranteed to be the same object across multiple renders. Instability, then, is the opposite: a new instance of the value is created on every render.

Consider some common examples. The state update function returned by useState or useReducer is constant; the hook returns the same function every time. The object returned by useRef is likewise guaranteed to remain the same, and changing ref.current does not trigger a re-render. But an array literal in your component body will produce a fresh array instance each render — harmless until you pass it somewhere that expects stability. The same issue applies to anonymous functions and object literals. It seems obvious in retrospect, but many developers discover the hard way that passing an anonymous function to an onPress prop caused a rendering issue. When you need a stable value, useCallback and useMemo are the tools to reach for.

A useful definition: stability is about what conditions cause a value to change. A value is stable if it does not change just because a render happened. A value is unstable if it is recreated on every render. What follows are a few places where this distinction causes real damage.

APIs That Dictate Stability

The first place stable values matter is in the dependency lists of useEffect, useCallback, and useMemo. The behavior of these hooks is tied to changes in the listed dependencies. If a dependency is unstable — changing each render — then the cached result from useCallback or useMemo is always discarded, and an effect will re-run on every render.

It's also worth framing these hooks the other way around: they generate stable values. Given a source of stable values, you can produce a new stable value with useMemo or useCallback, or trigger a reaction to specific changes via useEffect.

Not every API that expects a stable value makes that requirement explicit. A common surprise is useFocusEffect from React Navigation. It resembles useEffect but behaves differently. The React Navigation docs put it gently: to avoid running the effect too often, wrap the callback in useCallback before passing it to useFocusEffect.

Here's what is happening under the hood. Every time your component renders, a new anonymous function is created. useFocusEffect has no way to know if that new function closes over the same values as the previous incarnation, so it must assume the behavior has changed. The effect can't just re-run — it treats the change as a blur-then-focus sequence, invoking the cleanup function and then re-subscribing. On every render. The code still works correctly — it unsubscribes and resubscribes each time — but it is almost certainly not what you intended. If you want a subscription established once, wrapping that callback in useCallback makes the behavior match the intent.

One misconception worth correcting: useCallback does not save memory. The function you pass is created on every render just as if the hook were absent; it is simply ignored when dependencies don't change. This is slightly different from useMemo, which only invokes its factory function when a new value is actually needed. In both cases, however, a function may be created and discarded — which is a performance concern separate from the stability guarantee.

When Inline "Components" Cause a Remount

Another scenario that goes wrong is the use of an inline function that returns JSX, commonly found in code that conditionally renders a portion of a component. The pitfall is in how the result is rendered.

If you treat the helper as a plain function and invoke it directly in your JSX, the returned elements are inserted inline. React reconciles them normally. If you instead treat the function as a component and render it with <Component /> syntax, the behavior is subtly but catastrophically different.

In the second approach, React sees a component type — an anonymous function that is recreated on every render. It has no way to know that the new function is related to the function from the previous render. It therefore treats the change the same way it would treat swapping a different component type entirely: it destroys the entire subtree and mounts a fresh one. On every render, native views are torn down and recreated, which is expensive.

The correct pattern is to invoke details() directly, inserting the resulting JSX into the parent's return value. Another option is to wrap the logic in useMemo, though that does not provide the same rendering guarantees as memoizing a component — the reconciler may still detect and handle changes differently. The cleanest solution is to avoid the pattern altogether: if the logic looks like a component, make it a component. Define it at the module level. If optimization is later needed, React.memo can then be applied to a stable component reference.

The rule to remember: components should be defined in the component's module scope, not inside another component's body. A function defined inside a component body is a new instance on every render, and React cannot assume the new component bears any relation to the one from a previous render.

Caching, Not Memoization

Finally, stable values are essential when working with memoized components. If you push a callback to a component wrapped in React.memo, the memoization gives nothing unless the callback is stable across renders. But the takeaway isn't to wrap every event handler in useCallback. Balance that cost against the benefit.

A terminological aside: these APIs are often described as "memoizing" values. More precisely, they cache a value with a cache size of one, and invalidate that cache when any dependency changes. The practical guidance is the same either way.

Understanding what makes a value unstable — and where instability causes React to discard work — separates guesswork from deliberate optimization. Inline literals and functions change identity each render; hooks like useRef, useCallback, and useMemo grant stability on your terms; and the reconciler makes costly assumptions when component identity changes. Keep components in the module scope, and pass stable callbacks to memoized components.

Stable Values in the Wild

If you’re building a large React Native application, you’ll likely need to coordinate values across many components. useMemo and useCallback are the standard tools for this, but they have a few shortcomings that become apparent at scale.

The first issue is that useMemo only guarantees referential stability if its dependency array doesn’t change. Even an empty dependency array only ensures a stable value until the component remounts. This can cause subtle bugs when components that rely on a memoized value are re-rendered in a new parent context. Similarly, useCallback can invalidate a stable function reference if its dependencies change, forcing child components to re-render even when the function’s behavior is effectively unchanged.

To address these gaps, Shopify’s Point of Sale app relies on module-level constants. These values are created once when the module loads and remain genuinely stable for the entire lifetime of the application. For complex data structures, a module-level function (often a factory) handles creation. This approach sidesteps the reconciliation phase entirely — a module-level value is stable by definition, not by the grace of React’s dependency tracking.

Stable Modules vs. React Hooks

This module-level pattern isn't a replacement for the broader state management that hooks provide. State updates still flow through the normal React mechanisms. Instead, this is a pattern for those "functionally constant" values that describe a screen's configuration—things like layout options or closed-over actions that don't depend on changing state.

In practice, this leads to code that imports a pre-built object from a module rather than creating it inside a component. For instance, a component might use a module-level TextStyle constant in an Animated.Text to cache its configuration. The style object is created once, used on every render, and never causes a re-render by itself.

This pattern shines when you need a stable identity across different component instances. While a hook-based solution like useState preserves a value between renders of the same component, it does not share that value with other instances. Module-level constants are global singletons, which makes them ideal for coordinating layout logic or pre-computed style dependent on screen dimensions across distinct screens.

Choosing the Right Tool

Given the tools above, a common question is when to use which. The Shopify team's working rules are clear:

  • If a value depends on component state and must persist across re-renders, use useState.
  • If a value needs to be stable but doesn't drive state, use a module-level constant. If its creation is computationally heavy, wrap it in a factory function called once at module scope.
  • If a value must be driven by props and recalculated when those props change, useMemo is appropriate—but be aware that it isn't a guarantee of stability.
  • For callbacks that need to be stable but don't use state or props, prefer a function declared at module scope. useCallback should be reserved for cases where the closure needs specific props or state, and even then, a custom hook like useStableCallback can offer better guarantees if you manage the primary ref yourself.

There's a trade-off: module globals can make isolated testing harder and can lead to memory leaks if you store large data structures that should be garbage collected lazily. But for the stability problem—avoiding churn in object identity—direct module scope is the most straightforward and honest solution.