Introduction

In this tutorial, you will build a To-Do application using Django and React.

React is a JavaScript framework for developing SPAs (single-page applications). It has solid documentation and a vibrant ecosystem around it.

Django is a Python web framework that simplifies common practices in web development. Django is reliable and also has a vibrant ecosystem of stable libraries supporting common development needs.

For this application, React serves as the frontend, or client-side framework, handling the user interface and getting and setting data via requests to the Django backend, which is an API built using the Django REST framework (DRF).

At the end of this tutorial, you will have a fully working application:

[info]

If you want to deploy the app in this tutorial, you can deploy directly from a GitHub repo using an app platform.

This application will allow users to create tasks and mark them as complete or incomplete.

Key Takeaways

app illustration for: Key Takeaways
  • Full-Stack Architecture: Learn to build a complete web application using Django REST Framework for the backend API and React for the frontend interface
  • API Integration: Master the connection between Django backend and React frontend using Axios for HTTP requests and CORS configuration
  • CRUD Operations: Implement Create, Read, Update, and Delete functionality for todo items with proper state management
  • Modern Development Practices: Use Django REST Framework serializers, React hooks, and component-based architecture
  • Production-Ready Setup: Configure CORS headers, environment variables, and proper project structure for scalable applications
  • Authentication Ready: Foundation for adding JWT authentication and user-specific todo management
  • Deployment Foundation: Structure that supports Docker containerization and cloud deployment
  • AI Integration Ready: Architecture supports adding AI-powered features like smart task categorization and priority suggestions

Prerequisites

To follow along with this tutorial, you will need to:

This tutorial was verified with Python v3.9.1, pip v20.2.4, Django v3.1.6, djangorestframework v3.12.2, django-cors-headers v3.7.0, Node v15.8.0, npm v7.5.4, React v17.0.1, and axios v0.21.0.

Step 1 — Setting Up the Backend

In this section, you will create a new project directory and install Django.

Open a new terminal window and run the following command to create a new project directory:

				
					
mkdir <^>django-todo-react<^>

				
			

Next, navigate into the directory:

				
					
cd <^>django-todo-react<^>

				
			

Now install Pipenv using pip:

				
					
pip install pipenv

				
			

Note: Depending on your installation, you may need to use pip3 instead of pip.

And activate a new virtual environment:

				
					
pipenv shell

				
			

Install Django using Pipenv:

				
					
pipenv install django

				
			

Then create a new project called backend:

				
					
django-admin startproject backend

				
			

Next, navigate into the newly created backend directory:

				
					
cd backend

				
			

Start a new application called todo:

				
					
python manage.py startapp todo

				
			

Run migrations:

				
					
python manage.py migrate

				
			

And start up the server:

				
					
python manage.py runserver

				
			

Navigate to http://localhost:8000 in your web browser:

At this point, you will see an instance of a Django application running successfully. Once you are finished, you can stop the server (CONTROL+C or CTRL+C).

Registering the todo Application

Now that you have completed the setup for the backend, you can begin registering the todo application as an installed app so that Django can recognize it.

Open the backend/settings.py file in your code editor and add todo to the INSTALLED_APPS:

				
					
[label backend/settings.py]

# Application definition



INSTALLED_APPS = [

 'django.contrib.admin',

 'django.contrib.auth',

 'django.contrib.contenttypes',

 'django.contrib.sessions',

 'django.contrib.messages',

 'django.contrib.staticfiles',

 <^>'todo',<^>

]

				
			

Then, save your changes.

Defining the Todo Model

Let's create a model to define how the Todo items should be stored in the database.

Open the todo/models.py file in your code editor and add the following lines of code:

				
					
[label todo/models.py]

from django.db import models



# Create your models here.



<^>class Todo(models.Model):<^>

 <^>title = models.CharField(max_length=120)<^>

 <^>description = models.TextField()<^>

 <^>completed = models.BooleanField(default=False)<^>



 <^>def _str_(self):<^>

 <^>return self.title<^>

				
			

The code snippet above describes three properties on the Todo model:

  • title
  • description
  • completed

The completed property is the status of a task. A task will either be completed or not completed at any time. Because you have created a Todo model, you will need to create a migration file:

				
					
python manage.py makemigrations todo

				
			

And apply the changes to the database:

				
					
python manage.py migrate todo

				
			

You can test to see that CRUD operations work on the Todo model you created by using the admin interface that Django provides by default.

Open the todo/admin.py file with your code editor and add the following lines of code:

				
					
[label todo/admin.py]

from django.contrib import admin

<^>from .models import Todo<^>



<^>class TodoAdmin(admin.ModelAdmin):<^>

 <^>list_display = ('title', 'description', 'completed')<^>



# Register your models here.



<^>admin.site.register(Todo, TodoAdmin)<^>

				
			

Then, save your changes.

You will need to create a "superuser" account to access the admin interface. Run the following command in your terminal:

				
					
python manage.py createsuperuser

				
			

You will be prompted to enter a username, email, and password for the superuser. Be sure to enter details that you can remember because you will need them to log in to the admin dashboard.

Start the server once again:

				
					
python manage.py runserver

				
			

Navigate to http://localhost:8000/admin in your web browser. And log in with the username and password that was created earlier:

You can create, edit, and, delete Todo items using this interface:

After experimenting with this interface, you can stop the server (CONTROL+C or CTRL+C).

Step 2 — Setting Up the APIs

In this section, you will create an API using the Django REST framework.

Install the djangorestframework and django-cors-headers using Pipenv:

				
					
