Deep Dive Debouncing in React Native (Javascript)

Photo by AltumCode on Unsplash

Deep Dive Debouncing in React Native (Javascript)

·

9 min read

Debouncing is a programming practice used to ensure that time-consuming tasks do not fire so often, making them more efficient. It limits the rate at which a function gets called. Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called.

function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// Function to make API call
function searchAPI(query) {
  console.log(`Searching for: ${query}`);
  // Actual API call would go here
}

// Debounced version of searchAPI
const debouncedSearch = debounce(searchAPI, 300);

// Usage
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

Let's break down the debounce function step by step:

  1. Function Definition:
    function debounce(func, delay) {
    This creates a higher-order function named debounce that takes two parameters:

    • func: The function we want to debounce

    • delay: The time in milliseconds to wait before calling func

  2. Closure Variable:
    let timeoutId;
    This variable is defined in the outer function's scope. It will be used to store the ID of the setTimeout, allowing us to clear it if needed.

  3. Returned Function:
    return function (...args) {
    The debounce function returns a new function. This returned function is what will actually be called when we use the debounced version of our original function. The ...args syntax allows it to accept any number of arguments.

  4. Clearing Previous Timeout:
    clearTimeout(timeoutId);
    This line cancels any previously scheduled execution of the function. If this debounced function is called again before the delay has passed, we want to cancel the previous call and start the timer again.

  1. Setting New Timeout:
    timeoutId = setTimeout(() => {

    func.apply(this, args);

    }, delay);
    This creates a new timeout that will execute after the specified delay. When the timeout occurs, it calls the original function (func) with .apply(this, args). This ensures that the function is called with the correct this context and all the arguments that were passed to the debounced function.

Here's how it works in practice:

  1. When the debounced function is called for the first time, it sets a timeout to call the original function after the specified delay.

  2. If the debounced function is called again before the delay has passed, it clears the previous timeout and sets a new one.

  3. This process repeats until the debounced function is not called for the duration of the delay.

  4. Once the delay has passed without any new calls, the original function is finally executed.

This implementation effectively "groups" multiple function calls within the delay period into a single call, which is especially useful for functions that are expensive to run or that cause visible updates to the UI.

For example, if you debounce a search function with a 300ms delay, and a user types "hello" quickly, the search will only happen once, 300ms after they stop typing, instead of five separate times for each letter.

  1. func.apply():

    • apply() is a method available on all JavaScript functions.

    • It allows you to call a function with a given this value and arguments provided as an array (or an array-like object).

  2. this:

    • In this context, this refers to the execution context in which the debounced function is called.

    • By using this, we ensure that the original function (func) is called with the same context it would have if it wasn't debounced.

  3. args:

    • args is an array-like object containing all the arguments passed to the debounced function.

    • It comes from the ...args parameter in the returned function's definition.

The use of apply() here serves two important purposes:

  1. It preserves the this context, which is crucial if the original function relies on this.

  2. It allows passing all arguments that were given to the debounced function, regardless of how many there are.

An alternative to apply() could be the spread operator.

Q. What is the primary purpose of debouncing in React Native applications?

The primary purpose of debouncing in React Native apps is to optimize them, by preventing the multiple api calls without any use, Debounce use to delay function calling, and we can call function after a delay of given duration, Example : In search functionality we can use debouncing.

Q. Write a basic debounce function in JavaScript that can be used in a React Native component.

function debounce(func, delay){
    let timeOutId;

    return function(...args){
        clearTimeout(timeOutId);
        timeOutId = setTimeout(()=>{
            func.apply(this,args)
    },delay)
  }
}

Q: How would you implement debouncing on a TextInput component to delay API calls while the user is typing?

import React, { useState } from 'react';

const DebouncedTextInput = () => {
  const [inputValue, setInputValue] = useState('');

  // Debounce the API call with a 500ms delay
  const debouncedApiCall = debounce((value) => {
    // Simulate API call
    console.log('Making API call with:', value);
  }, 500);

  const handleChange = (e) => {
    setInputValue(e.target.value);
    debouncedApiCall(e.target.value);  // Call the debounced function
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
        placeholder="Type something..."
      />
      <p>Current Value: {inputValue}</p>
    </div>
  );
};

export default DebouncedTextInput;

Q. In a React Native app, you have a list that updates based on user input. How can debouncing improve the performance of this feature?

In a React Native app where a list updates based on user input, debouncing can significantly improve performance by reducing unnecessary renders and preventing the app from making multiple rapid API requests or updates. Without debouncing, every keystroke or input change could trigger a state update, causing the list to re-render or re-fetch data excessively. This can degrade performance, especially with large data sets or slow network connections.

Q. How Debouncing Improves Performance:

1.Reduces Frequent State Updates:

Every input change typically triggers a re-render of the component or triggers a state update, leading to performance bottlenecks. Debouncing ensures that the state update (or API call) only happens after the user has stopped typing for a certain delay period (e.g., 300ms–500ms). This minimizes the number of updates and re-renders, leading to smoother user interactions.

2.Limits API Calls:

If the list data is fetched from an API based on user input (e.g., filtering or searching), without debouncing, an API call might be made after every keystroke. This can overwhelm the server and cause unnecessary network requests. Debouncing ensures that the API call is only made once the user has finished typing, reducing the number of API requests and improving both app and server performance.

3.Improves Responsiveness:

By delaying the updates, debouncing allows the app to stay responsive to user inputs. Without debouncing, continuous updates or re-renders can lead to noticeable lags or UI freezing, especially on lower-end devices. Debouncing smooths out the experience by reducing the frequency of these operations.

import React, { useState } from 'react';

import { View, TextInput, FlatList, Text, StyleSheet } from 'react-native';

import debounce from 'lodash.debounce'; // You can install this from lodash for convenience

const DebouncedList = ({ fetchData }) => {

  const [query, setQuery] = useState('');

  const [data, setData] = useState([]);

  // Debounced function to fetch or filter data based on the input

  const debouncedFetchData = debounce((input) => {

    fetchData(input).then((results) => setData(results));  // Simulate an API call or filtering logic

  }, 500);  // 500ms debounce delay

  const handleChange = (text) => {

    setQuery(text);

    debouncedFetchData(text);  // Only fetch/filter data after user has stopped typing

  };

  return (

    <View style={styles.container}>

      <TextInput

        style={styles.input}

        value={query}

        onChangeText={handleChange}

        placeholder="Search..."

      />

      <FlatList

        data={data}

        keyExtractor={(item) => item.id}

        renderItem={({ item }) => <Text style={styles.item}>{item.name}</Text>}

      />

    </View>

  );

};

const styles = StyleSheet.create({

  container: { flex: 1, padding: 16 },

  input: { height: 40, borderColor: 'gray', borderWidth: 1, marginBottom: 16, paddingHorizontal: 8 },

  item: { padding: 10, borderBottomWidth: 1, borderBottomColor: '#ccc' },

});

export default DebouncedList;

Q: Create a custom hook called useDebounce that can be used to debounce any value in a functional component.

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 300);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search operation
    }
  }, [debouncedSearchTerm]);

  return (
    <TextInput
      value={searchTerm}
      onChangeText={setSearchTerm}
      placeholder="Search..."
    />
  );
}

