Back to Posts

React Native Expo Best Practices for 2025

By Lumina Software
react-nativeexpomobile-developmentbest-practices

React Native Expo Best Practices for 2025

React Native with Expo has evolved significantly, making it easier than ever to build cross-platform mobile apps. Here are the best practices we've learned building production apps in 2025.

Expo SDK 52 and Beyond

The latest Expo SDK brings powerful new features:

New Architecture by Default

Expo now uses the new React Native architecture by default, which means:

  • Faster performance: Improved rendering pipeline
  • Better TypeScript support: Stronger type safety
  • Concurrent features: React 19 concurrent rendering

Expo Router v4

The file-based routing system has matured:

// app/(tabs)/profile.tsx
export default function ProfileScreen() {
  return <View>...</View>;
}

// Automatic deep linking support
// app/profile/[id].tsx handles /profile/123

Performance Optimization

1. Image Optimization

Always optimize images:

import { Image } from 'expo-image';

// Use expo-image instead of React Native's Image
<Image
  source={{ uri: 'https://example.com/image.jpg' }}
  placeholder={blurhash}
  contentFit="cover"
  transition={200}
/>

2. Code Splitting

Use dynamic imports for heavy components:

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

3. List Optimization

For long lists, use FlashList:

import { FlashList } from '@shopify/flash-list';

<FlashList
  data={items}
  renderItem={renderItem}
  estimatedItemSize={100}
/>

State Management

Expo Router + Zustand

A lightweight combination:

import create from 'zustand';

const useStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

// Use in any component
function Profile() {
  const user = useStore((state) => state.user);
  return <Text>{user?.name}</Text>;
}

Server State with TanStack Query

For API data:

import { useQuery } from '@tanstack/react-query';

function Posts() {
  const { data, isLoading } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
  
  if (isLoading) return <Loading />;
  return <PostsList data={data} />;
}

Type Safety

Strict TypeScript Configuration

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true
  }
}

Type-Safe Navigation

import { router } from 'expo-router';

// Type-safe navigation
router.push({
  pathname: '/profile/[id]',
  params: { id: '123' },
});

Testing Strategy

Unit Tests with Vitest

import { describe, it, expect } from 'vitest';

describe('UserService', () => {
  it('should fetch user data', async () => {
    const user = await fetchUser('123');
    expect(user).toBeDefined();
  });
});

Component Tests with React Native Testing Library

import { render, screen } from '@testing-library/react-native';

test('renders user name', () => {
  render(<Profile user={{ name: 'John' }} />);
  expect(screen.getByText('John')).toBeTruthy();
});

Development Workflow

EAS Build

Use EAS for production builds:

# Development build
eas build --profile development --platform ios

# Production build
eas build --profile production --platform all

Environment Variables

Use expo-constants for environment variables:

import Constants from 'expo-constants';

const API_URL = Constants.expoConfig?.extra?.apiUrl;

Common Pitfalls to Avoid

  1. Not using Hermes: Always enable Hermes for better performance
  2. Ignoring bundle size: Monitor and optimize your bundle
  3. Skipping error boundaries: Implement proper error handling
  4. Not testing on real devices: Simulators don't catch everything
  5. Ignoring accessibility: Use accessibility labels and roles

Modern Patterns

Custom Hooks

function useUser(userId: string) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchUser(userId).then(setUser).finally(() => setLoading(false));
  }, [userId]);
  
  return { user, loading };
}

Error Boundaries

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorScreen />;
    }
    return this.props.children;
  }
}

Conclusion

React Native with Expo in 2025 offers a mature, powerful platform for mobile development. By following these best practices, you'll build apps that are performant, maintainable, and delightful to use.

The key is staying current with Expo's rapid evolution while maintaining solid engineering fundamentals.