pipenv install djangorestframework django-cors-headers

				
			

You need to add rest_framework and corsheaders to the list of installed applications. Open the backend/settings.py file in your code editor and update the INSTALLED_APPS and MIDDLEWARE sections:

				
					
[label backend/settings.py]

# Application definition



INSTALLED_APPS = [

 'django.contrib.admin',

 'django.contrib.auth',

 'django.contrib.contenttypes',

 'django.contrib.sessions',

 'django.contrib.messages',

 'django.contrib.staticfiles',

 <^>'corsheaders',<^>

 <^>'rest_framework',<^>

 'todo',

]



MIDDLEWARE = [

 'django.middleware.security.SecurityMiddleware',

 'django.contrib.sessions.middleware.SessionMiddleware',

 'django.middleware.common.CommonMiddleware',

 'django.middleware.csrf.CsrfViewMiddleware',

 'django.contrib.auth.middleware.AuthenticationMiddleware',

 'django.contrib.messages.middleware.MessageMiddleware',

 'django.middleware.clickjacking.XFrameOptionsMiddleware',

 <^>'corsheaders.middleware.CorsMiddleware',<^>

]

				
			

Then, add these lines of code to the bottom of the backend/settings.py file:

				
					
[label backend/settings.py]

<^>CORS_ORIGIN_WHITELIST = [<^>

 <^>'http://localhost:3000'<^>

<^>]<^>

				
			

django-cors-headers is a Python library that will prevent the errors that you would normally get due to CORS rules. In the CORS_ORIGIN_WHITELIST code, you whitelisted localhost:3000 because you want the frontend (which will be served on that port) of the application to interact with the API.

Creating serializers

You will need serializers to convert model instances to JSON so that the frontend can work with the received data.

Create a todo/serializers.py file with your code editor. Open the serializers.py file and update it with the following lines of code:

				
					
[label todo/serializers.py]

from rest_framework import serializers

from .models import Todo



class TodoSerializer(serializers.ModelSerializer):

 class Meta:

 model = Todo

 fields = ('id', 'title', 'description', 'completed')

				
			

This code specifies the model to work with and the fields to be converted to JSON.

Creating the View

You will need to create a TodoView class in the todo/views.py file.

Open the todo/views.py file with your code editor and add the following lines of code:

				
					
[label todo/views.py]

from django.shortcuts import render

<^>from rest_framework import viewsets<^>

<^>from .serializers import TodoSerializer<^>

<^>from .models import Todo<^>



# Create your views here.



<^>class TodoView(viewsets.ModelViewSet):<^>

 <^>serializer_class = TodoSerializer<^>

 <^>queryset = Todo.objects.all()<^>

				
			

The viewsets base class provides the implementation for CRUD operations by default. This code specifies the serializer_class and the queryset.

Open the backend/urls.py file with your code editor and replace the contents with the following lines of code:

				
					
[label backend/urls.py]

from django.contrib import admin

from django.urls import path<^>, include<^>

<^>from rest_framework import routers<^>

<^>from todo import views<^>



<^>router = routers.DefaultRouter()<^>

<^>router.register(r'todos', views.TodoView, 'todo')<^>



urlpatterns = [

 path('admin/', admin.site.urls),

 <^>path('api/', include(router.urls)),<^>

]

				
			

This code specifies the URL path for the API. This was the final step that completes the building of the API.

You can now perform CRUD operations on the Todo model. The router class allows you to make the following queries:

  • /todos/ – returns a list of all the Todo items. CREATE and READ operations can be performed here.
  • /todos/id – returns a single Todo item using the id primary key. UPDATE and DELETE operations can be performed here.

Let's restart the server:

				
					
python manage.py runserver

				
			

Navigate to http://localhost:8000/api/todos in your web browser:

You can CREATE a new Todo item using the interface:

If the Todo item is created successfully, you will be presented with a successful response:

You can also perform DELETE and UPDATE operations on specific Todo items using the id primary keys. Use the address structure /api/todos/{id} and provide an id.

Add 1 to the URL to examine the Todo item with the id of "1". Navigate to http://localhost:8000/api/todos/1 in your web browser:

This completes the building of the backend of the application.

Step 3 — Setting Up the Frontend

Now that you have the backend of the application complete, you can create the frontend and have it communicate with the backend over the interface that you created.

First, open a new terminal window and navigate to the django-todo-react project directory.

To set up the frontend, this tutorial will rely upon Create React App. There are several approaches to using create-react-app. One approach is to use npx to run the package and create the project:

				
					
npx create-react-app frontend

				
			

You can learn more about this approach by reading the How To Set Up a React Project with Create React App.

After the project is created, you can change into the newly created frontend directory:

				
					
cd frontend

				
			

Then, start the application:

				
					
npm start

				
			

Your web browser will open http://localhost:3000 and you will be presented with the default Create React App screen:

Next, install bootstrap and reactstrap to provide user interface tools.

				
					
npm install bootstrap<^>@4.6.0<^> reactstrap<^>@8.9.0 --legacy-peer-deps<^>

				
			

Note: You may encounter unable to resolve dependency tree errors depending on your versions of React, Bootstrap, and Reactstrap.

At the time of the revision, the latest version of popper.js has been deprecated and will conflict with React 17+. This is a known issue and it is possible to use the --legacy-peer-deps option when installing.

Open index.js in your code editor and add bootstrap.min.css:

				
					
[label frontend/src/index.js]

import React from 'react';

import ReactDOM from 'react-dom';

<^>import 'bootstrap/dist/css/bootstrap.css';<^>

