DEV Community

Cover image for Conquering API Overload in React: A Chill Guide for Devs
kevorklepedjian1
kevorklepedjian1

Posted on

Conquering API Overload in React: A Chill Guide for Devs

Building web applications is thrilling, but ever feel like your app is bombarding the server with requests? It's like having an overeager puppy who keeps pawing at the door for attention. This can slow things down for everyone.

Fear not, fellow developer! This guide will equip you with a few battle-tested tactics to keep those API requests under control. We'll be using React's useEffect hook, a trusty tool in your developer arsenal. By following these tips, you'll create a smoother user experience and become a more server-friendly developer (think superhero for servers!).


1. Debouncing: The Efficient Search Technique

Imagine you're creating a search bar for an online store. Every time someone types a character, a request zooms off to the server to find matching items. This can overload the server and create a frustrating experience for the user, like scrambling to find your keys when you're already running behind schedule.

Debouncing comes to the rescue! Think of it like having a helpful friend who says, "Hold on a moment. Let's confirm you actually want to search for 'oatmeal bath bombs' before we flood the server with requests."

Here's the core idea:

  • We use a concept called useRef to remember a "hold on" timer (like a snooze button).

Image description

  • Inside useEffect, we create a function that fetches product results.

  • Whenever the search term changes (as the user types), any existing timer is cleared to prevent unnecessary requests.

  • We set a new timer using setTimeout. This basically says, "Wait half a second (or any chosen delay) before fetching products." This ensures the request only happens after the user stops typing, providing a more responsive search experience.

Code:

import React, { useState, useEffect, useRef } from 'react';

function Search() {
  const [search, setSearch] = useState('');
  const delayTimer = useRef(null);

  useEffect(() => {
    // Cleanup function to clear timer when the component unmounts
    return () => {
      if (delayTimer.current) {
        clearTimeout(delayTimer.current);
      }
    };
  }, []);

  const fetchProducts = async (searchTerm) => {
    // Simulate API call with delay
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const response = { results: ['product A', 'product B', 'product C'] };
    console.log('Search results:', response.results);
    // Update UI with search results
  };

  const debouncedSearch = useRef(
    // Debounce function that delays invoking the provided function until after delay milliseconds have elapsed
    (fn, delay) => {
      if (delayTimer.current) {
        clearTimeout(delayTimer.current);
      }
      delayTimer.current = setTimeout(fn, delay);
    }
  ).current;

  // Function triggered by changes in search term
  const handleSearchChange = (e) => {
    const searchTerm = e.target.value;
    setSearch(searchTerm);
    // Debounce the fetchProducts function call
    debouncedSearch(() => fetchProducts(searchTerm), 500);
  };

  return (
    <div>
      <input
        type="text"
        value={search}
        onChange={handleSearchChange}
        placeholder="Search Products"
      />
    </div>
  );
}

export default Search;
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Reduced server load by minimizing unnecessary requests.
  • Improved user experience with a smoother search process.

2. Throttling: Keeping the Chat Flowing Smoothly

Throttling is another technique for managing user input, but with a twist. Let's say you're building a live chat app where messages are sent as they're typed. Imagine the app suffers from the "Keyboard Warrior Effect," where a flurry of messages overwhelms the server.

Throttling ensures that only one update is sent within a specific timeframe, even if the user types like a lightning bolt. ⚡ Here's how it works:

  • We again use useRef to keep track of a flag that indicates if a request to update the chat is ongoing.
  • Inside useEffect, when the chat message changes, the flag is checked.
  • If the flag is off (meaning no update is in progress), it's flipped on, and the request to update the chat is sent.
  • Once the update is sent, the flag is flipped back off. This approach ensures that a burst of typing doesn't trigger a barrage of update requests, potentially overloading the server.

Benefits:

  • Reduced server load by minimizing unnecessary requests.
  • Improved user experience with a smoother chat flow.
