Static Assets and Frontend Integration

Using React or Vue with Django

Modern frontend frameworks like React and Vue enable building sophisticated single-page applications (SPAs) that communicate with Django backends through APIs. This chapter covers various integration patterns, from simple component embedding to full SPA architectures, including authentication, state management, and deployment strategies.

Using React or Vue with Django

Modern frontend frameworks like React and Vue enable building sophisticated single-page applications (SPAs) that communicate with Django backends through APIs. This chapter covers various integration patterns, from simple component embedding to full SPA architectures, including authentication, state management, and deployment strategies.

Integration Patterns

Pattern 1: Component Islands

Embed React/Vue components within Django templates for specific interactive features.

Pattern 2: Hybrid Architecture

Use Django templates for some pages and SPA for others, sharing authentication and data.

Pattern 3: Full SPA with API Backend

Complete separation where Django serves only APIs and React/Vue handles all frontend rendering.

React Integration with Django

Setting Up React with Vite

// package.json
{
  "name": "django-react-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.8.0",
    "axios": "^1.6.0",
    "@tanstack/react-query": "^5.0.0",
    "zustand": "^4.4.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.2.0",
    "vite": "^5.0.0",
    "eslint": "^8.55.0",
    "eslint-plugin-react": "^7.33.0",
    "eslint-plugin-react-hooks": "^4.6.0"
  }
}
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  
  build: {
    outDir: 'staticfiles/dist',
    manifest: true,
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'frontend/js/main.jsx'),
        components: resolve(__dirname, 'frontend/js/components.jsx')
      }
    }
  },
  
  server: {
    port: 3000,
    proxy: {
      '/api': 'http://localhost:8000',
      '/admin': 'http://localhost:8000'
    }
  },
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'frontend'),
      '@components': resolve(__dirname, 'frontend/js/components'),
      '@hooks': resolve(__dirname, 'frontend/js/hooks'),
      '@utils': resolve(__dirname, 'frontend/js/utils'),
      '@store': resolve(__dirname, 'frontend/js/store')
    }
  }
});

React Component Structure

// frontend/js/main.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './styles/main.css';

// Create React Query client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      retry: 1
    }
  }
});

// Mount React app
const container = document.getElementById('react-app');
if (container) {
  const root = createRoot(container);
  root.render(
    <React.StrictMode>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </QueryClientProvider>
    </React.StrictMode>
  );
}
// frontend/js/App.jsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { useAuthStore } from '@store/authStore';
import Navigation from '@components/Navigation';
import Home from '@components/pages/Home';
import Blog from '@components/pages/Blog';
import BlogPost from '@components/pages/BlogPost';
import Dashboard from '@components/pages/Dashboard';
import Login from '@components/auth/Login';
import ProtectedRoute from '@components/auth/ProtectedRoute';

function App() {
  const { user, loading } = useAuthStore();

  if (loading) {
    return <div className="loading">Loading...</div>;
  }

  return (
    <div className="app">
      <Navigation user={user} />
      
      <main className="main-content">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/blog" element={<Blog />} />
          <Route path="/blog/:slug" element={<BlogPost />} />
          <Route path="/login" element={<Login />} />
          <Route 
            path="/dashboard" 
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            } 
          />
        </Routes>
      </main>
    </div>
  );
}

export default App;

Authentication Integration

// frontend/js/store/authStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import api from '@utils/api';

export const useAuthStore = create(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      loading: true,
      
      // Initialize auth state
      initialize: async () => {
        const token = get().token;
        if (token) {
          try {
            const user = await api.get('/auth/user/');
            set({ user: user.data, loading: false });
          } catch (error) {
            set({ user: null, token: null, loading: false });
          }
        } else {
          set({ loading: false });
        }
      },
      
      // Login
      login: async (credentials) => {
        try {
          const response = await api.post('/auth/login/', credentials);
          const { user, token } = response.data;
          
          set({ user, token });
          api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
          
          return { success: true };
        } catch (error) {
          return { 
            success: false, 
            error: error.response?.data?.message || 'Login failed' 
          };
        }
      },
      
      // Logout
      logout: async () => {
        try {
          await api.post('/auth/logout/');
        } catch (error) {
          // Continue with logout even if API call fails
        }
        
        set({ user: null, token: null });
        delete api.defaults.headers.common['Authorization'];
      },
      
      // Update user
      updateUser: (userData) => {
        set({ user: { ...get().user, ...userData } });
      }
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({ token: state.token })
    }
  )
);