import './index.css';

import App from './App';

import reportWebVitals from './reportWebVitals';



ReactDOM.render(

 <React.StrictMode>

 <App />

 </React.StrictMode>,

 document.getElementById('root')

);



// If you want to start measuring performance in your app, pass a function

// to log results (for example: reportWebVitals(console.log))

// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals

reportWebVitals();

				
			

If you are having difficulty with this step, you can consult the official documentation for adding bootstrap.

Open App.js in your code editor and add the following lines of code:

				
					
[label frontend/src/App.js]

import React, { Component } from "react";



const todoItems = [

 {

 id: 1,

 title: "Go to Market",

 description: "Buy ingredients to prepare dinner",

 completed: true,

 },

 {

 id: 2,

 title: "Study",

 description: "Read Algebra and History textbook for the upcoming test",

 completed: false,

 },

 {

 id: 3,

 title: "Sammy's books",

 description: "Go to library to return Sammy's books",

 completed: true,

 },

 {

 id: 4,

 title: "Article",

 description: "Write article on how to use Django with React",

 completed: false,

 },

];



class App extends Component {

 constructor(props) {

 super(props);

 this.state = {

 viewCompleted: false,

 todoList: todoItems,

 };

 }



 displayCompleted = (status) => {

 if (status) {

 return this.setState({ viewCompleted: true });

 }



 return this.setState({ viewCompleted: false });

 };



 renderTabList = () => {

 return (

 <div className="nav nav-tabs">

 <span

 className={this.state.viewCompleted ? "nav-link active" : "nav-link"}

 onClick={() => this.displayCompleted(true)}

 >

 Complete

 </span>

 <span

 className={this.state.viewCompleted ? "nav-link" : "nav-link active"}

 onClick={() => this.displayCompleted(false)}

 >

 Incomplete

 </span>

 </div>

 );

 };



 renderItems = () => {

 const { viewCompleted } = this.state;

 const newItems = this.state.todoList.filter(

 (item) => item.completed == viewCompleted

 );



 return newItems.map((item) => (

 <li

 key={item.id}

 className="list-group-item d-flex justify-content-between align-items-center"

 >

 <span

 className={`todo-title mr-2 ${

 this.state.viewCompleted ? "completed-todo" : ""

 }`}

 title={item.description}

 >

 {item.title}

 </span>

 <span>

 <button

 className="btn btn-secondary mr-2"

 >

 Edit

 </button>

 <button

 className="btn btn-danger"

 >

 Delete

 </button>

 </span>

 </li>

 ));

 };



 render() {

 return (

 <main className="container">

 <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>

 <div className="row">

 <div className="col-md-6 col-sm-10 mx-auto p-0">

 <div className="card p-3">

 <div className="mb-4">

 <button

 className="btn btn-primary"

 >

 Add task

 </button>

 </div>

 {this.renderTabList()}

 <ul className="list-group list-group-flush border-top-0">

 {this.renderItems()}

 </ul>

 </div>

 </div>

 </div>

 </main>

 );

 }

}



export default App;

				
			

This code includes some hardcoded values for four items. These will be temporary values until items are fetched from the backend.

The renderTabList() function renders two spans that help control which set of items are displayed. Clicking on the Completed tab will display the completed tasks. Clicking on the Incomplete tab will display the incomplete tasks.

Save your changes and observe the application in your web browser:

To handle actions such as adding and editing tasks, you will need to create a modal component.

First, create a components folder in the src directory:

				
					
mkdir src/components

				
			

Then, create a Modal.js file and open it with your code editor. Add the following lines of code:

				
					
[label frontend/src/components/Modal.js]

import React, { Component } from "react";

import {

 Button,

 Modal,

 ModalHeader,

 ModalBody,

 ModalFooter,

 Form,

 FormGroup,

 Input,

 Label,

} from "reactstrap";



export default class CustomModal extends Component {

 constructor(props) {

 super(props);

 this.state = {

 activeItem: this.props.activeItem,

 };

 }



 handleChange = (e) => {

 let { name, value } = e.target;



 if (e.target.type === "checkbox") {

 value = e.target.checked;

 }



 const activeItem = { ...this.state.activeItem, [name]: value };



 this.setState({ activeItem });

 };



 render() {

 const { toggle, onSave } = this.props;



 return (

 <Modal isOpen={true} toggle={toggle}>

 <ModalHeader toggle={toggle}>Todo Item</ModalHeader>

 <ModalBody>

 <Form>

 <FormGroup>

 <Label for="todo-title">Title</Label>

 <Input

 type="text"

 id="todo-title"

 name="title"

 value={this.state.activeItem.title}

 onChange={this.handleChange}

 placeholder="Enter Todo Title"

 />

 </FormGroup>

 <FormGroup>

 <Label for="todo-description">Description</Label>

 <Input

 type="text"

 id="todo-description"

 name="description"

 value={this.state.activeItem.description}

 onChange={this.handleChange}

 placeholder="Enter Todo description"

 />

 </FormGroup>

 <FormGroup check>

 <Label check>

 <Input

 type="checkbox"

 name="completed"

 checked={this.state.activeItem.completed}

 onChange={this.handleChange}

 />

 Completed

 </Label>

 </FormGroup>

 </Form>

 </ModalBody>

 <ModalFooter>

 <Button

 color="success"

 onClick={() => onSave(this.state.activeItem)}

 >

 Save

 </Button>

 </ModalFooter>

 </Modal>

 );

 }

}

				
			

