
Introduction
React Hooks revolutionized the way we write React components by allowing us to use state and other React features in functional components. React Native hooks provide the same benefits: cleaner code, better reusability, and improved performance. This chapter will cover all the essential hooks you need to master for React Native development.
What are React Hooks?
Hooks are functions that let you “hook into” React state and lifecycle features from functional components. They were introduced in React 16.8 and have become the standard way of writing React components.
Rules of Hooks
Before diving into specific hooks, it’s crucial to understand the two fundamental rules:
- Only call hooks at the top level – Don’t call hooks inside loops, conditions, or nested functions
- Only call hooks from React functions – Call them from React functional components or custom hooks
Built-in React Hooks in React Native
1. useState Hook
The useState hook allows you to add state to functional components.
Basic Syntax
const [state, setState] = useState(initialValue);
Example: Counter App
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
const CounterApp = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<View style={styles.container}>
<Text style={styles.countText}>Count: {count}</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={increment}>
<Text style={styles.buttonText}>+</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={decrement}>
<Text style={styles.buttonText}>-</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={reset}>
<Text style={styles.buttonText}>Reset</Text>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
countText: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
buttonContainer: {
flexDirection: 'row',
gap: 10,
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default CounterApp;
Complex State Example
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
const UserForm = () => {
const [user, setUser] = useState({
name: '',
email: '',
age: '',
});
const updateUser = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value,
}));
};
const handleSubmit = () => {
console.log('User data:', user);
// Reset form
setUser({ name: '', email: '', age: '' });
};
return (
<View style={styles.container}>
<Text style={styles.title}>User Information</Text>
<TextInput
style={styles.input}
placeholder="Name"
value={user.name}
onChangeText={(text) => updateUser('name', text)}
/>
<TextInput
style={styles.input}
placeholder="Email"
value={user.email}
onChangeText={(text) => updateUser('email', text)}
keyboardType="email-address"
/>
<TextInput
style={styles.input}
placeholder="Age"
value={user.age}
onChangeText={(text) => updateUser('age', text)}
keyboardType="numeric"
/>
<TouchableOpacity style={styles.submitButton} onPress={handleSubmit}>
<Text style={styles.submitButtonText}>Submit</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 12,
marginBottom: 15,
borderRadius: 8,
fontSize: 16,
},
submitButton: {
backgroundColor: '#28a745',
padding: 15,
borderRadius: 8,
alignItems: 'center',
},
submitButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default UserForm;
2. useEffect Hook
The useEffect hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Basic Syntax
useEffect(() => {
// Side effect logic
return () => {
// Cleanup logic (optional)
};
}, [dependencies]); // Dependencies array (optional)
Example: Data Fetching
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
const PostsList = () => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchPosts();
}, []); // Empty dependency array means this runs once on mount
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data.slice(0, 10)); // Get first 10 posts
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const renderPost = ({ item }) => (
<View style={styles.postContainer}>
<Text style={styles.postTitle}>{item.title}</Text>
<Text style={styles.postBody}>{item.body}</Text>
</View>
);
if (loading) {
return (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading posts...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.centerContainer}>
<Text style={styles.errorText}>Error: {error}</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Latest Posts</Text>
<FlatList
data={posts}
renderItem={renderPost}
keyExtractor={(item) => item.id.toString()}
showsVerticalScrollIndicator={false}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 15,
},
centerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 15,
textAlign: 'center',
},
postContainer: {
backgroundColor: 'white',
padding: 15,
marginBottom: 10,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
postTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
},
postBody: {
fontSize: 14,
color: '#666',
lineHeight: 20,
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#666',
},
errorText: {
fontSize: 16,
color: '#ff3333',
textAlign: 'center',
},
});
export default PostsList;
useEffect with Cleanup
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(true);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
}
// Cleanup function
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isActive]); // Dependency on isActive
// Cleanup when component unmounts
useEffect(() => {
return () => {
console.log('Timer component unmounted');
};
}, []);
return (
<View style={styles.container}>
<Text style={styles.timerText}>Timer: {seconds}s</Text>
<Text style={styles.statusText}>
Status: {isActive ? 'Running' : 'Paused'}
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
timerText: {
fontSize: 32,
fontWeight: 'bold',
color: '#007AFF',
},
statusText: {
fontSize: 18,
marginTop: 10,
color: '#666',
},
});
export default Timer;
3. useContext Hook
The useContext hook allows you to consume context values without nesting.
Example: Theme Context
// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
export const ThemeProvider = ({ children }) => {
const [isDark, setIsDark] = useState(false);
const theme = {
isDark,
colors: {
background: isDark ? '#000' : '#fff',
text: isDark ? '#fff' : '#000',
primary: '#007AFF',
},
toggleTheme: () => setIsDark(!isDark),
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};
// ThemedComponent.js
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useTheme } from './ThemeContext';
const ThemedComponent = () => {
const { colors, isDark, toggleTheme } = useTheme();
const dynamicStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
text: {
color: colors.text,
fontSize: 18,
marginBottom: 20,
},
button: {
backgroundColor: colors.primary,
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
return (
<View style={dynamicStyles.container}>
<Text style={dynamicStyles.text}>
Current theme: {isDark ? 'Dark' : 'Light'}
</Text>
<TouchableOpacity style={dynamicStyles.button} onPress={toggleTheme}>
<Text style={dynamicStyles.buttonText}>Toggle Theme</Text>
</TouchableOpacity>
</View>
);
};
export default ThemedComponent;
4. useReducer Hook
The useReducer hook is an alternative to useState for managing complex state logic.
Example: Todo App with useReducer
import React, { useReducer, useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
// Reducer function
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: Date.now(),
text: action.payload,
completed: false,
},
];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
case 'CLEAR_COMPLETED':
return state.filter(todo => !todo.completed);
default:
return state;
}
};
const TodoApp = () => {
const [todos, dispatch] = useReducer(todoReducer, []);
const [inputText, setInputText] = useState('');
const addTodo = () => {
if (inputText.trim()) {
dispatch({ type: 'ADD_TODO', payload: inputText.trim() });
setInputText('');
}
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
const deleteTodo = (id) => {
dispatch({ type: 'DELETE_TODO', payload: id });
};
const clearCompleted = () => {
dispatch({ type: 'CLEAR_COMPLETED' });
};
const renderTodo = ({ item }) => (
<View style={styles.todoItem}>
<TouchableOpacity
style={[
styles.todoCheckbox,
item.completed && styles.todoCheckboxCompleted,
]}
onPress={() => toggleTodo(item.id)}
>
<Text style={styles.todoCheckboxText}>
{item.completed ? '✓' : ''}
</Text>
</TouchableOpacity>
<Text
style={[
styles.todoText,
item.completed && styles.todoTextCompleted,
]}
>
{item.text}
</Text>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => deleteTodo(item.id)}
>
<Text style={styles.deleteButtonText}>×</Text>
</TouchableOpacity>
</View>
);
const completedCount = todos.filter(todo => todo.completed).length;
return (
<View style={styles.container}>
<Text style={styles.title}>Todo App</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Add a new todo..."
value={inputText}
onChangeText={setInputText}
onSubmitEditing={addTodo}
/>
<TouchableOpacity style={styles.addButton} onPress={addTodo}>
<Text style={styles.addButtonText}>Add</Text>
</TouchableOpacity>
</View>
<View style={styles.statsContainer}>
<Text style={styles.statsText}>
Total: {todos.length} | Completed: {completedCount}
</Text>
{completedCount > 0 && (
<TouchableOpacity onPress={clearCompleted}>
<Text style={styles.clearText}>Clear Completed</Text>
</TouchableOpacity>
)}
</View>
<FlatList
data={todos}
renderItem={renderTodo}
keyExtractor={(item) => item.id.toString()}
style={styles.todoList}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
color: '#333',
},
inputContainer: {
flexDirection: 'row',
marginBottom: 20,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#ddd',
padding: 12,
borderRadius: 8,
backgroundColor: 'white',
marginRight: 10,
},
addButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
justifyContent: 'center',
},
addButtonText: {
color: 'white',
fontWeight: 'bold',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
},
statsText: {
fontSize: 14,
color: '#666',
},
clearText: {
fontSize: 14,
color: '#ff3333',
fontWeight: 'bold',
},
todoList: {
flex: 1,
},
todoItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
marginBottom: 8,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
todoCheckbox: {
width: 24,
height: 24,
borderWidth: 2,
borderColor: '#ddd',
borderRadius: 12,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
todoCheckboxCompleted: {
backgroundColor: '#28a745',
borderColor: '#28a745',
},
todoCheckboxText: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
todoText: {
flex: 1,
fontSize: 16,
color: '#333',
},
todoTextCompleted: {
textDecorationLine: 'line-through',
color: '#999',
},
deleteButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#ff3333',
justifyContent: 'center',
alignItems: 'center',
},
deleteButtonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});
export default TodoApp;
5. useMemo Hook
The useMemo hook memoizes expensive calculations to optimize performance.
Example: Expensive Calculation
import React, { useState, useMemo } from 'react';
import {
View,
Text,
TextInput,
FlatList,
StyleSheet,
} from 'react-native';
const ExpensiveList = () => {
const [searchTerm, setSearchTerm] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// Mock data
const rawData = useMemo(() => {
return Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i + 1}`,
value: Math.floor(Math.random() * 1000),
}));
}, []);
// Expensive filtering and sorting operation
const processedData = useMemo(() => {
console.log('Processing data...'); // This should only log when dependencies change
let filtered = rawData.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
filtered.sort((a, b) => {
if (sortOrder === 'asc') {
return a.value - b.value;
} else {
return b.value - a.value;
}
});
return filtered;
}, [rawData, searchTerm, sortOrder]);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemValue}>Value: {item.value}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>Optimized List</Text>
<TextInput
style={styles.searchInput}
placeholder="Search items..."
value={searchTerm}
onChangeText={setSearchTerm}
/>
<View style={styles.sortContainer}>
<Text style={styles.sortLabel}>Sort by value:</Text>
<TouchableOpacity
style={[
styles.sortButton,
sortOrder === 'asc' && styles.sortButtonActive,
]}
onPress={() => setSortOrder('asc')}
>
<Text style={styles.sortButtonText}>↑ Ascending</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sortButton,
sortOrder === 'desc' && styles.sortButtonActive,
]}
onPress={() => setSortOrder('desc')}
>
<Text style={styles.sortButtonText}>↓ Descending</Text>
</TouchableOpacity>
</View>
<Text style={styles.resultCount}>
Showing {processedData.length} items
</Text>
<FlatList
data={processedData}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
style={styles.list}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 15,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
searchInput: {
backgroundColor: 'white',
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
marginBottom: 15,
},
sortContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 15,
},
sortLabel: {
fontSize: 16,
marginRight: 10,
},
sortButton: {
backgroundColor: '#e0e0e0',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 6,
marginRight: 10,
},
sortButtonActive: {
backgroundColor: '#007AFF',
},
sortButtonText: {
fontSize: 14,
color: '#333',
},
resultCount: {
fontSize: 14,
color: '#666',
marginBottom: 10,
},
list: {
flex: 1,
},
item: {
backgroundColor: 'white',
padding: 15,
marginBottom: 8,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
},
itemValue: {
fontSize: 14,
color: '#666',
},
});
export default ExpensiveList;
6. useCallback Hook
The useCallback hook memoizes functions to prevent unnecessary re-renders.
Example: Parent-Child Optimization
import React, { useState, useCallback, memo } from 'react';
import {
View,
Text,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
// Memoized child component
const TaskItem = memo(({ task, onToggle, onDelete }) => {
console.log(`Rendering TaskItem: ${task.title}`);
return (
<View style={styles.taskItem}>
<TouchableOpacity
style={[
styles.taskCheckbox,
task.completed && styles.taskCheckboxCompleted,
]}
onPress={() => onToggle(task.id)}
>
<Text style={styles.taskCheckboxText}>
{task.completed ? '✓' : ''}
</Text>
</TouchableOpacity>
<Text
style={[
styles.taskTitle,
task.completed && styles.taskTitleCompleted,
]}
>
{task.title}
</Text>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => onDelete(task.id)}
>
<Text style={styles.deleteButtonText}>Delete</Text>
</TouchableOpacity>
</View>
);
});
const TaskManager = () => {
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn React Native', completed: false },
{ id: 2, title: 'Build an app', completed: false },
{ id: 3, title: 'Deploy to store', completed: false },
]);
const [counter, setCounter] = useState(0);
// Without useCallback, these functions would be recreated on every render
const handleToggleTask = useCallback((taskId) => {
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);
}, []);
const handleDeleteTask = useCallback((taskId) => {
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId));
}, []);
const addNewTask = useCallback(() => {
const newTask = {
id: Date.now(),
title: `New Task ${tasks.length + 1}`,
completed: false,
};
setTasks(prevTasks => [...prevTasks, newTask]);
}, [tasks.length]);
const renderTask = useCallback(({ item }) => (
<TaskItem
task={item}
onToggle={handleToggleTask}
onDelete={handleDeleteTask}
/>
), [handleToggleTask, handleDeleteTask]);
return (
<View style={styles.container}>
<Text style={styles.title}>Task Manager</Text>
{/* This counter demonstrates that the component re-renders,
but TaskItem components won't re-render unnecessarily */}
<View style={styles.counterContainer}>
<Text style={styles.counterText}>Counter: {counter}</Text>
<TouchableOpacity
style={styles.counterButton}
onPress={() => setCounter(counter + 1)}
>
<Text style={styles.counterButtonText}>Increment</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.addButton} onPress={addNewTask}>
<Text style={styles.addButtonText}>Add New Task</Text>
</TouchableOpacity>
<FlatList
data={tasks}
renderItem={renderTask}
keyExtractor={(item) => item.id.toString()}
style={styles.taskList}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
counterContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
borderRadius: 8,
marginBottom: 15,
},
counterText: {
fontSize: 16,
},
counterButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 6,
},
counterButtonText: {
color: 'white',
fontWeight: 'bold',
},
addButton: {
backgroundColor: '#28a745',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginBottom: 15,
},
addButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
taskList: {
flex: 1,
},
taskItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
marginBottom: 8,
borderRadius: 8,
},
taskCheckbox: {
width: 24,
height: 24,
borderWidth: 2,
borderColor: '#ddd',
borderRadius: 12,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
taskCheckboxCompleted: {
backgroundColor: '#28a745',
borderColor: '#28a745',
},
taskCheckboxText: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
taskTitle: {
flex: 1,
fontSize: 16,
color: '#333',
},
taskTitleCompleted: {
textDecorationLine: 'line-through',
color: '#999',
},
deleteButton: {
backgroundColor: '#ff3333',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
deleteButtonText: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
});
export default TaskManager;
7. useRef Hook
The useRef hook creates a mutable reference that persists for the lifetime of the component. It’s commonly used for accessing DOM elements or storing mutable values.
Example: Focus Management and Imperative Actions
import React, { useRef, useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
StyleSheet,
Animated,
} from 'react-native';
const RefExamples = () => {
// Refs for TextInput focus management
const firstNameRef = useRef(null);
const lastNameRef = useRef(null);
const emailRef = useRef(null);
// Ref for storing previous value
const prevCountRef = useRef();
const [count, setCount] = useState(0);
// Ref for ScrollView
const scrollViewRef = useRef(null);
// Ref for Animated.Value
const fadeAnim = useRef(new Animated.Value(1)).current;
// Ref for storing interval ID
const intervalRef = useRef(null);
const [timer, setTimer] = useState(0);
const [isRunning, setIsRunning] = useState(false);
// Store previous count value
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
// Auto-focus first input on mount
useEffect(() => {
firstNameRef.current?.focus();
}, []);
// Timer functions
const startTimer = () => {
if (!isRunning) {
setIsRunning(true);
intervalRef.current = setInterval(() => {
setTimer(prevTimer => prevTimer + 1);
}, 1000);
}
};
const stopTimer = () => {
if (isRunning) {
setIsRunning(false);
clearInterval(intervalRef.current);
}
};
const resetTimer = () => {
setIsRunning(false);
clearInterval(intervalRef.current);
setTimer(0);
};
// Cleanup interval on unmount
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
// Animation functions
const fadeOut = () => {
Animated.timing(fadeAnim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}).start();
};
const fadeIn = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}).start();
};
// Focus management
const focusNext = (nextRef) => {
nextRef.current?.focus();
};
const scrollToTop = () => {
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true });
};
const scrollToBottom = () => {
scrollViewRef.current?.scrollToEnd({ animated: true });
};
return (
<ScrollView ref={scrollViewRef} style={styles.container}>
<Text style={styles.title}>useRef Hook Examples</Text>
{/* Focus Management Example */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>1. Focus Management</Text>
<TextInput
ref={firstNameRef}
style={styles.input}
placeholder="First Name"
returnKeyType="next"
onSubmitEditing={() => focusNext(lastNameRef)}
/>
<TextInput
ref={lastNameRef}
style={styles.input}
placeholder="Last Name"
returnKeyType="next"
onSubmitEditing={() => focusNext(emailRef)}
/>
<TextInput
ref={emailRef}
style={styles.input}
placeholder="Email"
keyboardType="email-address"
returnKeyType="done"
/>
<TouchableOpacity
style={styles.button}
onPress={() => firstNameRef.current?.focus()}
>
<Text style={styles.buttonText}>Focus First Input</Text>
</TouchableOpacity>
</View>
{/* Previous Value Example */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>2. Storing Previous Values</Text>
<Text style={styles.text}>Current count: {count}</Text>
<Text style={styles.text}>Previous count: {prevCount}</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={styles.button}
onPress={() => setCount(count + 1)}
>
<Text style={styles.buttonText}>Increment</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => setCount(count - 1)}
>
<Text style={styles.buttonText}>Decrement</Text>
</TouchableOpacity>
</View>
</View>
{/* Timer Example */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>3. Storing Mutable Values (Timer)</Text>
<Text style={styles.timerText}>Timer: {timer}s</Text>
<Text style={styles.text}>Status: {isRunning ? 'Running' : 'Stopped'}</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.startButton]}
onPress={startTimer}
disabled={isRunning}
>
<Text style={styles.buttonText}>Start</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.stopButton]}
onPress={stopTimer}
disabled={!isRunning}
>
<Text style={styles.buttonText}>Stop</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.resetButton]}
onPress={resetTimer}
>
<Text style={styles.buttonText}>Reset</Text>
</TouchableOpacity>
</View>
</View>
{/* Animation Example */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>4. Animated Values</Text>
<Animated.View
style={[
styles.animatedBox,
{ opacity: fadeAnim }
]}
>
<Text style={styles.animatedText}>Animated Box</Text>
</Animated.View>
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.button} onPress={fadeOut}>
<Text style={styles.buttonText}>Fade Out</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={fadeIn}>
<Text style={styles.buttonText}>Fade In</Text>
</TouchableOpacity>
</View>
</View>
{/* ScrollView Control */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>5. ScrollView Control</Text>
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.button} onPress={scrollToTop}>
<Text style={styles.buttonText}>Scroll to Top</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={scrollToBottom}>
<Text style={styles.buttonText}>Scroll to Bottom</Text>
</TouchableOpacity>
</View>
</View>
{/* Spacer to demonstrate scrolling */}
<View style={styles.spacer} />
<Text style={styles.bottomText}>Bottom of the page</Text>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
color: '#333',
},
section: {
backgroundColor: 'white',
padding: 20,
marginBottom: 20,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 12,
marginBottom: 10,
borderRadius: 8,
fontSize: 16,
},
text: {
fontSize: 16,
marginBottom: 10,
color: '#333',
},
timerText: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
color: '#007AFF',
},
button: {
backgroundColor: '#007AFF',
padding: 12,
borderRadius: 8,
alignItems: 'center',
marginBottom: 10,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 10,
},
startButton: {
backgroundColor: '#28a745',
flex: 1,
marginRight: 5,
},
stopButton: {
backgroundColor: '#dc3545',
flex: 1,
marginHorizontal: 5,
},
resetButton: {
backgroundColor: '#6c757d',
flex: 1,
marginLeft: 5,
},
animatedBox: {
width: 100,
height: 100,
backgroundColor: '#007AFF',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
marginBottom: 20,
},
animatedText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
spacer: {
height: 500,
},
bottomText: {
fontSize: 18,
textAlign: 'center',
marginBottom: 50,
color: '#666',
},
});
export default RefExamples;
Custom Hooks
Custom hooks are JavaScript functions that use built-in hooks and allow you to extract component logic into reusable functions.
Example: Custom useLocalStorage Hook
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
const useAsyncStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(initialValue);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadStoredValue();
}, [key]);
const loadStoredValue = async () => {
try {
setLoading(true);
const item = await AsyncStorage.getItem(key);
if (item !== null) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.error(`Error loading ${key} from AsyncStorage:`, error);
} finally {
setLoading(false);
}
};
const setValue = async (value) => {
try {
setStoredValue(value);
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error saving ${key} to AsyncStorage:`, error);
}
};
const removeValue = async () => {
try {
setStoredValue(initialValue);
await AsyncStorage.removeItem(key);
} catch (error) {
console.error(`Error removing ${key} from AsyncStorage:`, error);
}
};
return [storedValue, setValue, removeValue, loading];
};
export default useAsyncStorage;
Example: Custom useApi Hook
// hooks/useApi.js
import { useState, useEffect, useCallback } from 'react';
const useApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async (customUrl = url, customOptions = options) => {
try {
setLoading(true);
setError(null);
const response = await fetch(customUrl, {
headers: {
'Content-Type': 'application/json',
...customOptions.headers,
},
...customOptions,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
if (url && options.immediate !== false) {
fetchData();
}
}, [fetchData, url]);
const refetch = useCallback(() => fetchData(), [fetchData]);
return {
data,
loading,
error,
refetch,
fetchData,
};
};
export default useApi;
Example: Custom useDebounce Hook
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
Using Custom Hooks Together
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
FlatList,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import useApi from './hooks/useApi';
import useDebounce from './hooks/useDebounce';
import useAsyncStorage from './hooks/useAsyncStorage';
const SearchApp = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Custom hooks in action
const [favorites, setFavorites, removeFavorites] = useAsyncStorage('favorites', []);
const { data: searchResults, loading, error, fetchData } = useApi(
null, // URL will be set dynamically
{ immediate: false }
);
// Search when debounced term changes
React.useEffect(() => {
if (debouncedSearchTerm.trim()) {
const searchUrl = `https://jsonplaceholder.typicode.com/posts?q=${debouncedSearchTerm}`;
fetchData(searchUrl);
}
}, [debouncedSearchTerm, fetchData]);
const addToFavorites = (post) => {
const newFavorites = [...favorites, post];
setFavorites(newFavorites);
};
const removeFromFavorites = (postId) => {
const newFavorites = favorites.filter(post => post.id !== postId);
setFavorites(newFavorites);
};
const isFavorite = (postId) => {
return favorites.some(post => post.id === postId);
};
const renderPost = ({ item }) => (
<View style={styles.postItem}>
<Text style={styles.postTitle}>{item.title}</Text>
<Text style={styles.postBody}>{item.body}</Text>
<TouchableOpacity
style={[
styles.favoriteButton,
isFavorite(item.id) && styles.favoriteButtonActive,
]}
onPress={() =>
isFavorite(item.id)
? removeFromFavorites(item.id)
: addToFavorites(item)
}
>
<Text style={styles.favoriteButtonText}>
{isFavorite(item.id) ? '♥ Remove' : '♡ Favorite'}
</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>Search with Custom Hooks</Text>
<TextInput
style={styles.searchInput}
placeholder="Search posts..."
value={searchTerm}
onChangeText={setSearchTerm}
/>
<Text style={styles.statsText}>
Favorites: {favorites.length} |
Search results: {searchResults?.length || 0}
</Text>
{loading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Searching...</Text>
</View>
)}
{error && (
<Text style={styles.errorText}>Error: {error}</Text>
)}
{searchResults && (
<FlatList
data={searchResults}
renderItem={renderPost}
keyExtractor={(item) => item.id.toString()}
style={styles.resultsList}
/>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
searchInput: {
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
borderWidth: 1,
borderColor: '#ddd',
fontSize: 16,
marginBottom: 15,
},
statsText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 15,
},
loadingContainer: {
alignItems: 'center',
padding: 20,
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#666',
},
errorText: {
color: '#ff3333',
textAlign: 'center',
fontSize: 16,
padding: 20,
},
resultsList: {
flex: 1,
},
postItem: {
backgroundColor: 'white',
padding: 15,
marginBottom: 10,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
postTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
},
postBody: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 10,
},
favoriteButton: {
backgroundColor: '#e0e0e0',
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 6,
alignSelf: 'flex-start',
},
favoriteButtonActive: {
backgroundColor: '#ff6b6b',
},
favoriteButtonText: {
fontSize: 14,
fontWeight: 'bold',
color: '#333',
},
});
export default SearchApp;
Best Practices and Common Patterns
1. Dependency Arrays
Always be careful with dependency arrays in useEffect, useMemo, and useCallback:
// ❌ Missing dependencies
useEffect(() => {
fetchData(userId);
}, []); // userId should be in dependencies
// ✅ Correct dependencies
useEffect(() => {
fetchData(userId);
}, [userId]);
// ❌ Unnecessary dependencies
const memoizedValue = useMemo(() => {
return expensiveCalculation(data);
}, [data, someUnrelatedValue]); // someUnrelatedValue is unnecessary
// ✅ Only necessary dependencies
const memoizedValue = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
2. State Updates
Use functional updates when the new state depends on the previous state:
// ❌ Direct state dependency
const increment = () => {
setCount(count + 1); // Depends on current count value
};
// ✅ Functional update
const increment = () => {
setCount(prevCount => prevCount + 1); // Safe from stale closures
};
3. Cleanup in useEffect
Always clean up side effects to prevent memory leaks:
useEffect(() => {
const subscription = api.subscribe(handleData);
const timer = setInterval(updateTimer, 1000);
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, []);
4. Custom Hook Naming
Always start custom hook names with “use”:
// ✅ Good naming
const useCounter = (initialValue = 0) => { ... };
const useLocalStorage = (key, defaultValue) => { ... };
const useApi = (url) => { ... };
// ❌ Bad naming
const counter = (initialValue = 0) => { ... };
const localStorage = (key, defaultValue) => { ... };
Performance Considerations
1. Avoid Unnecessary Re-renders
Use memo, useMemo, and useCallback strategically:
// Wrap expensive components with memo
const ExpensiveComponent = memo(({ data, onUpdate }) => {
return (
<View>
{/* Complex rendering logic */}
</View>
);
});
// Memoize expensive calculations
const processedData = useMemo(() => {
return heavyDataProcessing(rawData);
}, [rawData]);
// Memoize callback functions
const handleUpdate = useCallback((id, newValue) => {
updateItem(id, newValue);
}, [updateItem]);
2. Optimize useEffect
Split effects by concerns and optimize dependencies:
// ❌ One large effect
useEffect(() => {
fetchUserData();
setupWebSocket();
trackAnalytics();
}, [userId, socketUrl, analyticsConfig]);
// ✅ Separate effects by concern
useEffect(() => {
fetchUserData();
}, [userId]);
useEffect(() => {
setupWebSocket();
}, [socketUrl]);
useEffect(() => {
trackAnalytics();
}, [analyticsConfig]);
Common Mistakes and How to Avoid Them
1. Infinite Loops in useEffect
// ❌ This will cause infinite loops
useEffect(() => {
setData({ ...data, newProp: 'value' });
}, [data]);
// ✅ Use functional updates or be specific about dependencies
useEffect(() => {
setData(prevData => ({ ...prevData, newProp: 'value' }));
}, []); // Empty dependency if it should run once
// Or be specific about what part of data you care about
useEffect(() => {
if (data.shouldUpdate) {
setData({ ...data, newProp: 'value' });
}
}, [data.shouldUpdate]);
2. Stale Closures
// ❌ Stale closure problem
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // This 'count' might be stale
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency array causes stale closure
// ✅ Use functional updates
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
3. Not Cleaning Up
// ❌ Memory leak - no cleanup
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
// Missing cleanup!
}, []);
// ✅ Proper cleanup
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
return () => clearInterval(timer);
}, []);
Testing Components with Hooks
When testing components that use hooks, you can use React Testing Library:
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import CounterApp from './CounterApp';
describe('CounterApp', () => {
test('increments counter when button is pressed', () => {
const { getByText } = render(<CounterApp />);
const incrementButton = getByText('+');
const countText = getByText('Count: 0');
fireEvent.press(incrementButton);
expect(getByText('Count: 1')).toBeTruthy();
});
test('resets counter to zero', () => {
const { getByText } = render(<CounterApp />);
const incrementButton = getByText('+');
const resetButton = getByText('Reset');
// Increment a few times
fireEvent.press(incrementButton);
fireEvent.press(incrementButton);
expect(getByText('Count: 2')).toBeTruthy();
// Reset
fireEvent.press(resetButton);
expect(getByText('Count: 0')).toBeTruthy();
});
});
Conclusion
React Hooks have revolutionized the way we write React Native applications by providing a cleaner, more intuitive way to manage state and side effects in functional components. The key benefits include:
- Simplified State Management: useState and useReducer make state management straightforward
- Powerful Side Effect Handling: useEffect provides a unified way to handle component lifecycle
- Performance Optimization: useMemo and useCallback help optimize expensive operations
- Code Reusability: Custom hooks allow you to extract and share logic between components
- Better Testing: Functional components with hooks are easier to test
As you continue building React Native applications, hooks will become an essential part of your toolkit. Practice using them in different scenarios, create your own custom hooks, and always keep performance and best practices in mind.
In the next chapter, we’ll explore Lists and Data Display components like FlatList and SectionList, which are crucial for building efficient, scrollable interfaces in React Native applications.
Related Articles:
- React Native vs Flutter in 2024: A Detailed Comparison
- Top 10 React Native Libraries You Must Know in 2024
- React vs React Native: A Deep Dive into Key Differences
- How to Set Up Navigation in React Native : Step-by-Step Guide
- What is Axios? Fetch vs Axios: What’s the Difference?
- React Native Environment Setup: A Beginner’s Step-by-Step Guide
- React Native Web: A Step-by-Step Guide to Cross-Platform Development