Connecting Python Backends to React

Module 23: Web Frameworks II (Python) - Friday: Lecture 3

Introduction to Frontend-Backend Architecture

Modern web applications often use a decoupled architecture where the frontend and backend are separate applications that communicate via APIs. This approach offers several advantages, including independent development, scalability, and technology flexibility.

Real-world analogy: Think of this like a restaurant where the kitchen (backend) and dining area (frontend) are separate spaces with a clear interface between them. The waitstaff takes orders from customers and brings them to the kitchen, then delivers the prepared food back to the customers - they are the API that connects the two domains.

graph LR subgraph "Frontend (React)" A[Components] --> B[State Management] B --> C[API Client] end subgraph "Backend (Python)" D[API Endpoints] --> E[Business Logic] E --> F[Database] end C <-->|HTTP/JSON| D style A fill:#61dafb,stroke:#333,stroke-width:2px style B fill:#61dafb,stroke:#333,stroke-width:2px style C fill:#61dafb,stroke:#333,stroke-width:2px style D fill:#4b8bbe,stroke:#333,stroke-width:2px style E fill:#4b8bbe,stroke:#333,stroke-width:2px style F fill:#4b8bbe,stroke:#333,stroke-width:2px

Benefits of Decoupled Architecture

Challenges to Consider

Setting Up the Backend for React Integration

Before connecting your Python backend to React, you need to configure it properly to handle cross-origin requests and provide appropriate responses.

Configuring CORS in Flask


# Install the Flask-CORS extension
# pip install flask-cors

# app.py
from flask import Flask, jsonify
from flask_restful import Api
from flask_cors import CORS

app = Flask(__name__)
# Enable CORS for all routes
CORS(app)
# Or specify allowed origins
# CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})

api = Api(app)

# ... your API resources and routes

if __name__ == '__main__':
    app.run(debug=True)
            

Configuring CORS in Django


# Install the django-cors-headers package
# pip install django-cors-headers

# settings.py
INSTALLED_APPS = [
    # ...
    'corsheaders',
    # ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ... other middleware
]

# Allow all origins (development only)
CORS_ALLOW_ALL_ORIGINS = True

# Or specify allowed origins (more secure)
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "https://yourdomain.com",
]

# Optional: Allow credentials (cookies, authorization headers)
CORS_ALLOW_CREDENTIALS = True

# Optional: Specify which headers can be used
CORS_ALLOW_HEADERS = [
    "accept",
    "authorization",
    "content-type",
    "user-agent",
    "x-csrftoken",
    "x-requested-with",
]
            

Security note: Always restrict CORS to specific origins in production environments. Allowing all origins (Access-Control-Allow-Origin: *) can expose your API to security risks.

Consistent Response Format

For a seamless frontend experience, it's important to maintain a consistent response format throughout your API:


# Flask example
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({
                'status': 'error',
                'message': 'User not found',
                'data': None
            }), 404
        
        return jsonify({
            'status': 'success',
            'message': 'User retrieved successfully',
            'data': user.to_dict()
        }), 200
    except Exception as e:
        return jsonify({
            'status': 'error',
            'message': str(e),
            'data': None
        }), 500

# Django REST Framework example
from rest_framework.views import exception_handler
from rest_framework.response import Response

def custom_exception_handler(exc, context):
    # Call REST framework's default exception handler first
    response = exception_handler(exc, context)
    
    # Now add the custom response structure
    if response is not None:
        response.data = {
            'status': 'error',
            'message': response.data.get('detail', 'An error occurred'),
            'data': None
        }
    
    return response

# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'yourapp.utils.custom_exception_handler',
}

# views.py
from rest_framework.response import Response

class UserViewSet(viewsets.ModelViewSet):
    # ... other code
    
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response({
            'status': 'success',
            'message': 'User retrieved successfully',
            'data': serializer.data
        })
            

Authentication Options

Authentication is a critical consideration when connecting your React frontend to a Python backend. Here are some common approaches:

graph TD A[Authentication Approaches] --> B[Session-based Auth] A --> C[Token-based Auth] A --> D[JWT Auth] A --> E[OAuth] B --> F[Cookies/Sessions] C --> G[API Keys/Tokens] D --> H[JSON Web Tokens] E --> I[Third-party Auth] F --> J["+ Built-in to frameworks
- CORS challenges"] G --> K["+ Simple to implement
- Limited metadata"] H --> L["+ Self-contained
+ Stateless
- Size and security concerns"] I --> M["+ Delegated auth
- Implementation complexity"]

Token-based Authentication

A popular and straightforward approach for React-Python API communication:


# Django REST Framework with Token Authentication
# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework.authtoken',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

# urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    # ...
    path('api/token/', obtain_auth_token, name='api_token'),
]
            

React side implementation:


// Login component (simplified)
import React, { useState } from 'react';
import axios from 'axios';

const Login = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  
  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post('http://localhost:8000/api/token/', {
        username,
        password
      });
      
      // Store the token in localStorage
      localStorage.setItem('token', response.data.token);
      
      // Redirect or update app state
      // ...
    } catch (error) {
      console.error('Login failed', error);
    }
  };
  
  return (
    <form onSubmit={handleLogin}>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
};