This code creates a CustomModal class and it nests the Modal component that is derived from the reactstrap library.

This code also defined three fields in the form:

  • title
  • description
  • completed

These are the same fields that we defined as properties on the Todo model in the backend.

The CustomModal receives activeItem, toggle, and onSave as props:

  1. activeItem represents the Todo item to be edited.
  1. toggle is a function used to control the Modal's state (i.e., open or close the modal).
  1. onSave is a function that is called to save the edited values of the Todo item.

Next, you will import the CustomModal component into the App.js file.

Revisit the src/App.js file with your code editor and replace the entire contents with the following lines of code:

				
					
[label frontend/src/App.js]

import React, { Component } from "react";

<^>import Modal from "./components/Modal";<^>



const todoItems = [

 {

 id: 1,

 title: "Go to Market",

 description: "Buy ingredients to prepare dinner",

 completed: true,

 },

 {

 id: 2,

 title: "Study",

 description: "Read Algebra and History textbook for the upcoming test",

 completed: false,

 },

 {

 id: 3,

 title: "Sammy's books",

 description: "Go to library to return Sammy's books",

 completed: true,

 },

 {

 id: 4,

 title: "Article",

 description: "Write article on how to use Django with React",

 completed: false,

 },

];



class App extends Component {

 constructor(props) {

 super(props);

 this.state = {

 viewCompleted: false,

 todoList: todoItems,

 <^>modal: false,<^>

 <^>activeItem: {<^>

 <^>title: "",<^>

 <^>description: "",<^>

 <^>completed: false,<^>

 <^>},<^>

 };

 }



 <^>toggle = () => {<^>

 <^>this.setState({ modal: !this.state.modal });<^>

 <^>};<^>



 <^>handleSubmit = (item) => {<^>

 <^>this.toggle();<^>



 <^>alert("save" + JSON.stringify(item));<^>

 <^>};<^>



 <^>handleDelete = (item) => {<^>

 <^>alert("delete" + JSON.stringify(item));<^>

 <^>};<^>



 <^>createItem = () => {<^>

 <^>const item = { title: "", description: "", completed: false };<^>



 <^>this.setState({ activeItem: item, modal: !this.state.modal });<^>

 <^>};<^>



 <^>editItem = (item) => {<^>

 <^>this.setState({ activeItem: item, modal: !this.state.modal });<^>

 <^>};<^>



 displayCompleted = (status) => {

 if (status) {

 return this.setState({ viewCompleted: true });

 }



 return this.setState({ viewCompleted: false });

 };



 renderTabList = () => {

 return (

 <div className="nav nav-tabs">

 <span

 className={this.state.viewCompleted ? "nav-link active" : "nav-link"}

 onClick={() => this.displayCompleted(true)}

 >

 Complete

 </span>

 <span

 className={this.state.viewCompleted ? "nav-link" : "nav-link active"}

 onClick={() => this.displayCompleted(false)}

 >

 Incomplete

 </span>

 </div>

 );

 };



 renderItems = () => {

 const { viewCompleted } = this.state;

 const newItems = this.state.todoList.filter(

 (item) => item.completed === viewCompleted

 );



 return newItems.map((item) => (

 <li

 key={item.id}

 className="list-group-item d-flex justify-content-between align-items-center"

 >

 <span

 className={`todo-title mr-2 ${

 this.state.viewCompleted ? "completed-todo" : ""

 }`}

 title={item.description}

 >

 {item.title}

 </span>

 <span>

 <button

 className="btn btn-secondary mr-2"

 <^>onClick={() => this.editItem(item)}<^>

 >

 Edit

 </button>

 <button

 className="btn btn-danger"

 <^>onClick={() => this.handleDelete(item)}<^>

 >

 Delete

 </button>

 </span>

 </li>

 ));

 };



 render() {

 return (

 <main className="container">

 <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>

 <div className="row">

 <div className="col-md-6 col-sm-10 mx-auto p-0">

 <div className="card p-3">

 <div className="mb-4">

 <button

 className="btn btn-primary"

 <^>onClick={this.createItem}<^>

 >

 Add task

 </button>

 </div>

 {this.renderTabList()}

 <ul className="list-group list-group-flush border-top-0">

 {this.renderItems()}

 </ul>

 </div>

 </div>

 </div>

 <^>{this.state.modal ? (<^>

 <^><Modal<^>

 <^>activeItem={this.state.activeItem}<^>

 <^>toggle={this.toggle}<^>

 <^>onSave={this.handleSubmit}<^>

 <^>/><^>

 <^>) : null}<^>

 </main>

 );

 }

}



export default App;

				
			

Save your changes and observe the application in your web browser:

If you attempt to edit and save a Todo item, you will get an alert displaying the Todo item's object. Clicking on Save or Delete will perform the respective actions on the Todo item.

Note:: Depending on your version of React and Reactstrap, you may experience console errors. At the time of the revision, Warning: Legacy context API has been detected within a strict-mode tree. and Warning: findDOMNode is deprecated in StrictMode. are known issues.

Now, you will modify the application so that it interacts with the Django API you built in the previous section. Revisit the first terminal window and ensure the server is running. If it is not running, use the following command:

				
					
python manage.py runserver

				
			

Note: If you closed this terminal window, remember that you will need to navigate to the backend directory and use the virtual Pipenv shell.

To make requests to the API endpoints on the backend server, you will install a JavaScript library called axios.

In the second terminal window, ensure that you are in the frontend directory and install axios:

				
					
npm install axios<^>@0.21.1<^>

				
			