// Initialize auth on app start
useAuthStore.getState().initialize();
// frontend/js/components/auth/Login.jsx
import React, { useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '@store/authStore';

function Login() {
  const [credentials, setCredentials] = useState({ username: '', password: '' });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { user, login } = useAuthStore();
  const location = useLocation();
  
  // Redirect if already logged in
  if (user) {
    const from = location.state?.from?.pathname || '/dashboard';
    return <Navigate to={from} replace />;
  }
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    
    const result = await login(credentials);
    
    if (result.success) {
      const from = location.state?.from?.pathname || '/dashboard';
      window.location.href = from; // Full page redirect
    } else {
      setError(result.error);
    }
    
    setLoading(false);
  };
  
  return (
    <div className="login-container">
      <form onSubmit={handleSubmit} className="login-form">
        <h2>Login</h2>
        
        {error && <div className="error-message">{error}</div>}
        
        <div className="form-group">
          <label htmlFor="username">Username:</label>
          <input
            type="text"
            id="username"
            value={credentials.username}
            onChange={(e) => setCredentials(prev => ({
              ...prev,
              username: e.target.value
            }))}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="password">Password:</label>
          <input
            type="password"
            id="password"
            value={credentials.password}
            onChange={(e) => setCredentials(prev => ({
              ...prev,
              password: e.target.value
            }))}
            required
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Logging in...' : 'Login'}
        </button>
      </form>
    </div>
  );
}

export default Login;

API Integration with React Query

// frontend/js/utils/api.js
import axios from 'axios';

// Create axios instance
const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json'
  }
});

// Request interceptor for CSRF token
api.interceptors.request.use((config) => {
  const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
  if (csrfToken) {
    config.headers['X-CSRFToken'] = csrfToken;
  }
  return config;
});

// Response interceptor for error handling
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle unauthorized access
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;
// frontend/js/hooks/useBlog.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@utils/api';

export function useBlogPosts() {
  return useQuery({
    queryKey: ['blogPosts'],
    queryFn: async () => {
      const response = await api.get('/blog/posts/');
      return response.data;
    }
  });
}

export function useBlogPost(slug) {
  return useQuery({
    queryKey: ['blogPost', slug],
    queryFn: async () => {
      const response = await api.get(`/blog/posts/${slug}/`);
      return response.data;
    },
    enabled: !!slug
  });
}

export function useCreateBlogPost() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (postData) => {
      const response = await api.post('/blog/posts/', postData);
      return response.data;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['blogPosts'] });
    }
  });
}

export function useUpdateBlogPost() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async ({ id, data }) => {
      const response = await api.put(`/blog/posts/${id}/`, data);
      return response.data;
    },
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: ['blogPosts'] });
      queryClient.invalidateQueries({ queryKey: ['blogPost', data.slug] });
    }
  });
}

Vue Integration with Django

Setting Up Vue with Vite

// package.json
{
  "name": "django-vue-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0",
    "axios": "^1.6.0",
    "@vueuse/core": "^10.7.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.5.0",
    "vite": "^5.0.0",
    "eslint": "^8.55.0",
    "eslint-plugin-vue": "^9.19.0"
  }
}
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  
  build: {
    outDir: 'staticfiles/dist',
    manifest: true,
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'frontend/js/main.js'),
        components: resolve(__dirname, 'frontend/js/components.js')
      }
    }
  },
  
  server: {
    port: 3000,
    proxy: {
      '/api': 'http://localhost:8000'
    }
  },
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'frontend'),
      '@components': resolve(__dirname, 'frontend/js/components'),
      '@composables': resolve(__dirname, 'frontend/js/composables'),
      '@stores': resolve(__dirname, 'frontend/js/stores')
    }
  }
});

Vue Application Structure

// frontend/js/main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia } from 'pinia';
import App from './App.vue';
import routes from './routes';
import './styles/main.css';

// Create router
const router = createRouter({
  history: createWebHistory(),
  routes
});

// Create Pinia store
const pinia = createPinia();

// Create and mount app
const app = createApp(App);
app.use(router);
app.use(pinia);

// Mount to DOM
const container = document.getElementById('vue-app');
if (container) {
  app.mount(container);
}
<!-- frontend/js/App.vue -->
<template>
  <div class="app">
    <Navigation :user="user" @logout="handleLogout" />
    
    <main class="main-content">
      <router-view />
    </main>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';