// Authenticated API request
const fetchData = async () => {
  const token = localStorage.getItem('token');
  
  try {
    const response = await axios.get('http://localhost:8000/api/data/', {
      headers: {
        'Authorization': `Token ${token}`
      }
    });
    
    return response.data;
  } catch (error) {
    console.error('Request failed', error);
    // Handle expired tokens or auth errors
    if (error.response && error.response.status === 401) {
      localStorage.removeItem('token');
      // Redirect to login
    }
  }
};
            

JWT Authentication

JSON Web Tokens provide a more flexible token-based authentication system:


# Django with Simple JWT
# pip install djangorestframework-simplejwt

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

# urls.py
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    # ...
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
            

React implementation with refresh tokens:


// Authentication service
import axios from 'axios';
import jwt_decode from 'jwt-decode';

const API_URL = 'http://localhost:8000/api';

class AuthService {
  // Login and obtain tokens
  static async login(username, password) {
    try {
      const response = await axios.post(`${API_URL}/token/`, {
        username,
        password
      });
      
      const { access, refresh } = response.data;
      
      // Store tokens
      this.setTokens(access, refresh);
      
      return true;
    } catch (error) {
      console.error('Login failed:', error);
      return false;
    }
  }
  
  // Store tokens
  static setTokens(access, refresh) {
    localStorage.setItem('access_token', access);
    localStorage.setItem('refresh_token', refresh);
  }
  
  // Get access token, refreshing if necessary
  static async getAccessToken() {
    const accessToken = localStorage.getItem('access_token');
    const refreshToken = localStorage.getItem('refresh_token');
    
    if (!accessToken || !refreshToken) {
      return null;
    }
    
    // Check if token is expired
    try {
      const decoded = jwt_decode(accessToken);
      const currentTime = Date.now() / 1000;
      
      if (decoded.exp > currentTime) {
        // Token still valid
        return accessToken;
      }
      
      // Token expired, try to refresh
      const response = await axios.post(`${API_URL}/token/refresh/`, {
        refresh: refreshToken
      });
      
      const newAccessToken = response.data.access;
      localStorage.setItem('access_token', newAccessToken);
      
      return newAccessToken;
    } catch (error) {
      // Refresh failed, clear tokens
      this.logout();
      return null;
    }
  }
  
  // Logout
  static logout() {
    localStorage.removeItem('access_token');
    localStorage.removeItem('refresh_token');
  }
  
  // Check if user is authenticated
  static isAuthenticated() {
    return !!localStorage.getItem('access_token');
  }
}

// API service with token handling
class ApiService {
  static async get(endpoint) {
    try {
      const token = await AuthService.getAccessToken();
      
      if (!token) {
        throw new Error('Not authenticated');
      }
      
      const response = await axios.get(`${API_URL}${endpoint}`, {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      });
      
      return response.data;
    } catch (error) {
      console.error(`GET ${endpoint} failed:`, error);
      throw error;
    }
  }
  
  // Similar methods for post, put, delete...
}

export { AuthService, ApiService };
            

React Setup for API Communication

On the React side, you'll need to set up HTTP clients and implement proper state management to interact with your Python backend.

Setting Up Axios

Axios is a popular HTTP client for React applications:


// Install axios
// npm install axios

// api/client.js - Setup a client instance
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'http://localhost:8000/api',
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add request interceptor for authentication
apiClient.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// Add response interceptor for error handling
apiClient.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    // Handle 401 Unauthorized errors
    if (error.response && error.response.status === 401) {
      // Clear token and redirect to login
      localStorage.removeItem('token');
      window.location = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;
            

Creating API Services

Organize your API calls into service modules:


// api/services/userService.js
import apiClient from '../client';

export default {
  async getUsers() {
    return apiClient.get('/users/');
  },
  
  async getUser(id) {
    return apiClient.get(`/users/${id}/`);
  },
  
  async createUser(data) {
    return apiClient.post('/users/', data);
  },
  
  async updateUser(id, data) {
    return apiClient.put(`/users/${id}/`, data);
  },
  
  async deleteUser(id) {
    return apiClient.delete(`/users/${id}/`);
  }
};

// api/services/authService.js
import apiClient from '../client';

export default {
  async login(credentials) {
    const response = await apiClient.post('/token/', credentials);
    if (response.data.token) {
      localStorage.setItem('token', response.data.token);
    }
    return response.data;
  },
  
  async register(userData) {
    return apiClient.post('/register/', userData);
  },
  
  logout() {
    localStorage.removeItem('token');
  },
  
  getCurrentUser() {
    return JSON.parse(localStorage.getItem('user'));
  },
  
  isAuthenticated() {
    return !!localStorage.getItem('token');
  }
};
            

Using API Services in Components


// components/UserList.js
import React, { useState, useEffect } from 'react';
import userService from '../api/services/userService';

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await userService.getUsers();
        setUsers(response.data.data); // Assuming your API returns { data: [...] }
        setError(null);
      } catch (err) {
        setError('Failed to fetch users');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUsers();
  }, []);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div className="user-list">
      <h2>Users</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;
            

API State Management

Managing API state effectively is crucial for building responsive React applications. There are several approaches to this challenge:

Local Component State

The simplest approach, as shown in the previous example, uses React's useState and useEffect hooks in each component.

Context API for API State

For sharing API state across components without prop drilling:


// context/UserContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import userService from '../api/services/userService';

const UserContext = createContext();

export const UserProvider = ({ children }) => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchUsers = async () => {
    try {
      setLoading(true);
      const response = await userService.getUsers();
      setUsers(response.data.data);
      setError(null);
    } catch (err) {
      setError('Failed to fetch users');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };
  
  const createUser = async (userData) => {
    try {
      setLoading(true);
      const response = await userService.createUser(userData);
      setUsers([...users, response.data.data]);
      return response.data.data;
    } catch (err) {
      setError('Failed to create user');
      console.error(err);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  // Similar methods for update, delete
  
  // Load users when the context is first used
  useEffect(() => {
    fetchUsers();
  }, []);
  
  return (
    <UserContext.Provider
      value={{
        users,
        loading,
        error,
        fetchUsers,
        createUser,
        // other methods
      }}
    >
      {children}
    </UserContext.Provider>
  );
};

// Custom hook for using the context
export const useUsers = () => {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUsers must be used within a UserProvider');
  }
  return context;
};
            

Using the context in components:


// App.js
import { UserProvider } from './context/UserContext';

function App() {
  return (
    <UserProvider>
      <div className="App">
        {/* Components that use user data */}
        <UserList />
        <UserForm />
      </div>
    </UserProvider>
  );
}

// components/UserList.js
import { useUsers } from '../context/UserContext';

const UserList = () => {
  const { users, loading, error, fetchUsers } = useUsers();
  
  // Now you can use these values without fetching directly
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <button onClick={fetchUsers}>Refresh</button>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};
            

Using Redux with API Calls

For larger applications, Redux provides a robust state management solution:


// Install Redux packages
// npm install redux react-redux redux-thunk @reduxjs/toolkit

// redux/slices/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import userService from '../../api/services/userService';

// Async thunks for API operations
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, { rejectWithValue }) => {
    try {
      const response = await userService.getUsers();
      return response.data.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

export const createUser = createAsyncThunk(
  'users/createUser',
  async (userData, { rejectWithValue }) => {
    try {
      const response = await userService.createUser(userData);
      return response.data.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// User slice
const userSlice = createSlice({
  name: 'users',
  initialState: {
    items: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null,
  },
  reducers: {
    // Additional reducers if needed
  },
  extraReducers: (builder) => {
    builder
      // Handle fetchUsers
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload || 'Failed to fetch users';
      })
      
      // Handle createUser
      .addCase(createUser.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(createUser.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items.push(action.payload);
      })
      .addCase(createUser.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload || 'Failed to create user';
      });
  },
});

export default userSlice.reducer;

// redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';

export const store = configureStore({
  reducer: {
    users: userReducer,
    // other reducers
  },
});
            

Using Redux in components:


// components/UserList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from '../redux/slices/userSlice';

const UserList = () => {
  const dispatch = useDispatch();
  const { items, status, error } = useSelector((state) => state.users);
  
  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [status, dispatch]);
  
  if (status === 'loading') return <div>Loading...</div>;
  if (status === 'failed') return <div>Error: {error}</div>;
  
  return (
    <div>
      <button onClick={() => dispatch(fetchUsers())}>Refresh</button>
      <ul>
        {items.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};
            

Using React Query

React Query is a powerful library specifically designed for managing API state:


// Install React Query
// npm install react-query

// Setup in App.js
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        {/* Your components */}
        <UserList />
        <UserForm />
      </div>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

// hooks/useUserData.js
import { useQuery, useMutation, useQueryClient } from 'react-query';
import userService from '../api/services/userService';

export function useUsers() {
  return useQuery('users', async () => {
    const response = await userService.getUsers();
    return response.data.data;
  });
}

export function useUser(id) {
  return useQuery(['user', id], async () => {
    const response = await userService.getUser(id);
    return response.data.data;
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();
  
  return useMutation(
    (userData) => userService.createUser(userData),
    {
      onSuccess: () => {
        // Invalidate the users query to trigger a refetch
        queryClient.invalidateQueries('users');
      },
    }
  );
}

// components/UserList.js
import React from 'react';
import { useUsers } from '../hooks/useUserData';

const UserList = () => {
  const { data: users, isLoading, error } = useUsers();
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

// components/UserForm.js
import React, { useState } from 'react';
import { useCreateUser } from '../hooks/useUserData';

const UserForm = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  
  const createUser = useCreateUser();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    createUser.mutate({ name, email });
    setName('');
    setEmail('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <button type="submit" disabled={createUser.isLoading}>
        {createUser.isLoading ? 'Adding...' : 'Add User'}
      </button>
      {createUser.error && <div>Error: {createUser.error.message}</div>}
    </form>
  );
};
            

Handling Forms and Data Submission

Forms are a critical part of most web applications. Here's how to handle form submission from React to a Python backend:

Basic Form Handling


// components/ProductForm.js
import React, { useState } from 'react';
import apiClient from '../api/client';

const ProductForm = () => {
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [description, setDescription] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Basic validation
    if (!name || !price) {
      setError('Name and price are required');
      return;
    }
    
    try {
      setLoading(true);
      setError(null);
      setSuccess(false);
      
      const response = await apiClient.post('/products/', {
        name,
        price: parseFloat(price),
        description
      });
      
      // Clear form
      setName('');
      setPrice('');
      setDescription('');
      setSuccess(true);
      
      console.log('Product created:', response.data);
    } catch (err) {
      setError(err.response?.data?.message || 'Failed to create product');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="product-form">
      <h2>Add New Product</h2>
      
      {success && (
        <div className="success-message">
          Product created successfully!
        </div>
      )}
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="name">Product Name</label>
          <input
            type="text"
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
            disabled={loading}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="price">Price</label>
          <input
            type="number"
            id="price"
            value={price}
            onChange={(e) => setPrice(e.target.value)}
            disabled={loading}
            step="0.01"
            min="0"
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="description">Description</label>
          <textarea
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            disabled={loading}
            rows="4"
          ></textarea>
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Adding...' : 'Add Product'}
        </button>
      </form>
    </div>
  );
};

export default ProductForm;
            

Using Formik and Yup for Form Management

For more complex forms, Formik with Yup validation is a powerful combination:


// Install Formik and Yup
// npm install formik yup

// components/UserFormWithFormik.js
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import apiClient from '../api/client';

// Validation schema
const UserSchema = Yup.object().shape({
  username: Yup.string()
    .min(3, 'Username must be at least 3 characters')
    .max(20, 'Username must be less than 20 characters')
    .required('Username is required'),
  email: Yup.string()
    .email('Invalid email address')
    .required('Email is required'),
  password: Yup.string()
    .min(8, 'Password must be at least 8 characters')
    .matches(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
      'Password must contain at least one uppercase letter, one lowercase letter, and one number'
    )
    .required('Password is required'),
  confirmPassword: Yup.string()
    .oneOf([Yup.ref('password'), null], 'Passwords must match')
    .required('Confirm password is required'),
  agreeToTerms: Yup.boolean()
    .oneOf([true], 'You must accept the terms and conditions')
});

const UserForm = () => {
  const handleSubmit = async (values, { setSubmitting, resetForm, setStatus }) => {
    try {
      // Remove confirmPassword before sending to API
      const { confirmPassword, ...userData } = values;
      
      const response = await apiClient.post('/users/', userData);
      
      resetForm();
      setStatus({ success: true, message: 'User created successfully!' });
      console.log('Success:', response.data);
    } catch (error) {
      setStatus({ 
        success: false, 
        message: error.response?.data?.message || 'An error occurred' 
      });
      console.error('Error:', error);
    } finally {
      setSubmitting(false);
    }
  };
  
  return (
    <div className="user-form">
      <h2>Create User Account</h2>
      
      <Formik
        initialValues={{
          username: '',
          email: '',
          password: '',
          confirmPassword: '',
          agreeToTerms: false
        }}
        validationSchema={UserSchema}
        onSubmit={handleSubmit}
      >
        {({ isSubmitting, status }) => (
          <Form>
            {status && status.message && (
              <div className={status.success ? 'success-message' : 'error-message'}>
                {status.message}
              </div>
            )}
            
            <div className="form-group">
              <label htmlFor="username">Username</label>
              <Field type="text" name="username" id="username" />
              <ErrorMessage name="username" component="div" className="error" />
            </div>
            
            <div className="form-group">
              <label htmlFor="email">Email</label>
              <Field type="email" name="email" id="email" />
              <ErrorMessage name="email" component="div" className="error" />
            </div>
            
            <div className="form-group">
              <label htmlFor="password">Password</label>
              <Field type="password" name="password" id="password" />
              <ErrorMessage name="password" component="div" className="error" />
            </div>
            
            <div className="form-group">
              <label htmlFor="confirmPassword">Confirm Password</label>
              <Field type="password" name="confirmPassword" id="confirmPassword" />
              <ErrorMessage name="confirmPassword" component="div" className="error" />
            </div>
            
            <div className="form-group checkbox">
              <Field type="checkbox" name="agreeToTerms" id="agreeToTerms" />
              <label htmlFor="agreeToTerms">
                I agree to the terms and conditions
              </label>
              <ErrorMessage name="agreeToTerms" component="div" className="error" />
            </div>
            
            <button type="submit" disabled={isSubmitting}>
              {isSubmitting ? 'Creating...' : 'Create Account'}
            </button>
          </Form>
        )}
      </Formik>
    </div>
  );
};

export default UserForm;
            

Handling File Uploads

Uploading files from React to a Python backend requires special handling:


// components/FileUploadForm.js
import React, { useState } from 'react';
import apiClient from '../api/client';

const FileUploadForm = () => {
  const [file, setFile] = useState(null);
  const [title, setTitle] = useState('');
  const [loading, setLoading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  
  const handleFileChange = (e) => {
    setFile(e.target.files[0]);
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!file) {
      setError('Please select a file to upload');
      return;
    }
    
    // Create FormData object
    const formData = new FormData();
    formData.append('file', file);
    formData.append('title', title);
    
    try {
      setLoading(true);
      setError(null);
      setSuccess(false);
      setProgress(0);
      
      // Upload with progress tracking
      const response = await apiClient.post('/documents/', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        },
      });
      
      setSuccess(true);
      setTitle('');
      setFile(null);
      
      console.log('Upload successful:', response.data);
    } catch (err) {
      setError(err.response?.data?.message || 'Upload failed');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="upload-form">
      <h2>Upload Document</h2>
      
      {success && (
        <div className="success-message">
          File uploaded successfully!
        </div>
      )}
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="title">Document Title</label>
          <input
            type="text"
            id="title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            disabled={loading}
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="file">Select File</label>
          <input
            type="file"
            id="file"
            onChange={handleFileChange}
            disabled={loading}
          />
        </div>
        
        {loading && (
          <div className="progress-bar">
            <div 
              className="progress" 
              style={{ width: `${progress}%` }}
            ></div>
            <span>{progress}%</span>
          </div>
        )}
        
        <button type="submit" disabled={loading}>
          {loading ? 'Uploading...' : 'Upload'}
        </button>
      </form>
    </div>
  );
};

export default FileUploadForm;
            

Python backend (Django) implementation for file upload:


# Django views.py
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Document
from .serializers import DocumentSerializer

class DocumentUploadView(APIView):
    parser_classes = (MultiPartParser, FormParser)
    
    def post(self, request, format=None):
        serializer = DocumentSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(uploaded_by=request.user)
            return Response(
                {'message': 'File uploaded successfully', 'data': serializer.data},
                status=status.HTTP_201_CREATED
            )
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# models.py
class Document(models.Model):
    title = models.CharField(max_length=255)
    file = models.FileField(upload_to='documents/')
    uploaded_at = models.DateTimeField(auto_now_add=True)
    uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE)
    
    def __str__(self):
        return self.title

# serializers.py
class DocumentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Document
        fields = ('id', 'title', 'file', 'uploaded_at')
        read_only_fields = ('uploaded_at',)
            

Real-Time Communication

For real-time features like chat, notifications, or live updates, you can connect Python backends with React using WebSockets.

Django Channels with React

Django Channels extends Django to handle WebSockets alongside HTTP:


# Django Channels setup
# pip install channels daphne

# settings.py
INSTALLED_APPS = [
    # ...
    'channels',
]

ASGI_APPLICATION = 'your_project.asgi.application'
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing  # Your WebSocket routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})

# chat/consumers.py
from channels.generic.websocket import AsyncJsonWebsocketConsumer

class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = f'chat_{self.room_name}'
        
        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()
    
    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )
    
    # Receive message from WebSocket
    async def receive_json(self, content):
        message = content['message']
        username = content['username']
        
        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'username': username
            }
        )
    
    # Receive message from room group
    async def chat_message(self, event):
        message = event['message']
        username = event['username']
        
        # Send message to WebSocket
        await self.send_json({
            'message': message,
            'username': username
        })

