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.
Embed React/Vue components within Django templates for specific interactive features.
Use Django templates for some pages and SPA for others, sharing authentication and data.
Complete separation where Django serves only APIs and React/Vue handles all frontend rendering.
// 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')
}
}
});
// 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;
// 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;
// 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] });
}
});
}
// 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')
}
}
});
// 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>
// 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
};
});
// 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
};
}
# 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)
# 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/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>
// 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 %}
# 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.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.
Using Build Tools like Vite or Webpack
Modern build tools transform the frontend development experience by providing features like hot module replacement, code splitting, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. This chapter covers integrating Vite and Webpack with Django to create efficient development workflows and optimized production builds.
Managing CORS
Cross-Origin Resource Sharing (CORS) is essential when building modern web applications where frontend and backend are served from different origins. This chapter covers CORS configuration in Django, security best practices, and troubleshooting common issues when integrating with frontend frameworks.