React Native Cheatsheet

State and Hooks

Use this React Native reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

useState

import { useState } from 'react';

const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
const [list, setList] = useState<string[]>([]);

// Update based on previous value (always use for derived state)
setCount(prev => prev + 1);

// Partial object update
setUser(prev => ({ ...prev!, name: 'Alice' }));

// Array mutations
setList(prev => [...prev, 'new item']);
setList(prev => prev.filter(item => item !== 'old'));
setList(prev => prev.map(item => item === 'old' ? 'new' : item));

// Lazy initializer (runs once)
const [state, setState] = useState(() => expensiveInit());

useEffect

import { useEffect } from 'react';

// Runs after every render
useEffect(() => { console.log('rendered'); });

// Runs once on mount
useEffect(() => {
  fetchData();
}, []);

// Runs when dep changes
useEffect(() => {
  fetchUser(userId);
}, [userId]);

// Cleanup (runs before next effect + on unmount)
useEffect(() => {
  const sub = subscribe(userId);
  return () => sub.unsubscribe();
}, [userId]);

Common RN patterns with useEffect

// AppState listener
useEffect(() => {
  const sub = AppState.addEventListener('change', nextState => {
    if (nextState === 'active') refreshData();
  });
  return () => sub.remove();
}, []);

// NetInfo listener
useEffect(() => {
  const unsubscribe = NetInfo.addEventListener(state => {
    setIsOnline(state.isConnected ?? false);
  });
  return unsubscribe;
}, []);

// Keyboard listeners
useEffect(() => {
  const show = Keyboard.addListener('keyboardDidShow', e => {
    setKeyboardHeight(e.endCoordinates.height);
  });
  const hide = Keyboard.addListener('keyboardDidHide', () => setKeyboardHeight(0));
  return () => { show.remove(); hide.remove(); };
}, []);

useRef

import { useRef } from 'react';

// Component refs
const inputRef = useRef<TextInput>(null);
const listRef = useRef<FlatList>(null);

inputRef.current?.focus();
inputRef.current?.blur();
inputRef.current?.clear();
listRef.current?.scrollToEnd({ animated: true });

// Mutable value that doesn't trigger re-render
const timerRef = useRef<ReturnType<typeof setTimeout>>();
timerRef.current = setTimeout(() => {}, 1000);
clearTimeout(timerRef.current);

// Tracking previous value
const prevCount = useRef(count);
useEffect(() => { prevCount.current = count; }, [count]);

useCallback

import { useCallback } from 'react';

// Memoize function — stable reference across renders
const handlePress = useCallback(() => {
  doSomething(id);
}, [id]);

// Critical for FlatList renderItem to prevent re-renders
const renderItem = useCallback(({ item }) => (
  <ItemCard item={item} onPress={handlePress} />
), [handlePress]);

<FlatList renderItem={renderItem} ... />

useMemo

import { useMemo } from 'react';

// Expensive computation
const sortedList = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// Filtered data
const filteredData = useMemo(
  () => data.filter(item => item.active && item.name.includes(query)),
  [data, query]
);

useContext

import { createContext, useContext, useState } from 'react';

// Create
const ThemeContext = createContext<'light' | 'dark'>('light');

// Provide
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}

// Consume
function MyComponent() {
  const theme = useContext(ThemeContext);
  return <View style={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }} />;
}

useReducer

import { useReducer } from 'react';

type State = { count: number; loading: boolean };
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'set_loading'; payload: boolean };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'decrement': return { ...state, count: state.count - 1 };
    case 'set_loading': return { ...state, loading: action.payload };
    default: return state;
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, loading: false });

dispatch({ type: 'increment' });
dispatch({ type: 'set_loading', payload: true });

Custom Hooks

// useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);
  return debouncedValue;
}

const debouncedSearch = useDebounce(searchText, 300);
// useAsyncData
function useAsyncData<T>(fetcher: () => Promise<T>, deps: any[] = []) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetcher()
      .then(result => { if (!cancelled) setData(result); })
      .catch(err => { if (!cancelled) setError(err); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, deps);

  return { data, loading, error };
}

const { data: users, loading } = useAsyncData(() => fetchUsers(), []);
// useAppState
import { AppState, AppStateStatus } from 'react-native';

function useAppState() {
  const [appState, setAppState] = useState(AppState.currentState);
  useEffect(() => {
    const sub = AppState.addEventListener('change', setAppState);
    return () => sub.remove();
  }, []);
  return appState;
}

const appState = useAppState(); // 'active' | 'background' | 'inactive'
// useKeyboard
import { Keyboard, KeyboardEvent } from 'react-native';

function useKeyboard() {
  const [keyboardHeight, setKeyboardHeight] = useState(0);
  useEffect(() => {
    const show = Keyboard.addListener('keyboardWillShow', (e: KeyboardEvent) => {
      setKeyboardHeight(e.endCoordinates.height);
    });
    const hide = Keyboard.addListener('keyboardWillHide', () => setKeyboardHeight(0));
    return () => { show.remove(); hide.remove(); };
  }, []);
  return keyboardHeight;
}

React Query (TanStack Query) — Data Fetching

npm install @tanstack/react-query
import { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';

const queryClient = new QueryClient();

// Wrap app
<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

// Query
const { data, isLoading, isError, refetch } = useQuery({
  queryKey: ['users', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 5 * 60 * 1000,   // 5 min
  gcTime: 10 * 60 * 1000,     // 10 min
});

// Mutation
const mutation = useMutation({
  mutationFn: (newUser) => postUser(newUser),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
});

mutation.mutate({ name: 'Alice' });

Zustand — Global State

npm install zustand
import { create } from 'zustand';

interface AppStore {
  count: number;
  user: User | null;
  increment: () => void;
  setUser: (user: User) => void;
}

const useStore = create<AppStore>((set) => ({
  count: 0,
  user: null,
  increment: () => set(state => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
}));

// In component
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);

useAnimatedValue (Animated API)

import { Animated, useAnimatedValue } from 'react-native';

// RN 0.71+ hook — creates a stable Animated.Value
const opacity = useAnimatedValue(0);
const translateY = useAnimatedValue(20);

// Pre-0.71 equivalent:
// const opacity = useRef(new Animated.Value(0)).current;

// Fade in
Animated.timing(opacity, {
  toValue: 1,
  duration: 300,
  useNativeDriver: true,
}).start();

// Spring
Animated.spring(translateY, {
  toValue: 0,
  tension: 50,
  friction: 7,
  useNativeDriver: true,
}).start(({ finished }) => console.log('done'));

// Sequence
Animated.sequence([
  Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }),
  Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }),
]).start();

// Parallel
Animated.parallel([
  Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
]).start();

<Animated.View style={{ opacity, transform: [{ translateY }] }}>
  <Text>Animated</Text>
</Animated.View>

useNativeDriver: true offloads animation to the native thread — always use it when possible. Limitation: only opacity and transform are supported (no backgroundColor, width, height).