# chat/routing.py
from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
            

React implementation with WebSockets:


// components/ChatRoom.js
import React, { useState, useEffect, useRef } from 'react';
import useAuth from '../hooks/useAuth';

const ChatRoom = ({ roomName }) => {
  const [messages, setMessages] = useState([]);
  const [message, setMessage] = useState('');
  const [connected, setConnected] = useState(false);
  const { user } = useAuth();
  const socketRef = useRef(null);
  const messagesEndRef = useRef(null);
  
  // Auto-scroll to bottom of messages
  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };
  
  useEffect(() => {
    scrollToBottom();
  }, [messages]);
  
  useEffect(() => {
    // Create WebSocket connection
    const socket = new WebSocket(
      `ws://${window.location.host}/ws/chat/${roomName}/`
    );
    
    socketRef.current = socket;
    
    // Connection opened
    socket.addEventListener('open', (event) => {
      console.log('Connected to the chat server!');
      setConnected(true);
    });
    
    // Listen for messages
    socket.addEventListener('message', (event) => {
      const data = JSON.parse(event.data);
      setMessages((prevMessages) => [...prevMessages, data]);
    });
    
    // Connection closed
    socket.addEventListener('close', (event) => {
      console.log('Disconnected from the chat server');
      setConnected(false);
    });
    
    // Clean up on unmount
    return () => {
      socket.close();
    };
  }, [roomName]);
  
  const sendMessage = (e) => {
    e.preventDefault();
    
    if (message.trim() && connected) {
      socketRef.current.send(
        JSON.stringify({
          message: message,
          username: user.username,
        })
      );
      setMessage('');
    }
  };
  
  return (
    <div className="chat-room">
      <h2>Chat Room: {roomName}</h2>
      
      <div className="connection-status">
        Status: {connected ? (
          <span className="connected">Connected</span>
        ) : (
          <span className="disconnected">Disconnected</span>
        )}
      </div>
      
      <div className="message-list">
        {messages.map((msg, index) => (
          <div 
            key={index} 
            className={`message ${msg.username === user.username ? 'own-message' : ''}`}
          >
            <div className="message-username">{msg.username}</div>
            <div className="message-content">{msg.message}</div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      
      <form onSubmit={sendMessage} className="message-form">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type a message..."
          disabled={!connected}
        />
        <button type="submit" disabled={!connected || !message.trim()}>
          Send
        </button>
      </form>
    </div>
  );
};

export default ChatRoom;
            

Socket.IO with Flask

For Flask applications, Socket.IO provides real-time communication:


# pip install flask-socketio

# app.py
from flask import Flask, render_template
from flask_socketio import SocketIO, emit, join_room, leave_room
from flask_cors import CORS

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")

@app.route('/')
def index():
    return "Flask Socket.IO Server"

@socketio.on('connect')
def handle_connect():
    print('Client connected')

@socketio.on('disconnect')
def handle_disconnect():
    print('Client disconnected')

@socketio.on('join')
def on_join(data):
    username = data['username']
    room = data['room']
    join_room(room)
    emit('message', {'username': 'System', 'message': f'{username} has joined the room.'}, to=room)

@socketio.on('leave')
def on_leave(data):
    username = data['username']
    room = data['room']
    leave_room(room)
    emit('message', {'username': 'System', 'message': f'{username} has left the room.'}, to=room)

@socketio.on('message')
def handle_message(data):
    room = data['room']
    emit('message', {
        'username': data['username'],
        'message': data['message']
    }, to=room)

if __name__ == '__main__':
    socketio.run(app, debug=True)
            

React implementation with Socket.IO client:


// npm install socket.io-client

// components/FlaskChatRoom.js
import React, { useState, useEffect, useRef } from 'react';
import io from 'socket.io-client';
import useAuth from '../hooks/useAuth';

const FlaskChatRoom = ({ roomName }) => {
  const [messages, setMessages] = useState([]);
  const [message, setMessage] = useState('');
  const [connected, setConnected] = useState(false);
  const { user } = useAuth();
  const socketRef = useRef(null);
  const messagesEndRef = useRef(null);
  
  useEffect(() => {
    // Connect to the Socket.IO server
    const socket = io('http://localhost:5000');
    socketRef.current = socket;
    
    // Connection events
    socket.on('connect', () => {
      setConnected(true);
      
      // Join the room
      socket.emit('join', {
        username: user.username,
        room: roomName
      });
    });
    
    socket.on('disconnect', () => {
      setConnected(false);
    });
    
    // Listen for messages
    socket.on('message', (data) => {
      setMessages((prevMessages) => [...prevMessages, data]);
    });
    
    // Clean up on unmount
    return () => {
      // Leave the room before disconnecting
      socket.emit('leave', {
        username: user.username,
        room: roomName
      });
      socket.disconnect();
    };
  }, [roomName, user.username]);
  
  // Scroll to bottom when messages change
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  const sendMessage = (e) => {
    e.preventDefault();
    
    if (message.trim() && connected) {
      socketRef.current.emit('message', {
        username: user.username,
        message: message,
        room: roomName
      });
      setMessage('');
    }
  };
  
  return (
    <div className="chat-room">
      <h2>Chat Room: {roomName}</h2>
      
      <div className="connection-status">
        Status: {connected ? (
          <span className="connected">Connected</span>
        ) : (
          <span className="disconnected">Disconnected</span>
        )}
      </div>
      
      <div className="message-list">
        {messages.map((msg, index) => (
          <div 
            key={index} 
            className={`message ${msg.username === user.username ? 'own-message' : 
                      msg.username === 'System' ? 'system-message' : ''}`}
          >
            <div className="message-username">{msg.username}</div>
            <div className="message-content">{msg.message}</div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      
      <form onSubmit={sendMessage} className="message-form">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type a message..."
          disabled={!connected}
        />
        <button type="submit" disabled={!connected || !message.trim()}>
          Send
        </button>
      </form>
    </div>
  );
};

export default FlaskChatRoom;
            

Deployment Considerations

When deploying a React frontend with a Python backend, several approaches are available:

Separate Deployment

Deploy the React app and Python API separately:

graph TD A[React Frontend] -->|API Requests| B[Python Backend] C[Static Host
e.g. Netlify, Vercel] --> A D[API Server
e.g. Heroku, AWS] --> B style A fill:#61dafb,stroke:#333,stroke-width:2px style B fill:#4b8bbe,stroke:#333,stroke-width:2px style C fill:#f9f9f9,stroke:#333,stroke-width:2px style D fill:#f9f9f9,stroke:#333,stroke-width:2px

Benefits:

Considerations:

Static Files Served by Python Backend

Build React and serve it through your Python application:

graph TD A[Python Web Server] --> B[Static Files
from React Build] A --> C[API Endpoints] style A fill:#4b8bbe,stroke:#333,stroke-width:2px style B fill:#61dafb,stroke:#333,stroke-width:2px style C fill:#4b8bbe,stroke:#333,stroke-width:2px

Django example:


# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'frontend/build'),  # React build folder
        ],
        # ...
    },
]

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'frontend/build/static'),
]

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

