Common Pitfalls in React Native

🧠 Common Pitfalls in React Native Performance & Native Module Integration

React Native empowers developers to build cross-platform apps using JavaScript, but with that power comes the responsibility of understanding performance trade-offs. Here are 3–4 common pitfalls developers run into when optimizing component performance or working with native modules.


🚫 1. Unnecessary Re-renders with Functional Components

One common pitfall is allowing components to re-render too often due to reference changes or lack of memoization.

Symptoms:

  • Sluggish UI
  • Flickering or jank on scroll
  • Poor battery usage

Example:

const MyComponent = ({ items }) => {
  const handlePress = () => {
    console.log('Pressed!');
  };

  return (
    <FlatList
      data={items}
      renderItem={({ item }) => (
        <TouchableOpacity onPress={handlePress}>
          <Text>{item.name}</Text>
        </TouchableOpacity>
      )}
    />
  );
};

Problem:

  • handlePress is re-created on every render.
  • Causes all TouchableOpacity to re-render unnecessarily.

Fix:

const MyComponent = ({ items }) => {
  const handlePress = useCallback(() => {
    console.log('Pressed!');
  }, []);

  const renderItem = useCallback(({ item }) => (
    <TouchableOpacity onPress={handlePress}>
      <Text>{item.name}</Text>
    </TouchableOpacity>
  ), [handlePress]);

  return <FlatList data={items} renderItem={renderItem} />;
};

🌀 2. Using Anonymous Functions in Render

This is similar to #1, but happens more subtly in deeply nested structures.

Example:

return items.map(item => (
  <TouchableOpacity key={item.id} onPress={() => doSomething(item.id)}>
    <Text>{item.name}</Text>
  </TouchableOpacity>
));

Why it’s bad:

  • Creates new function instances on every render.
  • Prevents internal optimization of React Native’s VirtualizedList components.

Better:

const handlePress = useCallback((id) => {
  doSomething(id);
}, []);

return items.map(item => (
  <TouchableOpacity key={item.id} onPress={() => handlePress(item.id)}>
    <Text>{item.name}</Text>
  </TouchableOpacity>
));

⚠️ 3. Not Batching Native Calls

React Native bridges JavaScript and native code using a batched message queue. But if you make too many native calls (e.g., animations, sensor access) without batching, you cause a bridge bottleneck.

Example (Bad):

for (let i = 0; i < 100; i++) {
  NativeModule.sendEvent(`Event ${i}`);
}

Fix: Use native-side batching or debounce your calls from JS.

const sendBatchedEvents = (events) => {
  NativeModule.sendBatch(events);
};

useEffect(() => {
  const batch = Array.from({ length: 100 }, (_, i) => `Event ${i}`);
  sendBatchedEvents(batch);
}, []);

🔌 4. Native Modules Without Fallbacks or Error Handling

When integrating custom native modules, forgetting to handle cases where the module doesn’t exist (e.g., on web or during testing) leads to crashes.

Example (Crash risk):

NativeModules.MyCustomModule.doSomethingCool();

Safer Approach:

const MyModule = NativeModules.MyCustomModule;

if (MyModule?.doSomethingCool) {
  MyModule.doSomethingCool();
} else {
  console.warn('MyCustomModule is not available');
}

Or create a safe wrapper:

export const doSomethingCool = () => {
  try {
    NativeModules.MyCustomModule?.doSomethingCool();
  } catch (e) {
    console.error('Error using MyCustomModule', e);
  }
};

✅ Summary: Best Practices

  • ✅ Use useCallback, memo, and stable references.
  • ✅ Avoid inline anonymous functions inside lists.
  • ✅ Batch native module calls when possible.
  • ✅ Wrap native modules with fallbacks for safer cross-platform use.

With thoughtful performance practices and cautious native integration, your React Native app can feel just as smooth as a native one — while still shipping faster across platforms.


Let me know if you want this as a downloadable file or ready to paste into a blog platform like Medium or Dev.to!