import { useAuthStore } from '@stores/auth';
import Navigation from '@components/Navigation.vue';

const authStore = useAuthStore();
const { user, initialize } = authStore;

const handleLogout = async () => {
  await authStore.logout();
};

onMounted(() => {
  initialize();
});
</script>

Vue State Management with Pinia

// frontend/js/stores/auth.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import api from '@/utils/api';

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref(null);
  const token = ref(localStorage.getItem('auth-token'));
  const loading = ref(true);
  
  // Getters
  const isAuthenticated = computed(() => !!user.value);
  
  // Actions
  const initialize = async () => {
    if (token.value) {
      try {
        const response = await api.get('/auth/user/');
        user.value = response.data;
      } catch (error) {
        token.value = null;
        localStorage.removeItem('auth-token');
      }
    }
    loading.value = false;
  };
  
  const login = async (credentials) => {
    try {
      const response = await api.post('/auth/login/', credentials);
      const { user: userData, token: authToken } = response.data;
      
      user.value = userData;
      token.value = authToken;
      localStorage.setItem('auth-token', authToken);
      
      // Set default authorization header
      api.defaults.headers.common['Authorization'] = `Bearer ${authToken}`;
      
      return { success: true };
    } catch (error) {
      return {
        success: false,
        error: error.response?.data?.message || 'Login failed'
      };
    }
  };
  
  const logout = async () => {
    try {
      await api.post('/auth/logout/');
    } catch (error) {
      // Continue with logout
    }
    
    user.value = null;
    token.value = null;
    localStorage.removeItem('auth-token');
    delete api.defaults.headers.common['Authorization'];
  };
  
  return {
    user,
    token,
    loading,
    isAuthenticated,
    initialize,
    login,
    logout
  };
});

Vue Composables for API Integration

// frontend/js/composables/useBlog.js
import { ref, computed } from 'vue';
import { useAsyncState } from '@vueuse/core';
import api from '@/utils/api';

export function useBlogPosts() {
  const { state: posts, isLoading, error, execute } = useAsyncState(
    () => api.get('/blog/posts/').then(res => res.data),
    [],
    { immediate: true }
  );
  
  const refresh = () => execute();
  
  return {
    posts,
    loading: isLoading,
    error,
    refresh
  };
}

export function useBlogPost(slug) {
  const { state: post, isLoading, error, execute } = useAsyncState(
    () => api.get(`/blog/posts/${slug}/`).then(res => res.data),
    null,
    { immediate: false }
  );
  
  const load = () => execute();
  
  return {
    post,
    loading: isLoading,
    error,
    load
  };
}

export function useBlogMutations() {
  const creating = ref(false);
  const updating = ref(false);
  const deleting = ref(false);
  
  const createPost = async (postData) => {
    creating.value = true;
    try {
      const response = await api.post('/blog/posts/', postData);
      return { success: true, data: response.data };
    } catch (error) {
      return { success: false, error: error.response?.data };
    } finally {
      creating.value = false;
    }
  };
  
  const updatePost = async (id, postData) => {
    updating.value = true;
    try {
      const response = await api.put(`/blog/posts/${id}/`, postData);
      return { success: true, data: response.data };
    } catch (error) {
      return { success: false, error: error.response?.data };
    } finally {
      updating.value = false;
    }
  };
  
  const deletePost = async (id) => {
    deleting.value = true;
    try {
      await api.delete(`/blog/posts/${id}/`);
      return { success: true };
    } catch (error) {
      return { success: false, error: error.response?.data };
    } finally {
      deleting.value = false;
    }
  };
  
  return {
    creating,
    updating,
    deleting,
    createPost,
    updatePost,
    deletePost
  };
}

Django Backend Configuration

API Views for Frontend Integration

# views/api.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from .models import BlogPost
from .serializers import BlogPostSerializer, UserSerializer