urlpatterns = [
    # API URLs
    path('api/users/', views.UserList.as_view()),
    # ...
    
    # Serve React's index.html for all other routes
    re_path(r'^(?!api/).*$', TemplateView.as_view(template_name='index.html')),
]
            

Flask example:


from flask import Flask, send_from_directory

app = Flask(__name__, static_folder='frontend/build/static', template_folder='frontend/build')

# API routes
@app.route('/api/users')
def users():
    # ...
    return jsonify(users)

# Serve React App
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    else:
        return send_from_directory(app.template_folder, 'index.html')
            

Docker Compose for Development and Production

Use Docker Compose to manage both services together:


# docker-compose.yml
version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    depends_on:
      - backend
    environment:
      - REACT_APP_API_URL=http://localhost:8000/api
    
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app
    
  db:
    image: postgres:13
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=app
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
            

React Dockerfile:


# Development Dockerfile
FROM node:14

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["npm", "start"]

# Production Dockerfile
FROM node:14 as build

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
            

Django Dockerfile:


FROM python:3.9

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
            

Best Practices and Common Pitfalls

Best Practices

Common Pitfalls

Practical Example: Task Management Application

Let's explore a complete example connecting a React frontend to a Django REST Framework backend for a task management application.

Backend: Django REST Framework