Then open the frontend/package.json file in your code editor and add a proxy:

				
					
[label frontend/package.json]

[...]

 "name": "frontend",

 "version": "0.1.0",

 "private": true,

 <^>"proxy": "http://localhost:8000",<^>

 "dependencies": {

 "axios": "^0.18.0",

 "bootstrap": "^4.1.3",

 "react": "^16.5.2",

 "react-dom": "^16.5.2",

 "react-scripts": "2.0.5",

 "reactstrap": "^6.5.0"

 },

[...]

				
			

The proxy will help in tunneling API requests to http://localhost:8000 where the Django application will handle them. Without this proxy, you would need to specify full paths:

				
					
axios.get("http://localhost:8000/api/todos/")

				
			

With proxy, you can provide relative paths:

				
					
axios.get("/api/todos/")

				
			

Note: You might need to restart the development server for the proxy to register with the application.

Revisit the frontend/src/App.js file and open it with your code editor. In this step, you will remove the hardcoded todoItems and use data from requests to the backend server. handleSubmit and handleDelete

Open the App.js file and replace it with this final version:

				
					
[label frontend/src/App.js]

import React, { Component } from "react";

import Modal from "./components/Modal";

<^>import axios from "axios";<^>



class App extends Component {

 constructor(props) {

 super(props);

 this.state = {

 viewCompleted: false,

 <^>todoList: [],<^>

 modal: false,

 activeItem: {

 title: "",

 description: "",

 completed: false,

 },

 };

 }



 <^>componentDidMount() {<^>

 <^>this.refreshList();<^>

 <^>}<^>



 <^>refreshList = () => {<^>

 <^>axios<^>

 <^>.get("/api/todos/")<^>

 <^>.then((res) => this.setState({ todoList: res.data }))<^>

 <^>.catch((err) => console.log(err));<^>

 <^>};<^>



 toggle = () => {

 this.setState({ modal: !this.state.modal });

 };



 handleSubmit = (item) => {

 this.toggle();



 <^>if (item.id) {<^>

 <^>axios<^>

 <^>.put(`/api/todos/${item.id}/`, item)<^>

 <^>.then((res) => this.refreshList());<^>

 <^>return;<^>

 <^>}<^>

 <^>axios<^>

 <^>.post("/api/todos/", item)<^>

 <^>.then((res) => this.refreshList());<^>

 };



 handleDelete = (item) => {

 <^>axios<^>

 <^>.delete(`/api/todos/${item.id}/`)<^>

 <^>.then((res) => this.refreshList());<^>

 };



 createItem = () => {

 const item = { title: "", description: "", completed: false };



 this.setState({ activeItem: item, modal: !this.state.modal });

 };



 editItem = (item) => {

 this.setState({ activeItem: item, modal: !this.state.modal });

 };



 displayCompleted = (status) => {

 if (status) {

 return this.setState({ viewCompleted: true });

 }



 return this.setState({ viewCompleted: false });

 };



 renderTabList = () => {

 return (

 <div className="nav nav-tabs">

 <span

 onClick={() => this.displayCompleted(true)}

 className={this.state.viewCompleted ? "nav-link active" : "nav-link"}

 >

 Complete

 </span>

 <span

 onClick={() => this.displayCompleted(false)}

 className={this.state.viewCompleted ? "nav-link" : "nav-link active"}

 >

 Incomplete

 </span>

 </div>

 );

 };



 renderItems = () => {

 const { viewCompleted } = this.state;

 const newItems = this.state.todoList.filter(

 (item) => item.completed === viewCompleted

 );



 return newItems.map((item) => (

 <li

 key={item.id}

 className="list-group-item d-flex justify-content-between align-items-center"

 >

 <span

 className={`todo-title mr-2 ${

 this.state.viewCompleted ? "completed-todo" : ""

 }`}

 title={item.description}

 >

 {item.title}

 </span>

 <span>

 <button

 className="btn btn-secondary mr-2"

 onClick={() => this.editItem(item)}

 >

 Edit

 </button>

 <button

 className="btn btn-danger"

 onClick={() => this.handleDelete(item)}

 >

 Delete

 </button>

 </span>

 </li>

 ));

 };



 render() {

 return (

 <main className="container">

 <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>

 <div className="row">

 <div className="col-md-6 col-sm-10 mx-auto p-0">

 <div className="card p-3">

 <div className="mb-4">

 <button

 className="btn btn-primary"

 onClick={this.createItem}

 >

 Add task

 </button>

 </div>

 {this.renderTabList()}

 <ul className="list-group list-group-flush border-top-0">

 {this.renderItems()}

 </ul>

 </div>

 </div>

 </div>

 {this.state.modal ? (

 <Modal

 activeItem={this.state.activeItem}

 toggle={this.toggle}

 onSave={this.handleSubmit}

 />

 ) : null}

 </main>

 );

 }

}



export default App;

				
			

The refreshList() function is reusable that is called each time an API request is completed. It updates the Todo list to display the most recent list of added items.

The handleSubmit() function takes care of both the create and update operations. If the item passed as the parameter doesn't have an id, then it has probably not been created, so the function creates it.

At this point, verify that your backend server is running in your first terminal window:

				
					
python manage.py runserver

				
			

Note: If you closed this terminal window, remember that you will need to navigate to the backend directory and use the virtual Pipenv shell.

And in your second terminal window, ensure that you are in the frontend directory and start your frontend application:

				
					
npm start

				
			

Now when you visit http://localhost:3000 with your web browser, your application will allow you to READ, CREATE, UPDATE, and DELETE tasks.