class AuthViewSet(viewsets.ViewSet):
    """Authentication endpoints for frontend."""
    
    def create(self, request):
        """Login endpoint."""
        username = request.data.get('username')
        password = request.data.get('password')
        
        if not username or not password:
            return Response(
                {'message': 'Username and password required'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        user = authenticate(username=username, password=password)
        if user:
            login(request, user)
            
            # Generate or get token (if using token auth)
            from rest_framework.authtoken.models import Token
            token, created = Token.objects.get_or_create(user=user)
            
            return Response({
                'user': UserSerializer(user).data,
                'token': token.key
            })
        
        return Response(
            {'message': 'Invalid credentials'},
            status=status.HTTP_401_UNAUTHORIZED
        )
    
    @action(detail=False, methods=['post'])
    def logout(self, request):
        """Logout endpoint."""
        logout(request)
        return Response({'message': 'Logged out successfully'})
    
    @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
    def user(self, request):
        """Get current user info."""
        return Response(UserSerializer(request.user).data)

class BlogPostViewSet(viewsets.ModelViewSet):
    """Blog post CRUD operations."""
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    lookup_field = 'slug'
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

URL Configuration

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views.api import AuthViewSet, BlogPostViewSet

# API router
router = DefaultRouter()
router.register(r'auth', AuthViewSet, basename='auth')
router.register(r'blog/posts', BlogPostViewSet)

urlpatterns = [
    # API endpoints
    path('api/', include(router.urls)),
    
    # Frontend routes (catch-all for SPA)
    path('', include('frontend.urls')),
]

Frontend URL Handling

# frontend/urls.py
from django.urls import path, re_path
from django.views.generic import TemplateView

urlpatterns = [
    # Serve React/Vue app for all frontend routes
    re_path(r'^(?!api/).*$', TemplateView.as_view(template_name='index.html')),
]
<!-- templates/index.html -->
{% load vite_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Django + React/Vue App</title>
    {% csrf_token %}
    {% vite_hmr %}
    {% vite_asset 'frontend/js/main.js' %}
</head>
<body>
    <div id="react-app"></div>
    <!-- or -->
    <div id="vue-app"></div>
</body>
</html>

Component Islands Pattern

Embedding React Components in Django Templates

// frontend/js/components.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import CommentSection from '@components/CommentSection';
import BlogPostForm from '@components/BlogPostForm';
import UserProfile from '@components/UserProfile';

const queryClient = new QueryClient();

// Component registry
const components = {
  CommentSection,
  BlogPostForm,
  UserProfile
};

// Mount components based on data attributes
document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('[data-react-component]').forEach(element => {
    const componentName = element.dataset.reactComponent;
    const props = JSON.parse(element.dataset.reactProps || '{}');
    
    const Component = components[componentName];
    if (Component) {
      const root = createRoot(element);
      root.render(
        <QueryClientProvider client={queryClient}>
          <Component {...props} />
        </QueryClientProvider>
      );
    }
  });
});
<!-- templates/blog/post_detail.html -->
{% extends 'base.html' %}
{% load vite_tags %}

{% block content %}
<article class="blog-post">
    <h1>{{ post.title }}</h1>
    <div class="post-content">
        {{ post.content|safe }}
    </div>
    
    <!-- Embed React component -->
    <div 
        data-react-component="CommentSection"
        data-react-props='{"postId": {{ post.id }}, "postSlug": "{{ post.slug }}"}'
    ></div>
</article>
{% endblock %}

{% block extra_js %}
    {% vite_asset 'frontend/js/components.js' %}
{% endblock %}

Production Deployment

Build Configuration

# docker-compose.yml
version: '3.8'

services:
  frontend-builder:
    image: node:18-alpine
    working_dir: /app
    volumes:
      - .:/app
    command: >
      sh -c "npm ci && npm run build"
  
  web:
    build: .
    ports:
      - "8000:8000"
    depends_on:
      - frontend-builder
    environment:
      - DEBUG=False
      - VITE_DEV_MODE=False

Nginx Configuration

# nginx.conf
server {
    listen 80;
    server_name example.com;
    
    # Serve static files
    location /static/ {
        alias /app/staticfiles/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # API requests to Django
    location /api/ {
        proxy_pass http://django:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # Admin requests to Django
    location /admin/ {
        proxy_pass http://django:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # SPA fallback - serve index.html for all other routes
    location / {
        try_files $uri $uri/ /index.html;
        root /app/staticfiles/dist;
    }
}

Integrating React or Vue with Django enables building modern, interactive web applications that leverage Django's robust backend capabilities with cutting-edge frontend technologies. Choose the integration pattern that best fits your project requirements: component islands for progressive enhancement, hybrid architecture for mixed rendering needs, or full SPA for maximum interactivity. Proper authentication handling, state management, and build configuration ensure a seamless development experience and production-ready deployment.