Project structure:


backend/
  ├── requirements.txt
  ├── manage.py
  ├── taskmanager/
  │   ├── __init__.py
  │   ├── settings.py
  │   ├── urls.py
  │   ├── asgi.py
  │   └── wsgi.py
  └── tasks/
      ├── __init__.py
      ├── admin.py
      ├── apps.py
      ├── models.py
      ├── serializers.py
      ├── views.py
      └── urls.py
            

Key backend files:


# tasks/models.py
from django.db import models
from django.contrib.auth.models import User

class Task(models.Model):
    STATUS_CHOICES = (
        ('pending', 'Pending'),
        ('in_progress', 'In Progress'),
        ('completed', 'Completed'),
    )
    
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    due_date = models.DateField(null=True, blank=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return self.title

# tasks/serializers.py
from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    
    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'status', 'due_date', 
                 'owner', 'created_at', 'updated_at']
        read_only_fields = ['created_at', 'updated_at']

# tasks/views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from .models import Task
from .serializers import TaskSerializer

class IsOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [permissions.IsAuthenticated, IsOwner]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['status', 'due_date']
    search_fields = ['title', 'description']
    ordering_fields = ['due_date', 'created_at', 'updated_at']
    
    def get_queryset(self):
        return Task.objects.filter(owner=self.request.user)
    
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)
    
    @action(detail=False, methods=['get'])
    def summary(self, request):
        queryset = self.get_queryset()
        pending_count = queryset.filter(status='pending').count()
        in_progress_count = queryset.filter(status='in_progress').count()
        completed_count = queryset.filter(status='completed').count()
        
        return Response({
            'pending': pending_count,
            'in_progress': in_progress_count,
            'completed': completed_count,
            'total': queryset.count()
        })