import React, { useState, useEffect, useRef } from 'react';

function ChatBox() {
  const [message, setMessage] = useState('');
  const isSending = useRef(false);

  useEffect(() => {
    if (!isSending.current) {
      isSending.current = true;
      // Simulate sending the message
      sendMessage(message);
      setTimeout(() => {
        isSending.current = false;
      }, 1000); // Throttling time: 1000 milliseconds (1 second)
    }
  }, [message]);

  const sendMessage = (message) => {
    // Simulate sending message
    console.log('Message sent:', message);
    // Actual API call or message sending logic can be placed here
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={message}
        onChange={handleMessageChange}
        placeholder="Type a message..."
      />
    </div>
  );
}

export default ChatBox;
Enter fullscreen mode Exit fullscreen mode

3. Conditional Requests: Fetching Only What You Need

Conditional requests are useful when you only need to fetch data based on specific criteria. For instance, imagine you're building a social media feed that displays posts from the people you follow. Fetching every single post on the platform would be like trying to read every book in the library at once!

Inside useEffect, you can add a condition before fetching new posts. This condition could involve comparing the last fetched post ID with the latest one available on the server. You might only call the server for newer posts (indicated by a higher ID). This approach optimizes requests by preventing unnecessary calls for the same data.

Code:

import React, { useEffect, useState } from 'react';

function PostFeed() {
  const [posts, setPosts] = useState([]);
  const [lastFetchedPostId, setLastFetchedPostId] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchPosts = async () => {
      if (!lastFetchedPostId || isLoading) return; // Skip fetching if there's no lastFetchedPostId or if isLoading is true

      setIsLoading(true);
      try {
        const response = await fetch('/api/posts?lastId=' + lastFetchedPostId);
        const newPosts = await response.json();
        setPosts((prevPosts) => prevPosts.concat(newPosts));
        setLastFetchedPostId(newPosts[newPosts.length - 1]?.id); // Update last ID if newPosts is not empty
      } catch (error) {
        console.error('Error fetching posts:', error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchPosts();
  }, [lastFetchedPostId, isLoading]);

  return (
    <div>
      {/* Render posts */}
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>

      {/* Render loading indicator if isLoading is true */}
      {isLoading && <p>Loading...</p>}
    </div>
  );
}

export default PostFeed;
Enter fullscreen mode Exit fullscreen mode

4. Caching: Remembering What You Saw

Caching involves storing fetched data, like a product list or recent posts, in your component's state or using a library like react-query for more advanced caching. This way, you can avoid redundant requests for data that hasn't changed. It's like having a handy shopping list so you don't keep asking your roommate if they need milk (again!).

Code:

import React, { useEffect, useState } from 'react';

function ProductDetails() {
  const [productId, setProductId] = useState(null);
  const [product, setProduct] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchProduct = async () => {
      try {
        const response = await fetch('/api/products/' + productId);
        if (!response.ok) {
          throw new Error('Failed to fetch product');
        }
        const productData = await response.json();
        setProduct(productData);
        setError(null); // Reset error state if successful
      } catch (error) {
        setError(error.message); // Set error message in case of failure
      }
    };

    if (productId && !product) {
      fetchProduct();
    }
  }, [productId, product]); // Re-fetch product only if ID changes or product is not yet fetched

  // Render product details or error message
  return (
    <div>
      {error ? (
        <p>Error: {error}</p>
      ) : (
        <div>
          {/* Display product details if available */}
          {product && (
            <div>
              <h2>{product.name}</h2>
              <p>{product.description}</p>
              {/* Render other product details */}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default ProductDetails;
Enter fullscreen mode Exit fullscreen mode

Thank you for reading! Now you're equipped to tackle API overload in your React projects. Feel free to experiment with these techniques and explore advanced caching solutions like react-query for even more control. Happy coding, and remember, a happy server is a productive server (and a happy developer too!), Let's connect on Linkedin.

Top comments (0)