Mastering Data Management in React and Next.js: A Practical Guide

Mastering Data Management in React and Next.js: A Practical Guide

Data fetching lies at the heart of any robust web application, and React paired with Next.js offers powerful tools for efficiently managing this process. In this guide, we'll dive into real-world examples of fetching, caching, and revalidating data to help you master these crucial concepts.

1. Fetching Data on the Server: A Practical Example

Let's consider a scenario where we need to fetch user data from an API on the server side using Next.js. We'll use the native fetch API extended by Next.js to configure caching and revalidation behavior.

// pages/user/[id].tsx

import { useRouter } from 'next/router';

const User = ({ user }) => {
  // Access user data fetched on the server
  return (
    <div>
      <h1>{user.name}</h1>
      {/* Display other user details */}
    </div>
  );
};

export async function getServerSideProps({ params }) {
  const res = await fetch(`https://api.example.com/users/${params.id}`);
  const user = await res.json();

  if (!user) {
    return {
      notFound: true,
    };
  }

  return {
    props: { user }, // Will be passed to the page component as props
  };
}

export default User;

In this example:

  • We use getServerSideProps to fetch user data on the server.

  • The fetch request is configured to handle caching and revalidation by Next.js.

2. Caching Data: Optimize Performance

Caching is crucial for preventing unnecessary data refetching. Let's explore a scenario where we want to cache a list of articles fetched from an API.

// pages/articles.tsx

import { useEffect, useState } from 'react';

const Articles = () => {
  const [articles, setArticles] = useState([]);

  useEffect(() => {
    const fetchArticles = async () => {
      const cachedArticles = localStorage.getItem('cachedArticles');

      if (cachedArticles) {
        // Use cached data if available
        setArticles(JSON.parse(cachedArticles));
      }

      // Fetch fresh data and update the cache
      const res = await fetch('https://api.example.com/articles');
      const newArticles = await res.json();
      setArticles(newArticles);
      localStorage.setItem('cachedArticles', JSON.stringify(newArticles));
    };

    fetchArticles();
  }, []);

  return (
    <div>
      <h1>Latest Articles</h1>
      {/* Render the list of articles */}
    </div>
  );
};

export default Articles;

Key points:

  • We use localStorage to cache articles locally.

  • The component first checks for cached data before making a fresh API request.

3. Revalidating Data: Keeping it Fresh

Revalidating is essential when dealing with frequently changing data. Let's explore time-based revalidation in a scenario where we fetch stock prices.

// pages/stocks.tsx

import { useEffect, useState } from 'react';

const Stocks = () => {
  const [stockPrices, setStockPrices] = useState([]);

  useEffect(() => {
    const fetchStockPrices = async () => {
      const res = await fetch('https://api.example.com/stocks', { next: { revalidate: 300 } });
      const newPrices = await res.json();
      setStockPrices(newPrices);
    };

    fetchStockPrices();
  }, []);

  return (
    <div>
      <h1>Stock Prices</h1>
      {/* Render the stock prices */}
    </div>
  );
};

export default Stocks;

Here:

  • We use the next.revalidate option to set a 5-minute cache lifetime.

  • The data will automatically revalidate after the specified time.

4. Opting Out of Data Caching: Fine-tuning Fetch Requests

In certain scenarios, you might need to opt out of caching. Let's explore a case where we fetch real-time data for a live chat application.

// components/ChatMessages.tsx

import { useEffect, useState } from 'react';

const ChatMessages = () => {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const fetchChatMessages = async () => {
      const res = await fetch('https://api.example.com/messages', { cache: 'no-store' });
      const newMessages = await res.json();
      setMessages(newMessages);
    };

    fetchChatMessages();
  }, []);

  return (
    <div>
      {/* Render the chat messages */}
    </div>
  );
};

export default ChatMessages;

Key considerations:

  • We use cache: 'no-store' to ensure real-time data fetching on every request.

5. Fetching Data on the Client: Leveraging Axios

For client-side data fetching, let's explore using the Axios library instead of SWR to fetch and cache GitHub repositories.

// pages/github-repos.tsx

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

const GitHubRepos = () => {
  const [repos, setRepos] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchGitHubRepos = async () => {
      try {
        const response = await axios.get('https://api.github.com/users/octocat/repos');
        setRepos(response.data);
      } catch (err) {
        setError('Error fetching data');
      }
    };

    fetchGitHubRepos();
  }, []);

  return (
    <div>
      <h1>GitHub Repositories</h1>
      {error ? <div>{error}</div> : /* Render the list of repositories */}
    </div>
  );
};

export default GitHubRepos;

In this updated example:

  • We replace the useSWR hook with the useState and useEffect hooks for manual data fetching.

  • Axios is used to make the HTTP request, providing more control over the fetching process.

  • The fetched data is stored in the repos state, and any errors are captured in the error state.

By making these changes, you have the flexibility to choose the data fetching library that best fits your needs, and in this case, Axios provides a robust and widely-used alternative to SWR.

Follow me on : Github Linkedin