# tasks/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

router = DefaultRouter()
router.register(r'tasks', TaskViewSet, basename='task')

urlpatterns = [
    path('', include(router.urls)),
]

# taskmanager/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('tasks.urls')),
    path('api/token/', obtain_auth_token, name='api_token'),
    path('api-auth/', include('rest_framework.urls')),
]
            

Frontend: React with Context API

Project structure:


frontend/
  ├── package.json
  ├── public/
  └── src/
      ├── App.js
      ├── index.js
      ├── api/
      │   ├── client.js
      │   └── tasks.js
      ├── components/
      │   ├── Layout/
      │   ├── Task/
      │   └── Auth/
      ├── context/
      │   ├── AuthContext.js
      │   └── TaskContext.js
      └── pages/
          ├── Dashboard.js
          ├── TaskList.js
          ├── TaskForm.js
          └── Login.js
            

Key frontend files:


// api/client.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8000/api',
  headers: {
    'Content-Type': 'application/json',
  },
});

apiClient.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Token ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response && error.response.status === 401) {
      localStorage.removeItem('token');
      window.location = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;

// api/tasks.js
import apiClient from './client';

export const getTasks = async (filters = {}) => {
  const queryParams = new URLSearchParams();
  
  Object.entries(filters).forEach(([key, value]) => {
    if (value) queryParams.append(key, value);
  });
  
  const response = await apiClient.get(`/tasks/?${queryParams}`);
  return response.data;
};

export const getTask = async (id) => {
  const response = await apiClient.get(`/tasks/${id}/`);
  return response.data;
};

export const createTask = async (taskData) => {
  const response = await apiClient.post('/tasks/', taskData);
  return response.data;
};

export const updateTask = async (id, taskData) => {
  const response = await apiClient.put(`/tasks/${id}/`, taskData);
  return response.data;
};

export const deleteTask = async (id) => {
  await apiClient.delete(`/tasks/${id}/`);
  return id;
};

export const getTaskSummary = async () => {
  const response = await apiClient.get('/tasks/summary/');
  return response.data;
};

// context/AuthContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import apiClient from '../api/client';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const checkUser = async () => {
      const token = localStorage.getItem('token');
      
      if (token) {
        try {
          // Get user profile using token
          const response = await apiClient.get('/users/me/');
          setUser(response.data);
        } catch (error) {
          console.error('Error fetching user:', error);
          localStorage.removeItem('token');
        }
      }
      
      setLoading(false);
    };
    
    checkUser();
  }, []);
  
  const login = async (username, password) => {
    try {
      const response = await apiClient.post('/token/', { username, password });
      localStorage.setItem('token', response.data.token);
      
      // Get user profile after login
      const userResponse = await apiClient.get('/users/me/');
      setUser(userResponse.data);
      
      return true;
    } catch (error) {
      console.error('Login error:', error);
      return false;
    }
  };
  
  const logout = () => {
    localStorage.removeItem('token');
    setUser(null);
  };
  
  const isAuthenticated = () => !!user;
  
  return (
    <AuthContext.Provider
      value={{
        user,
        loading,
        login,
        logout,
        isAuthenticated,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

// context/TaskContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import * as taskApi from '../api/tasks';

const TaskContext = createContext();

export const TaskProvider = ({ children }) => {
  const [tasks, setTasks] = useState([]);
  const [summary, setSummary] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [filters, setFilters] = useState({
    status: '',
    search: '',
    ordering: '-created_at'
  });
  
  const fetchTasks = async () => {
    try {
      setLoading(true);
      setError(null);
      const data = await taskApi.getTasks(filters);
      setTasks(data);
    } catch (err) {
      setError('Failed to fetch tasks');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };
  
  const fetchSummary = async () => {
    try {
      const data = await taskApi.getTaskSummary();
      setSummary(data);
    } catch (err) {
      console.error('Failed to fetch summary:', err);
    }
  };
  
  useEffect(() => {
    fetchTasks();
    fetchSummary();
  }, [filters]);
  
  const addTask = async (taskData) => {
    try {
      setLoading(true);
      setError(null);
      const newTask = await taskApi.createTask(taskData);
      setTasks([newTask, ...tasks]);
      fetchSummary();
      return newTask;
    } catch (err) {
      setError('Failed to add task');
      console.error(err);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  const updateTask = async (id, taskData) => {
    try {
      setLoading(true);
      setError(null);
      const updatedTask = await taskApi.updateTask(id, taskData);
      setTasks(tasks.map(task => 
        task.id === id ? updatedTask : task
      ));
      fetchSummary();
      return updatedTask;
    } catch (err) {
      setError('Failed to update task');
      console.error(err);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  const deleteTask = async (id) => {
    try {
      setLoading(true);
      setError(null);
      await taskApi.deleteTask(id);
      setTasks(tasks.filter(task => task.id !== id));
      fetchSummary();
    } catch (err) {
      setError('Failed to delete task');
      console.error(err);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  const updateFilters = (newFilters) => {
    setFilters({...filters, ...newFilters});
  };
  
  return (
    <TaskContext.Provider
      value={{
        tasks,
        summary,
        loading,
        error,
        filters,
        fetchTasks,
        addTask,
        updateTask,
        deleteTask,
        updateFilters,
      }}
    >
      {children}
    </TaskContext.Provider>
  );
};

export const useTasks = () => useContext(TaskContext);

// pages/TaskList.js
import React, { useState } from 'react';
import { useTasks } from '../context/TaskContext';
import TaskItem from '../components/Task/TaskItem';
import TaskFilters from '../components/Task/TaskFilters';

const TaskList = () => {
  const { tasks, loading, error, filters, updateFilters, deleteTask } = useTasks();
  const [selectedTask, setSelectedTask] = useState(null);
  
  const handleFilterChange = (newFilters) => {
    updateFilters(newFilters);
  };
  
  const handleTaskDelete = async (id) => {
    if (window.confirm('Are you sure you want to delete this task?')) {
      try {
        await deleteTask(id);
      } catch (err) {
        // Error is handled in context
      }
    }
  };
  
  if (loading) return <div className="loading">Loading tasks...</div>;
  if (error) return <div className="error">Error: {error}</div>;
  
  return (
    <div className="task-list-page">
      <h1>Task List</h1>
      
      <TaskFilters filters={filters} onChange={handleFilterChange} />
      
      {tasks.length === 0 ? (
        <div className="empty-state">
          <p>No tasks found. Create your first task!</p>
        </div>
      ) : (
        <div className="task-list">
          {tasks.map(task => (
            <TaskItem
              key={task.id}
              task={task}
              onSelect={() => setSelectedTask(task)}
              onDelete={() => handleTaskDelete(task.id)}
            />
          ))}
        </div>
      )}
      
      {selectedTask && (
        <TaskModal
          task={selectedTask}
          onClose={() => setSelectedTask(null)}
        />
      )}
    </div>
  );
};

export default TaskList;
            

Practice Activities

Basic Exercise: Weather Dashboard

Build a simple weather dashboard with React frontend and a Python backend that fetches weather data from a third-party API. Implement proper error handling and loading states.

Intermediate Exercise: Blog Platform

Create a blog platform with a Django REST Framework backend and React frontend. Include authentication, post creation/editing, comments, and filtering capabilities.

Advanced Exercise: E-Commerce Site

Develop an e-commerce application with product listing, search, shopping cart, checkout, and order management. Implement real-time stock updates using WebSockets.

Challenge: Multi-User Task Management

Build a Trello-like task management application with drag-and-drop boards, task assignments, file attachments, and real-time collaboration features.

Further Resources