Q. Practical Application: Implement a debounced search function that triggers an API call 300ms after the user stops typing.

import React, { useState, useCallback } from 'react';
import { TextInput } from 'react-native';
import debounce from 'lodash.debounce';

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');

  const debouncedSearch = useCallback(
    debounce((text) => {
      // Perform API call
      console.log('Searching API for:', text);
    }, 300),
    []
  );

  const handleSearch = (text) => {
    setSearchTerm(text);
    debouncedSearch(text);
  };

  return (
    <TextInput
      value={searchTerm}
      onChangeText={handleSearch}
      placeholder="Search..."
    />
  );
}

Q. Error Handling: Modify a debounce function to handle and propagate errors that might occur in the debounced function.

function debounceWithErrorHandling(func, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);

    return new Promise((resolve, reject) => {
      timeoutId = setTimeout(() => {
        try {
          const result = func.apply(this, args);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      }, delay);
    });
  };
}

// Usage
const debouncedSearch = debounceWithErrorHandling(async (term) => {
  if (term.length < 3) throw new Error('Search term too short');
  // Perform API call
}, 300);

// In a component
try {
  await debouncedSearch(searchTerm);
} catch (error) {
  console.error('Search error:', error);
}

Q. Cancellation: Implement a debounce function that includes a method to cancel the debounced operation if needed.

function debounceWithCancel(func, delay) {
  let timeoutId;

  const debouncedFunc = function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };

  debouncedFunc.cancel = function () {
    clearTimeout(timeoutId);
  };

  return debouncedFunc;
}

// Usage
const debouncedSearch = debounceWithCancel((term) => {
  console.log('Searching for:', term);
}, 300);

// In a component
const handleSearch = (text) => {
  debouncedSearch(text);
};

// To cancel (e.g., when component unmounts)
useEffect(() => {
  return () => debouncedSearch.cancel();
}, []);

Q. React Native Navigation: Debounce navigation actions to prevent rapid, unintended screen changes.

import { useMemo } from 'react';
import { useNavigation } from '@react-navigation/native';
import debounce from 'lodash.debounce';

function useDebounceNavigation() {
  const navigation = useNavigation();

  const debouncedNavigation = useMemo(
    () => ({
      navigate: debounce(navigation.navigate, 300, { leading: true, trailing: false }),
      goBack: debounce(navigation.goBack, 300, { leading: true, trailing: false }),
    }),
    [navigation]
  );

  return debouncedNavigation;
}

// Usage in a component
function MyComponent() {
  const debouncedNav = useDebounceNavigation();

  return (
    <Button
      title="Go to Details"
      onPress={() => debouncedNav.navigate('Details')}
    />
  );
}
  • Optimization: To optimize a debounce function for memory usage in a long-running React Native application:

  • Use a shared debounce function: Instead of creating a new debounce function for each component, create a utility file with shared debounce functions for common delays.

  • Clean up timeouts: Always clear timeouts when the component unmounts or when the debounced function is no longer needed.

  • Use weak maps for memoization: If you're memoizing debounced functions, use WeakMap to allow garbage collection of unused functions.

  • Avoid anonymous functions: Use named functions or class methods instead of creating new anonymous functions on each render.

  • Debounce at the lowest level possible: Debounce individual operations rather than entire component re-renders when possible.

Example of a memory-efficient debounce utility:

const debounceMap = new WeakMap();

export function efficientDebounce(func, delay, key = func) {
  if (!debounceMap.has(key)) {
    let timeoutId;
    const debouncedFunc = (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func(...args), delay);
    };
    debouncedFunc.cancel = () => clearTimeout(timeoutId);
    debounceMap.set(key, debouncedFunc);
  }
  return debounceMap.get(key);
}

// Usage
const debouncedSearch = efficientDebounce(searchAPI, 300);

// Clean up
useEffect(() => {
  return () => {
    debouncedSearch.cancel();
    debounceMap.delete(searchAPI);
  };
}, []);