All skills
callstackincubator avatar

/react-native-best-practices

@fa0bad0 official

Provides React Native performance optimization guidelines for FPS, TTI, bundle size, memory leaks, re-renders, and animations. Applies to tasks involving Hermes optimization, JS thread blocking, bridge overhead, FlashList, native modules, or debugging jank and frame drops.

Use this Skill: https://skilld.dev/gh/callstackincubator/agent-skills/react-native-best-practices

This session only. Nothing lands on disk.

referencesjs-concurrent-react.md

≈1.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Skill: Concurrent React

Use useDeferredValue and useTransition to improve perceived performance by prioritizing critical updates.

Quick Pattern

Incorrect (blocks input on every keystroke):

const [query, setQuery] = useState('');
<TextInput value={query} onChangeText={setQuery} />
<ExpensiveList query={query} />  // Blocks typing

Correct (input stays responsive):

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
<TextInput value={query} onChangeText={setQuery} />
<ExpensiveList query={deferredQuery} />  // Deferred update

When to Use

  • Search/filter inputs feel laggy with large result sets
  • Expensive computations block UI interactions
  • Loading states appear too frequently
  • Want to show stale content while loading new content
  • Need to prioritize user input over background updates

Prerequisites

  • React 18+ features (useDeferredValue, useTransition, Suspense)
  • React Native version that supports your target concurrent behavior; validate on the app architecture you ship

Concept Overview

Concurrent React allows updates to be:

  • Paused: Low-priority work can wait
  • Interrupted: User input takes priority
  • Abandoned: Outdated updates can be skipped

Step-by-Step Instructions

Pattern 1: Defer Expensive Rendering with useDeferredValue

Use when a value drives expensive computation but you want input to stay responsive.

import { useState, useDeferredValue } from 'react';

const SearchScreen = () => {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  // query updates immediately (input stays responsive)
  // deferredQuery updates when React has time
  
  return (
    <View>
      <TextInput
        value={query}
        onChangeText={setQuery}
        placeholder="Search..."
      />
      {/* ExpensiveList receives deferred value */}
      <ExpensiveList query={deferredQuery} />
    </View>
  );
};

Pattern 2: Show Stale Content While Loading

const SearchWithStaleIndicator = () => {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  
  return (
    <View>
      <TextInput value={query} onChangeText={setQuery} />
      <View style={isStale && { opacity: 0.7 }}>
        <SearchResults query={deferredQuery} />
      </View>
      {isStale && <ActivityIndicator />}
    </View>
  );
};

Pattern 3: Transition Non-Critical Updates with useTransition

Use when you have multiple state updates and want to mark some as low-priority.

import { useState, useTransition } from 'react';

const TransitionExample = () => {
  const [count, setCount] = useState(0);
  const [heavyData, setHeavyData] = useState(null);
  const [isPending, startTransition] = useTransition();
  
  const handleIncrement = () => {
    // High priority - updates immediately
    setCount(c => c + 1);
    
    // Low priority - can be interrupted
    startTransition(() => {
      setHeavyData(computeExpensiveData());
    });
  };
  
  return (
    <View>
      <Text>Count: {count}</Text>
      {isPending ? <ActivityIndicator /> : <HeavyComponent data={heavyData} />}
      <Button onPress={handleIncrement} title="Increment" />
    </View>
  );
};

Pattern 4: Suspense for Data Fetching

Use this only with a Suspense-enabled data source or framework integration. Wrapping arbitrary fetch() code in Suspense does not make it suspend automatically.

import { Suspense, useDeferredValue } from 'react';

const DataScreen = () => {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  return (
    <View>
      <TextInput value={query} onChangeText={setQuery} />
      <Suspense fallback={<LoadingSpinner />}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </View>
  );
};

Code Examples

Slow Component Optimization

// Without Concurrent React - UI freezes
const SlowSearch = () => {
  const [query, setQuery] = useState('');
  
  return (
    <>
      <TextInput value={query} onChangeText={setQuery} />
      <SlowComponent query={query} /> {/* Blocks every keystroke */}
    </>
  );
};

// With Concurrent React - UI stays responsive  
const FastSearch = () => {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  return (
    <>
      <TextInput value={query} onChangeText={setQuery} />
      <SlowComponent query={deferredQuery} />
    </>
  );
};

// Important: Wrap SlowComponent in memo to prevent re-renders from parent
const SlowComponent = memo(({ query }) => {
  // Expensive computation here
});

Automatic Batching (React 18+)

React 18 automatically batches state updates:

// Before React 18 - 2 re-renders
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Rendered twice
}, 1000);

// React 18+ - 1 re-render (automatic batching)
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Rendered once!
}, 1000);

When to Use Which

Scenario Solution
Single value drives expensive render useDeferredValue
Multiple state updates, some non-critical useTransition
Need loading indicator for transition useTransition (has isPending)
Data fetching with loading states Suspense + useDeferredValue
Simple parent-to-child value deferral useDeferredValue

Important Considerations

  1. Wrap expensive components in memo(): Without memoization, the component re-renders from parent anyway.

  2. Validate on your shipped architecture: Concurrent behavior depends on the React Native and React versions in the app.

  3. Don't overuse: Only defer truly expensive work. Adding complexity for fast components is counterproductive.

Common Pitfalls

  • Forgetting subtree isolation: useDeferredValue helps most when the expensive subtree is memoized or otherwise isolated from immediate parent re-renders
  • Using for simple state: Overhead isn't worth it for cheap updates
  • Expecting faster computation: These hooks don't make code faster, they prioritize what runs when

Related Skills

Source: SKILL.md on GitHub

1 alert17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill provides a comprehensive performance optimization guide for React Native applications. It contains standard developer workflows using command-line tools and patterns for advanced techniques like remote code splitting, which include detailed security hardening advice.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    11/31 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at fa0bad0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • Performance
  • react-native
  • optimization
  • profiling
  • fps
  • bundle-size
  • memory
  • hermes
  • flatlist
  • animations

README badge

README badge for callstackincubator/agent-skills/react-native-best-practices

Provides React Native performance optimization guidelines covering FPS, TTI, bundle size, memory leaks, re-renders, and animations. Includes reference documentation for Hermes, FlashList, Turbo Modules, React Compiler, Reanimated, and profiling with Xcode and Android Studio.

Generated from the current SKILL.md.

Does this skill cover Expo apps?
Yes. The guidelines apply to both bare React Native and Expo projects. Expo SDK 52+ supports tree shaking and Re.Pack code splitting.
What performance metrics should I measure first?
Start with FPS (use React Native DevTools), Time To Interactive (TTI), bundle size, and memory growth. The skill emphasizes measuring baseline metrics before and after optimization to validate improvements.
Does this cover Hermes optimization?
Yes. The skill includes Hermes-specific guidance such as disabling JS bundle compression on Android to enable mmap, and checking Hermes API coverage before removing polyfills.
What if I'm using a list library other than FlatList or FlashList?
The skill prioritizes FlatList and FlashList as virtualized replacements for ScrollView. It includes library version checks (e.g., FlashList v2 API changes) but focuses on these two libraries.
Does this include native profiling guidance?
Yes. The skill covers profiling with Xcode Instruments (iOS) and Android Studio CPU Profiler, plus guidance on native threading, Turbo Modules, and C++ performance-critical code.

Generated from the current SKILL.md. These answers refresh after source changes.