This completes the frontend and backend of the Todo application.

Step 4 — Adding User Authentication with JWT

To make this application production-ready, let's add JWT (JSON Web Token) authentication. This will allow users to have their own personal todo lists.

Backend Authentication Setup

First, install the required packages for JWT authentication:

				
					
pipenv install djangorestframework-simplejwt

				
			

Add JWT authentication to your Django settings. Open backend/settings.py and add the following:

				
					
[label backend/settings.py]

# Add to INSTALLED_APPS

INSTALLED_APPS = [

 # ... existing apps ...

 'rest_framework_simplejwt',

]



# Add JWT configuration

from datetime import timedelta



SIMPLE_JWT = {

 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),

 'REFRESH_TOKEN_LIFETIME': timedelta(days=1),

 'ROTATE_REFRESH_TOKENS': True,

}



REST_FRAMEWORK = {

 'DEFAULT_AUTHENTICATION_CLASSES': (

 'rest_framework_simplejwt.authentication.JWTAuthentication',

 ),

}

				
			

Update your backend/urls.py to include JWT token endpoints:

				
					
[label backend/urls.py]

from django.contrib import admin

from django.urls import path, include

from rest_framework import routers

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

from todo import views



router = routers.DefaultRouter()

router.register(r'todos', views.TodoView, 'todo')



urlpatterns = [

 path('admin/', admin.site.urls),

 path('api/', include(router.urls)),

 path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),

 path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),

]

				
			

Frontend Authentication Integration

Install axios for making authenticated requests:

				
					
npm install axios

				
			

Create an authentication context in frontend/src/AuthContext.js:

				
					
[label frontend/src/AuthContext.js]

import React, { createContext, useContext, useState, useEffect } from 'react';

import axios from 'axios';



const AuthContext = createContext();



export const useAuth = () => {

 return useContext(AuthContext);

};



export const AuthProvider = ({ children }) => {

 const [user, setUser] = useState(null);

 const [loading, setLoading] = useState(true);



 useEffect(() => {

 const token = localStorage.getItem('access_token');

 if (token) {

 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;

 // Verify token and get user info

 setUser({ isAuthenticated: true });

 }

 setLoading(false);

 }, []);



 const login = async (username, password) => {

 try {

 const response = await axios.post('/api/token/', {

 username,

 password,

 });

 const { access, refresh } = response.data;

 localStorage.setItem('access_token', access);

 localStorage.setItem('refresh_token', refresh);

 axios.defaults.headers.common['Authorization'] = `Bearer ${access}`;

 setUser({ isAuthenticated: true });

 return true;

 } catch (error) {

 console.error('Login failed:', error);

 return false;

 }

 };



 const logout = () => {

 localStorage.removeItem('access_token');

 localStorage.removeItem('refresh_token');

 delete axios.defaults.headers.common['Authorization'];

 setUser(null);

 };



 const value = {

 user,

 login,

 logout,

 loading,

 };



 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

};

				
			

Step 5 — Testing and Debugging

Backend Testing

Create tests for your Django API in todo/tests.py:

				
					
[label todo/tests.py]

from django.test import TestCase

from django.contrib.auth.models import User

from rest_framework.test import APITestCase

from rest_framework import status

from .models import Todo



class TodoModelTest(TestCase):

 def test_todo_creation(self):

 todo = Todo.objects.create(

 title="Test Todo",

 description="Test Description",

 completed=False

 )

 self.assertEqual(todo.title, "Test Todo")

 self.assertFalse(todo.completed)



class TodoAPITest(APITestCase):

 def setUp(self):

 self.user = User.objects.create_user(

 username='testuser',

 password='testpass123'

 )

 self.client.force_authenticate(user=self.user)



 def test_create_todo(self):

 data = {

 'title': 'Test Todo',

 'description': 'Test Description',

 'completed': False

 }

 response = self.client.post('/api/todos/', data)

 self.assertEqual(response.status_code, status.HTTP_201_CREATED)

 self.assertEqual(Todo.objects.count(), 1)



 def test_get_todos(self):

 Todo.objects.create(title="Test Todo", description="Test")

 response = self.client.get('/api/todos/')

 self.assertEqual(response.status_code, status.HTTP_200_OK)

 self.assertEqual(len(response.data), 1)

				
			

Run the tests:

				
					
python manage.py test

				
			

Frontend Testing

Install React Testing Library:

				
					
npm install --save-dev @testing-library/react @testing-library/jest-dom

				
			

Create a test for your Todo component in frontend/src/App.test.js:

				
					
[label frontend/src/App.test.js]

import React from 'react';

import { render, screen, fireEvent } from '@testing-library/react';

import App from './App';



// Mock axios

jest.mock('axios');

import axios from 'axios';



describe('Todo App', () => {

 beforeEach(() => {

 axios.get.mockResolvedValue({ data: [] });

 });



 test('renders todo app title', () => {

 render(<App />);

 const titleElement = screen.getByText(/todo app/i);

 expect(titleElement).toBeInTheDocument();

 });



 test('shows add task button', () => {

 render(<App />);

 const addButton = screen.getByText(/add task/i);

 expect(addButton).toBeInTheDocument();

 });

});

				
			

Step 6 — Production Deployment with Docker

Docker Configuration

Create a Dockerfile for the Django backend:

				
					
[label Dockerfile]

FROM python:3.9-slim



WORKDIR /app



# Install system dependencies

RUN apt-get update && apt-get install -y \

 postgresql-client \

 && rm -rf /var/lib/apt/lists/*



# Install Python dependencies

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt



# Copy project

COPY . .



# Collect static files

RUN python manage.py collectstatic --noinput



EXPOSE 8000



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

				
			

Create a docker-compose.yml for the full stack:

				
					
[label docker-compose.yml]

version: '3.8'



services:

 db:

 image: postgres:13

 environment:

 POSTGRES_DB: todoapp

 POSTGRES_USER: postgres

 POSTGRES_PASSWORD: postgres

 volumes:

 - postgres_data:/var/lib/postgresql/data



 backend:

 build: .

 command: python manage.py runserver 0.0.0.0:8000

 volumes:

 - .:/app

 ports:

 - "8000:8000"

 depends_on:

 - db

 environment:

 - DEBUG=1

 - DATABASE_URL=postgres://postgres:postgres@db:5432/todoapp



 frontend:

 build: ./frontend

 ports:

 - "3000:3000"

 volumes:

 - ./frontend:/app

 - /app/node_modules

 environment:

 - REACT_APP_API_URL=http://localhost:8000



volumes:

 postgres_data:

				
			

Environment Variables

Create a .env file for production settings:

				
					
[label .env]

DEBUG=False

SECRET_KEY=your-secret-key-here

DATABASE_URL=postgres://postgres:postgres@db:5432/todoapp

ALLOWED_HOSTS=localhost,127.0.0.1

CORS_ALLOWED_ORIGINS=http://localhost:3000

				
			

Pros and Cons of Django + React

Advantages

  • Separation of Concerns: Clear separation between backend API and frontend UI
  • Scalability: Can scale frontend and backend independently
  • Technology Flexibility: Can replace React with any frontend framework
  • API Reusability: Backend API can serve mobile apps, other web apps, or third-party integrations
  • Team Specialization: Frontend and backend developers can work independently

Disadvantages

  • Complexity: More moving parts compared to traditional Django templates
  • CORS Configuration: Requires careful CORS setup for cross-origin requests
  • SEO Challenges: Client-side rendering can impact SEO (though solutions exist)
  • Development Overhead: Requires managing two separate applications
  • Authentication Complexity: JWT tokens require additional security considerations

Step 7 — Adding AI-Powered Features (Optional)

Modern todo applications can benefit from AI integration. Here's how to add intelligent features to your Django-React app:

Smart Task Categorization

Add AI-powered task categorization using OpenAI's API. First, install the required package:

				
					
pipenv install openai

				
			

Create an AI service in todo/ai_services.py:

				
					
[label todo/ai_services.py]

import openai

from django.conf import settings



class TaskAIService:

 def __init__(self):

 openai.api_key = settings.OPENAI_API_KEY

 

 def categorize_task(self, title, description):

 """Categorize a task using AI"""

 prompt = f"""

 Categorize this task into one of these categories: work, personal, health, shopping, learning, other

 Task: {title}

 Description: {description}

 Return only the category name.

 """

 

 response = openai.ChatCompletion.create(

 model="gpt-3.5-turbo",

 messages=[{"role": "user", "content": prompt}],

 max_tokens=10

 )

 

 return response.choices[0].message.content.strip().lower()

 

 def suggest_priority(self, title, description, due_date=None):

 """Suggest task priority based on content and context"""

 prompt = f"""

 Analyze this task and suggest priority: high, medium, low

 Consider urgency, importance, and due date.

 Task: {title}

 Description: {description}

 Due Date: {due_date or 'No due date'}

 Return only: high, medium, or low

 """

 

 response = openai.ChatCompletion.create(

 model="gpt-3.5-turbo",

 messages=[{"role": "user", "content": prompt}],

 max_tokens=5

 )

 

 return response.choices[0].message.content.strip().lower()

				
			

Update your Todo model to include AI-generated fields:

				
					
[label todo/models.py]

from django.db import models



class Todo(models.Model):

 title = models.CharField(max_length=120)

 description = models.TextField()

 completed = models.BooleanField(default=False)

 # AI-generated fields

 ai_category = models.CharField(max_length=20, blank=True, null=True)

 ai_priority = models.CharField(max_length=10, blank=True, null=True)

 ai_confidence = models.FloatField(default=0.0)

 

 def __str__(self):

 return self.title

				
			

React AI Integration

Create an AI-powered task input component in frontend/src/components/AITaskInput.js:

				
					
[label frontend/src/components/AITaskInput.js]

import React, { useState } from 'react';

import { Button, Input, FormGroup, Label } from 'reactstrap';



const AITaskInput = ({ onTaskCreate }) => {

 const [title, setTitle] = useState('');

 const [description, setDescription] = useState('');

 const [isAnalyzing, setIsAnalyzing] = useState(false);

 const [aiSuggestions, setAiSuggestions] = useState(null);



 const analyzeWithAI = async () => {

 setIsAnalyzing(true);

 try {

 const response = await axios.post('/api/todos/analyze/', {

 title,

 description

 });

 setAiSuggestions(response.data);

 } catch (error) {

 console.error('AI analysis failed:', error);

 }

 setIsAnalyzing(false);

 };



 const handleSubmit = () => {

 const task = {

 title,

 description,

 ...(aiSuggestions && {

 ai_category: aiSuggestions.category,

 ai_priority: aiSuggestions.priority

 })

 };

 onTaskCreate(task);

 setTitle('');

 setDescription('');

 setAiSuggestions(null);

 };



 return (

 <div className="ai-task-input">

 <FormGroup>

 <Label>Task Title</Label>

 <Input

 value={title}

 onChange={(e) => setTitle(e.target.value)}

 placeholder="Enter task title"

 />

 </FormGroup>

 

 <FormGroup>

 <Label>Description</Label>

 <Input

 type="textarea"

 value={description}

 onChange={(e) => setDescription(e.target.value)}

 placeholder="Enter task description"

 />

 </FormGroup>

 

 <Button 

 color="info" 

 onClick={analyzeWithAI}

 disabled={isAnalyzing || !title.trim()}

 >

 {isAnalyzing ? 'Analyzing...' : '🤖 Analyze with AI'}

 </Button>

 

 {aiSuggestions && (

 <div className="ai-suggestions mt-3">

 <h6>AI Suggestions:</h6>

 <p><strong>Category:</strong> {aiSuggestions.category}</p>

 <p><strong>Priority:</strong> {aiSuggestions.priority}</p>

 </div>

 )}

 

 <Button color="primary" onClick={handleSubmit} className="mt-2">

 Create Task

 </Button>

 </div>

 );

};



export default AITaskInput;

				
			

AI-Powered Task Analytics

Add analytics to track AI suggestions and user behavior:

				
					
[label todo/analytics.py]

from django.db import models

from django.contrib.auth.models import User



class TaskAnalytics(models.Model):

 user = models.ForeignKey(User, on_delete=models.CASCADE)

 task = models.ForeignKey(Todo, on_delete=models.CASCADE)

 ai_category = models.CharField(max_length=20)

 ai_priority = models.CharField(max_length=10)

 user_accepted_category = models.BooleanField(default=False)

 user_accepted_priority = models.BooleanField(default=False)

 created_at = models.DateTimeField(auto_now_add=True)

 

 class Meta:

 verbose_name_plural = "Task Analytics"

				
			

This AI integration demonstrates how modern web applications can leverage artificial intelligence to enhance user experience while maintaining the clean separation between Django backend and React frontend.

Frequently Asked Questions

1. How do I connect Django and React for a full-stack app?

Django and React connect through a REST API. Django serves as the backend API using Django REST Framework, while React consumes this API using HTTP requests (typically with Axios). The connection requires proper CORS configuration to allow cross-origin requests between the frontend (usually on port 3000) and backend (usually on port 8000).

2. Why use Django REST Framework with React?

Django REST Framework (DRF) provides a powerful, flexible toolkit for building Web APIs. It offers built-in serialization, authentication, permissions, and browsable API interfaces. When paired with React, DRF creates a clean separation between your data layer and presentation layer, making your application more maintainable and scalable.

3. How can I deploy my Django-React app on the cloud provider?

You can deploy your Django-React app on the cloud provider using several methods:

  • an app platform: Deploy directly from your GitHub repository with automatic builds
  • Droplets: Use Docker containers with docker-compose for a more traditional VPS deployment
  • Kubernetes: For more complex, scalable applications

The tutorial includes Docker configuration that works seamlessly with the cloud provider's infrastructure.

4. What's the best way to manage state in React for this project?

For this todo application, React's built-in useState and useEffect hooks are sufficient. For larger applications, consider:

  • Redux Toolkit: For complex state management
  • Context API: For sharing state across components
  • Zustand: A lightweight alternative to Redux
  • React Query: For server state management and caching
5. Can I extend this to-do app with user authentication?

Yes! The tutorial includes a complete JWT authentication implementation in Step 4. This allows users to:

  • Register and log in with their own accounts
  • Have personal todo lists that are private to each user
  • Maintain secure sessions using JSON Web Tokens
  • Refresh tokens automatically for better user experience
6. Is Django and React a good combination?

Django and React make an excellent combination for full-stack development:

  • Django provides a robust, secure backend with excellent admin interface and ORM
  • React offers a modern, component-based frontend with excellent developer experience
  • Together, they provide clear separation of concerns and allow teams to specialize
  • The combination is particularly strong for data-driven applications and rapid prototyping
7. What frontend works best with Django?

Popular frontend frameworks that work well with Django include:

  • React: Most popular choice, excellent ecosystem
  • Vue.js: Easier learning curve, good for smaller teams
  • Angular: Enterprise-grade, full-featured framework
  • Svelte: Lightweight, compiled framework
  • Next.js: React-based with server-side rendering capabilities

The choice depends on your team's expertise, project requirements, and long-term goals.

8. Is Django harder than React?

Django has a steeper learning curve due to its "batteries included" philosophy and backend complexity. React is generally easier to start with but requires learning JavaScript, JSX, and component-based architecture. Both frameworks have extensive documentation and community support, making them accessible to developers with proper learning resources.

Conclusion

In this comprehensive tutorial, you built a production-ready To-Do application using Django and React. You learned how to:

You set up a Django backend with a REST API using Django REST Framework, created a React frontend that communicates with the Django API, and implemented JWT authentication to manage user-specific todo lists securely. You also wrote tests for both backend and frontend components, configured Docker for containerized deployment, and handled CORS and environment variables to prepare your application for production.

The application demonstrates modern full-stack development practices and provides a solid foundation for building more complex web applications. The separation of concerns between Django's robust backend and React's dynamic frontend makes this architecture ideal for scalable, maintainable applications.

Next Steps

To further enhance your Django-React application, consider:

  • API Documentation: Add Swagger/OpenAPI documentation for your Django REST API

If you'd like to learn more about Django, check out our Django topic page for exercises and programming projects.

If you'd like to learn more about React, take a look at our How To Code in React.js series, or check out our React topic page for exercises and programming projects.