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.
Benefits of Decoupled Architecture
- Independent Development: Frontend and backend teams can work separately
- Technology Freedom: Each side can use the best tools for its domain
- Scalability: Components can be scaled independently
- Maintainability: Clearer separation of concerns
- Reusability: The same API can serve multiple clients (web, mobile, third-party)
Challenges to Consider
- Increased Complexity: Managing two separate applications
- API Design: Need for careful planning and documentation
- Cross-Origin Issues: Handling CORS when domains differ
- Authentication: Implementing secure auth across separate systems
- Data Transfer: Optimizing the amount of data exchanged
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:
- 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:
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:
- Independent scaling for frontend and backend
- Can use specialized hosting for each part
- Easier to version and update each component separately
Considerations:
- Need to configure CORS properly
- May require setting up a custom domain and SSL
- Multiple infrastructure components to manage
Static Files Served by Python Backend
Build React and serve it through your Python application:
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
- Consistent API Design: Follow RESTful conventions and maintain a consistent response format
- Proper Error Handling: Provide meaningful error messages and appropriate status codes
- API Documentation: Use tools like Swagger or DRF's browsable API to document your endpoints
- State Management: Choose the right state management approach based on application complexity
- Loading and Error States: Always handle loading, success, and error states in the UI
- Authentication Flow: Implement secure token refresh and storage mechanisms
- Environment Variables: Use environment variables for configuration in both frontend and backend
- Code Organization: Structure your code for maintainability with services, hooks, and components
Common Pitfalls
- CORS Issues: Forgetting to configure CORS or configuring it incorrectly
- Authentication Leaks: Storing sensitive tokens in localStorage instead of secure HTTP-only cookies
- Missing Loading States: Not handling loading states, leading to poor user experience
- Inconsistent Error Handling: Different error formats across endpoints
- Inefficient API Calls: Making too many requests or requesting more data than needed
- Security Vulnerabilities: Not validating input properly on both client and server
- Prop Drilling: Passing props through many component levels instead of using context or state management
- Over-fetching: Requesting too much data when only a subset is needed
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.