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-uncontrolled-components.md

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

Skill: Uncontrolled Components

Fix TextInput synchronization and flickering issues by using the uncontrolled component pattern where React does not need to own every keystroke.

Quick Pattern

Before (controlled - may flicker on legacy arch):

<TextInput value={text} onChangeText={setText} />

After (uncontrolled - native owns state):

<TextInput defaultValue={text} onChangeText={setText} />

When to Use

  • TextInput flickers or shows wrong characters during fast typing
  • Text input lags behind user input on low-end devices
  • Using legacy (non-New Architecture) React Native
  • Need maximum input responsiveness
  • React does not need to transform, mask, validate, or own the value on every keystroke

Prerequisites

  • Understanding of React controlled vs uncontrolled components
  • TextInput component in use

Problem Description

Controlled TextInput Ping-Pong Communication

The diagram shows what happens when typing "TEST" with a controlled TextInput:

  1. User types "T" → onChangeText('T') fires
  2. React calls setValue('T') → native updates to "T"
  3. User types "E" → onChangeText('TE') fires
  4. React calls setValue('TE') → native updates to "TE"
  5. ...continues for each character

The problem: Each character requires a round-trip between native and JavaScript. On legacy architecture, if React state update is slow, native may show intermediate states (flicker).

New Architecture note: This issue is largely resolved in New Architecture, but uncontrolled pattern still provides best performance.

Use uncontrolled TextInput primarily as a responsiveness or legacy-architecture escape hatch. Keep controlled inputs when React must transform, mask, validate, or own the value on every keystroke.

Step-by-Step Instructions

1. Identify Controlled TextInput

// Controlled - value prop syncs state to native
const ControlledInput = () => {
  const [value, setValue] = useState('');
  
  return (
    <TextInput
      value={value}           // This causes sync issues
      onChangeText={setValue}
    />
  );
};

2. Convert to Uncontrolled

Remove the value prop to make it uncontrolled:

// Uncontrolled - native owns the state
const UncontrolledInput = () => {
  const [value, setValue] = useState('');
  
  return (
    <TextInput
      defaultValue={value}     // Only sets initial value
      onChangeText={setValue}  // Still updates React state
    />
  );
};

3. Use Ref for Programmatic Control

If you need to read/set value programmatically:

const UncontrolledWithRef = () => {
  const inputRef = useRef(null);
  
  const clearInput = () => {
    inputRef.current?.clear();
  };
  
  const getValue = () => {
    // Use onChangeText to track value, or native methods
  };
  
  return (
    <TextInput
      ref={inputRef}
      defaultValue=""
      onChangeText={(text) => console.log('Current:', text)}
    />
  );
};

Code Examples

Full Migration Example

Before (Controlled):

const SearchInput = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const handleChange = (text) => {
    setQuery(text);
    fetchResults(text).then(setResults);
  };
  
  return (
    <View>
      <TextInput
        value={query}              // Remove this
        onChangeText={handleChange}
        placeholder="Search..."
      />
      <ResultsList data={results} />
    </View>
  );
};

After (Uncontrolled):

const SearchInput = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const handleChange = (text) => {
    setQuery(text);
    fetchResults(text).then(setResults);
  };
  
  return (
    <View>
      <TextInput
        defaultValue=""           // Initial value only
        onChangeText={handleChange}
        placeholder="Search..."
      />
      <ResultsList data={results} />
    </View>
  );
};

When You Need Value Control

For input masking or validation that modifies input:

// Option 1: Accept the controlled behavior (may flicker)
const MaskedInput = () => {
  const [value, setValue] = useState('');
  
  const handleChange = (text) => {
    // Phone mask: (123) 456-7890
    const masked = maskPhone(text);
    setValue(masked);
  };
  
  return (
    <TextInput
      value={value}  // Necessary for masking
      onChangeText={handleChange}
    />
  );
};

// Option 2: Use a native masked input library
// react-native-masked-text handles this natively

Decision Matrix

Scenario Recommendation
Simple text input Uncontrolled
Search/filter input Uncontrolled
Form with validation on submit Uncontrolled
Input masking (phone, credit card) Controlled or native library
Character-by-character validation Controlled
New Architecture app Either works well

Common Pitfalls

  • Forgetting defaultValue: Without it, input starts empty
  • Trying to clear with state: Use ref.current.clear() instead
  • Mixing patterns: Don't use both value and defaultValue

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.