Fullstack Django React/Next.js

Learn how to improve the speed of a full-stack project built with Django and React/NextJS - Tutorial provided by AppSeed.

Make your Fullstack Django and React/Next.js Application faster with Caching
Make your Fullstack Django and React/Next.js Application faster with Caching

When your application starts getting slower because of big-size data fetched from the database, an API, or even internet connection issues, plan strategies to increase fetching speed. One way of doing that is to implement caching, and if you have a full-stack application, having a powerful caching strategy on the front end and the back end is a great way to make your users happy.
In this article, we will explore caching in a full-stack application in Django using the default caching feature implemented within the framework and React/Next.js with SWR, a caching-oriented fetching library.

🕮 Content provided by KOLAWOLE MANGABO, a passionate developer and author of Full Stack Django and React (best seller)

What is caching?

Before diving into how to cache data, we need to understand what is caching, how it works, what are some caching strategies and when to not use caching.

Caching in software computing involves storing data, files, and information in a temporary storage area, known as a cache, for quicker access. This concept is vividly illustrated through an anecdote by Peter Chester, who challenged an audience with a complex math question. Initially met with silence, the audience quickly responded with the correct answer after it was first calculated, demonstrating the principle of caching: the initial effort of calculation (or data retrieval) is performed once, with the result stored for faster future access.

In the realm of social media platforms, where vast numbers of users frequently request the same content, caching proves particularly beneficial. For instance, caching a trending tweet or a celebrity's Instagram post prevents repetitive database queries, thereby enhancing efficiency and user experience.

Cache Invalidation Strategies

Before delving into the challenges of caching, it's important to understand the strategies used to keep cached content up-to-date. These include:

  • Purging: This involves the immediate removal of content from the cache. When the content is requested again, it is re-fetched and stored anew.
  • Refreshing: In this approach, the cache is updated by fetching the latest content from the server, replacing the outdated version in the cache.
  • Banning: Here, content is marked as blacklisted in the cache. When a request matches the blacklisted content, it prompts fetching and updating the cache with the latest content.

Challenges and Considerations in Caching

While caching offers significant benefits such as faster load times, reduced bandwidth usage, fewer database queries, and decreased downtime, it also introduces certain complexities:

  • Potential for Outdated Information: Users might miss out on the latest updates due to the nature of cached content.
  • Complexity in Cache Invalidation: Implementing effective cache invalidation strategies is crucial to ensure that users always have access to the most current data.

Now that we understand how caching works, we can move to the first backend implementation of caching, in Django.

Django

Django is a powerful web Python framework that comes with caching built-in. This is great, as the configuration for caching is pretty straightforward, and comes with many clues you can take into account if you are looking to implement much more complex caching strategies.

The first step is to configure the caching storage, which can be done with Redis, or if you are in local development, in the in-memory storage MemcachedCache.

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

Once it is done, you can use DRF extensions like drf-extensions which offer caching mechanisms. You can use @cache_response decorator on your viewsets.

from rest_framework_extensions.cache.decorators import cache_response

class MyViewSet(viewsets.ViewSet):
    
    @cache_response(timeout=60*15)
    def list(self, request, *args, **kwargs):
        # Your logic here

Or if you are working with Django REST views, you can use cache_page from Django.

from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from rest_framework.views import APIView

class MyApiView(APIView):

    @method_decorator(cache_page(60*15))  # Cache for 15 minutes
    def get(self, request, *args, **kwargs):
        # Your GET method logic
Note : If your application is large enough and you have many views or viewsets that need caching, make your life easier by implementing an abstract class from which every viewsets for example, that need caching on the list action, can use caching.
from rest_framework.viewsets import ViewSet
from rest_framework_extensions.cache.decorators import cache_response

class CachedListViewSet(ViewSet):
    """
    An abstract base ViewSet class that adds caching to the list action.
    """

    cache_timeout = 60 * 15  # Default cache timeout of 15 minutes

    @cache_response(timeout=cache_timeout)
    def list(self, request, *args, **kwargs):
        # Default list action logic (can be overridden in subclasses)
        return super().list(request, *args, **kwargs)

And then you can use it like :

from myapp.viewsets import CachedListViewSet
from myapp.models import MyModel
from myapp.serializers import MyModelSerializer
from rest_framework import mixins, viewsets

class MyModelViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, CachedListViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

    # If you need a different cache timeout for this viewset, override it here
    # cache_timeout = 60 * 10  # Cache for 10 minutes

Now that we know how to implement caching in a Django REST application, how to invalidate the cache?
Well, cache invalidation comes in handy every time there is a creation, modification, or deletion of one of the objects cached. Let's see how it works in case of a viewset.

First, define a cache key as an attribute in your viewset class. This key will be used for caching and invalidating the list view.

from rest_framework.viewsets import ModelViewSet

class MyModelViewSet(ModelViewSet):
    cache_key = 'my_model_list_cache'
    # ... list, create, update, destroy methods ...

Next, override the create, update, and destroy methods in your viewset to invalidate the cache after these operations.

from django.core.cache import cache
from rest_framework.viewsets import ModelViewSet

class MyModelViewSet(ModelViewSet):
    cache_key = 'my_model_list_cache'

    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        cache.delete(self.cache_key)  # Invalidate cache
        return response

    def update(self, request, *args, **kwargs):
        response = super().update(request, *args, **kwargs)
        cache.delete(self.cache_key)  # Invalidate cache
        return response

    def destroy(self, request, *args, **kwargs):
        response = super().destroy(request, *args, **kwargs)
        cache.delete(self.cache_key)  # Invalidate cache
        return response

Great! Now that we understand caching in Django, let's move to caching on the frontend application built with React or Next.js.

Caching in React/Next.js with SWR

SWR (Stale-While-Revalidate) is a React hooks library that efficiently handles data fetching and caching, following a strategy that balances showing cached (stale) data and fetching updated information (revalidate).

Using SWR for Data Fetching and Caching

import useSWR from 'swr';

function MyComponent() {
  const { data, error } = useSWR('/api/data', fetcher);
  // ...
}

In this example, useSWR is used to fetch and cache data from an API endpoint.

Mutating and Updating the Cache

SWR uses keys (typically API URLs) for caching. The mutate function is used to revalidate and update the cache.

import useSWR, { mutate } from 'swr';

function MyComponent() {
  // ...

  function updateData(newData) {
    mutate('/api/data', newData, false); // Optimistically update the cache
    // Additional logic to update data
  }
}

Direct Cache Manipulation (Use with Caution)

Direct manipulation of the SWR cache is possible but should be done cautiously.

import { cache } from 'swr';

function MyComponent() {
  const key = '/api/data';
  const data = cache.get(key);
  // Modify data directly
  cache.set(key, modifiedData);
}

Revalidation Strategies in SWR

SWR offers several revalidation strategies:

  • revalidateOnFocus: Automatically revalidates data when the window gains focus.
  • refreshInterval: Periodically revalidates data at a specified interval.
  • revalidateOnReconnect: Revalidates data when the browser regains a network connection.
  • revalidateIfStale: Revalidates data if it's considered stale.
const { data, error } = useSWR('/api/data', fetcher, {
  revalidateOnFocus: true,
  refreshInterval: 5000,
  revalidateOnReconnect: true,
  revalidateIfStale: true
});

In this setup, SWR will revalidate the data based on the specified strategies, ensuring the data is up-to-date.


In short, caching is crucial for faster and more efficient full-stack applications. It speeds up load times, cuts down on data usage, and reduces database queries, making users happier. By using Django's built-in caching and SWR in React/Next.js, you can boost performance and keep your application running smoothly.

For more resources, feel free to acces: