DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 11: React Native Hooks

React Native Hooks

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:

  1. Only call hooks at the top level – Don’t call hooks inside loops, conditions, or nested functions
  2. 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:

Scroll to Top