In this tutorial, we will learn how to build a CRUD To-do application with Django REST framework for the backend, React and Redux for the frontend.
Table of Contents
Creating a virtual environment with Pipenv
First, we will create the folder for the project and navigate into it:
$ mkdir django-react-todo
$ cd django-react-todo
Let’s create a virtual environment by running this command:
$ pipenv --python 3
If you don’t have Pipenv installed yet, please install it by running this command:
$ pip install pipenv
We will install the packages we need:
$ pipenv install django djangorestframework
Creating a new project and some apps
In this tutorial, we will create a project named “todocrud”. We can leave out the extra folder which is automatically created by adding a dot to the end of the command and running:
$ django-admin startproject todocrud .
Next, we will create two apps. One is for the backend, the other is for the frontend:
$ python manage.py startapp todos
$ python manage.py startapp frontend
We will open the settings.py
file in the project directory, and configure to use the apps we created and Django REST framework:
# settings.py
INSTALLED_APPS = [
'frontend.apps.FrontendConfig', # added
'todos.apps.TodosConfig', # added
'rest_framework', # added
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
REST_FRAMEWORK = { # added
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny'
],
'DATETIME_FORMAT': "%m/%d/%Y %H:%M:%S",
}
We can specify the output format of a date and time by including DATETIME_FORMAT
in a configuration dictionary named REST_FRAMEWORK
.
Let’s apply migrations and start the development server:
$ python manage.py migrate
$ python manage.py runserver
Visit http://127.0.0.1:8000/ with your browser. If you see the page a rocket taking off, it worked well!
Writing backend’s modules
First, we will create a simple model. Open the models.py
file and write the following code:
# todos/models.py
from django.db import models
class Todo(models.Model):
task = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.task
Next, we will build a simple model-backed API using REST framework. Let’s create a new folder named api
and create new files __init__.py
, serializers.py
, views.py
and urls.py
in it:
todos/
api/
__init__.py
serializers.py
urls.py
views.py
Since the api
is a module, we need to include __init__.py
file.
Let’s define the API representation in the serializers.py
file:
# todos/api/serializers.py
from rest_framework import serializers
from todos.models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = '__all__'
The ModelSerializer
class will create fields that correspond to the Model fields.
Next, we will define the view behavior in the api/views.py
file:
# todos/api/views.py
from rest_framework import viewsets
from .serializers import TodoSerializer
from todos.models import Todo
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
We will finally write the URL configuration using Routers
:
# todos/api/urls.py
from rest_framework import routers
from .views import TodoViewSet
router = routers.DefaultRouter()
router.register('todos', TodoViewSet, 'todos')
# router.register('<The URL prefix>', <The viewset class>, '<The URL name>')
urlpatterns = router.urls
We use three arguments to the register()
method, but the third argument is not required.
Writing frontend’s modules
In the frontend, all we have to do is write simple views and URL patterns.
Open the frontend/views.py
file and create the two views:
# frontend/views.py
from django.shortcuts import render
from django.views.generic.detail import DetailView
from todos.models import Todo
def index(request):
return render(request, 'frontend/index.html')
class TodoDetailView(DetailView):
model = Todo
template_name = 'frontend/index.html'
We will create the frontend/index.html
file later. Don’t worry about it now.
Add a new urls.py
file to the same directory and create the URL conf:
# frontend/urls.py
from django.urls import path
from .views import index, TodoDetailView
urlpatterns = [
path('', index),
path('edit/<int:pk>', TodoDetailView.as_view()),
path('delete/<int:pk>', TodoDetailView.as_view()),
]
Although the path for the Django admin
site is left, we are not going to use it in this tutorial.
As a final part of this chapter, we will make a new migration file and apply changes to our databases by running the commands below:
$ python manage.py makemigrations
$ python manage.py migrate
Creating directories
First of all, let’s create all of the directories we need:
$ mkdir -p ./frontend/src/{components,actions,reducers}
$ mkdir -p ./frontend/{static,templates}/frontend
The above command should create the directories as follows:
frontend/
src/
actions/
components/
reducers/
static/
frontend/
templates/
frontend/
Installing packages
We need to create a package.json
file by running the following command before installing packages:
$ npm init -y
In order to use npm
, Node.js needs to be installed.
Then, let’s install all the packages we use with npm
command:
$ npm i -D webpack webpack-cli
$ npm i -D @babel/core babel-loader @babel/polyfill @babel/preset-env @babel/preset-react babel-plugin-transform-class-properties
$ npm i react react-dom react-router-dom
$ npm i redux react-redux redux-thunk redux-devtools-extension
$ npm i redux-form
$ npm i axios
$ npm i lodash
Creating config files
Add a file named .babelrc
to the root directory and configure Babel:
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-react"
],
"plugins": ["transform-class-properties"]
}
We can use Async/Await with Babel by installing @babel/polyfill
and writing as above.
Secondary, add a file named webpack.config.js
to the same directory and write a configuration for webpack
:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
};
Additionally, we need to rewrite the "scripts”
property of the package.json
file:
// package.json
{
// ...
"scripts": {
"dev": "webpack --mode development --watch ./frontend/src/index.js --output ./frontend/static/frontend/main.js",
"build": "webpack --mode production ./frontend/src/index.js --output ./frontend/static/frontend/main.js"
},
// ...
}
Two new scripts have been defined. We can run scripts with npm run dev
for development or npm run build
for production. When these scripts are run, webpack
bundles modules and output the main.js
file.
Creating principal files
Let’s create three principal files and render the first word.
We will create a file named index.js
that is called first when we run the React application:
// frontend/src/index.js
import App from './components/App';
Next, we will create a file named App.js
that is a parent component:
// frontend/src/components/App.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class App extends Component {
render() {
return (
<div>
<h1>ToDoCRUD</h1>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
We will finally create a template file named index.html
that is specified in the views.py
file:
<!-- templates/frontend/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- semantic-ui CDN -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
<title>ToDoCRUD</title>
</head>
<body>
<div id='app'></div>
{% load static %}
<script src="{% static 'frontend/main.js' %}"></script>
</body>
</html>
In this tutorial, we will use Semantic UI as a CSS framework.
Place the wrapper for rendering the App component and the bundled script into the <body>
tag.
Checking the display
Let’s see whether it is displayed correctly.
Open another terminal and run the script:
$ npm run dev
The main.js
file should be generated in the static/frontend
directory.
Then, start the dev server and visit http://127.0.0.1:8000/:
$ python manage.py runserver
If the word “ToDoCRUD” is displayed, everything is going well so far :)
It is time to use Redux. We will create Actions, Reducers and Store.
Actions
Let’s define all the type properties in advance. Add a new file named types.js
into the src/actions
directory:
// actions/types.js
export const GET_TODOS = 'GET_TODOS';
export const GET_TODO = 'GET_TODO';
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const EDIT_TODO = 'EDIT_TODO';
In order to create actions, we need to define Action Creators. Add a new file named todos.js
into the src/actions
directory:
// actions/todos.js
import axios from 'axios';
import { GET_TODOS } from './types';
// GET TODOS
export const getTodos = () => async dispatch => {
const res = await axios.get('/api/todos/');
dispatch({
type: GET_TODOS,
payload: res.data
});
};
Reducers
Reducers specify how the application’s state changes in response to actions sent to the store.
That is the role of Reducers. Add a new file namedtodos.js
into thesrc/reducers
directory and write a child reducer:
// reducers/todos.js
import _ from 'lodash';
import { GET_TODOS } from '../actions/types';
export default (state = {}, action) => {
switch (action.type) {
case GET_TODOS:
return {
...state,
..._.mapKeys(action.payload, 'id')
};
default:
return state;
}
};
Lodash is a JavaScript utility library. It is not a requirement, but it can cut down on development time and make your codebase smaller.
Let’s create the parent reducer to put together every child reducer using combineReducers()
. Add a new file named index.js
into the src/reducers
directory:
// reducers/index.js
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import todos from './todos';
export default combineReducers({
form: formReducer,
todos
});
In order to use redux-form
, we need to include its reducer in the combineReducers
function.
Store
The Store is an object to hold the state of our application. In addition, we will use the recommended middleware Redux Thunk to write async logic that interacts with the store. Let’s create a new file named store.js
in the src
directory:
// fronted/src/store.js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import reduxThunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(reduxThunk))
);
export default store;
Use of Redux DevTools is optional, but it is very useful because it visualizes the state changes of Redux. I will omit how to use it here, but it is highly recommended.
Components
First, create a new folder named todos
in the components
directory. And then, add a new file named TodoList.js
into the folder we created:
// components/todos/TodoList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTodos } from '../../actions/todos';
class TodoList extends Component {
componentDidMount() {
this.props.getTodos();
}
render() {
return (
<div className='ui relaxed divided list' style={{ marginTop: '2rem' }}>
{this.props.todos.map(todo => (
<div className='item' key={todo.id}>
<i className='large calendar outline middle aligned icon' />
<div className='content'>
<a className='header'>{todo.task}</a>
<div className='description'>{todo.created_at}</div>
</div>
</div>
))}
</div>
);
}
}
const mapStateToProps = state => ({
todos: Object.values(state.todos)
});
export default connect(
mapStateToProps,
{ getTodos }
)(TodoList);
We use Semantic UI list to decorate the list.
The connect()
function connects this component to the store. It accepts mapStateToProps
as the first argument, Action Creators as the second argument. We will be able to use the store state as Props by specifying mapStateToProps
.
We will create a new file named Dashboard.js
in the same directory. It is just a container for TodoList
and a form we will create in the next chapter:
// components/todos/Dashboard.js
import React, { Component } from 'react';
import TodoList from './TodoList';
class Dashboard extends Component {
render() {
return (
<div className='ui container'>
<div>Todo Create Form</div>
<TodoList />
</div>
);
}
}
export default Dashboard;
Open the App.js
file and update as follows:
// components/App.js
import { Provider } from 'react-redux'; // added
import store from '../store'; // added
class App extends Component {
render() {
return (
<Provider store={store}>
<Dashboard />
</Provider>
);
}
}
The Provider
makes the store available to the component nested inside of it.
Checking the display
First, visit http://127.0.0.1:8000/api/todos/ and create several objects. And then, visit http://127.0.0.1:8000/.
You should see a simple list of the objects you created. Did it work?
Actions
Open the actions/todos.js
file, and add a new action creator:
// actions/todos.js
import { reset } from 'redux-form'; // added
import { GET_TODOS, ADD_TODO } from './types'; // added ADD_TODO
// ADD TODO
export const addTodo = formValues => async dispatch => {
const res = await axios.post('/api/todos/', { ...formValues });
dispatch({
type: ADD_TODO,
payload: res.data
});
dispatch(reset('todoForm'));
};
Dispatching reset('formName')
clears our form after we submission succeeds. We will specify the form name later in the Form component.
Reducers
Open the reducers/todos.js
file, and add a new action to the reducer:
// reducers/todos.js
import { GET_TODOS, ADD_TODO } from '../actions/types'; // added ADD_TODO
export default (state = {}, action) => {
switch (action.type) {
// ...
case ADD_TODO: // added
return {
...state,
[action.payload.id]: action.payload
};
// ...
}
};
Components
Let’s create a Form component. We will create a Form separately as a reusable component so that it can also be used for editing. Create a new file named TodoForm.js
in the components/todos
directory:
// components/todos/TodoForm.js
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
class TodoForm extends Component {
renderField = ({ input, label, meta: { touched, error } }) => {
return (
<div className={`field ${touched && error ? 'error' : ''}`}>
<label>{label}</label>
<input {...input} autoComplete='off' />
{touched && error && (
<span className='ui pointing red basic label'>{error}</span>
)}
</div>
);
};
onSubmit = formValues => {
this.props.onSubmit(formValues);
};
render() {
return (
<div className='ui segment'>
<form
onSubmit={this.props.handleSubmit(this.onSubmit)}
className='ui form error'
>
<Field name='task' component={this.renderField} label='Task' />
<button className='ui primary button'>Add</button>
</form>
</div>
);
}
}
const validate = formValues => {
const errors = {};
if (!formValues.task) {
errors.task = 'Please enter at least 1 character';
}
return errors;
};
export default reduxForm({
form: 'todoForm',
touchOnBlur: false,
validate
})(Form);
The tutorial would be lengthy, so I will leave out how to use Redux Form. To understand how the Redux Form works, it is a good idea to try to customize your form referring to the documentation.
'todoForm'
is the name of this form. That is what we used in the action creator addTodo
.
When we click in the textbox and then remove the focus, it displays a validation error, so specify touchOnBlur: false
to disable it.
Next, let’s create a component for adding new todos. Create a new file named TodoCreate.js
in the components/todos
directory:
// components/todos/TodoCreate.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../../actions/todos';
import TodoForm from './TodoForm';
class TodoCreate extends Component {
onSubmit = formValues => {
this.props.addTodo(formValues);
};
render() {
return (
<div style={{ marginTop: '2rem' }}>
<TodoForm destroyOnUnmount={false} onSubmit={this.onSubmit} />
</div>
);
}
}
export default connect(
null,
{ addTodo }
)(TodoCreate);
All we have to do is render the TodoForm
. By setting destroyOnUnmount
to false
, we can disable that the Redux Form automatically destroys a form state in the Redux store when the component is unmounted. It is for displaying the form state in an editing form.
If we don’t need to specify a mapStateToProps
function, set null
into connect()
.
Let’s view and test the form. Open the Dashboard.js
file, and update as follows:
// components/todos/Dashboard.js
import TodoCreate from './TodoCreate'; // added
class Dashboard extends Component {
render() {
return (
<div className='ui container'>
<TodoCreate /> // added
<TodoList />
</div>
);
}
}
export default Dashboard;
Let’s take a break and create a header. Create a new folder named layout
, and then add a new file name Header.js
into it:
// components/layout/Header.js
import React, { Component } from 'react';
class Header extends Component {
render() {
return (
<div className='ui inverted menu' style={{ borderRadius: '0' }}>
<a className='header item'>TodoCRUD</a>
<a className='item'>Home</a>
</div>
);
}
}
export default Header;
Open the App.js
file and nest the Header
component:
// components/App.js
import Header from './layout/Header'; // added
class App extends Component {
render() {
return (
<Provider store={store}>
<Header /> // added
<Dashboard />
</Provider>
);
}
}
In this tutorial, the header is just an ornament.
First, we will create a history
object using the history package. We can use it for changing the current location. Create a new file named history.js
in the frontend/src
directory, and write the code below:
// frontend/src/history.js
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
Actions
Open the actions/todos.js
file, and add two new action creators:
// actions/todos.js
import history from '../history'; // added
import { GET_TODOS, GET_TODO, ADD_TODO, DELETE_TODO } from './types'; // added GET_TODO and DELETE_TODO
// GET TODO
export const getTodo = id => async dispatch => { // added
const res = await axios.get(`/api/todos/${id}/`);
dispatch({
type: GET_TODO,
payload: res.data
});
};
// DELETE TODO
export const deleteTodo = id => async dispatch => { // added
await axios.delete(`/api/todos/${id}/`);
dispatch({
type: DELETE_TODO,
payload: id
});
history.push('/');
We have created getTodo
to get a specific object and deleteTodo
to delete the object.
We are going to create a modal window for confirmation of deletion later. The history.push('/')
method automatically takes us from the modal window to the index page after removing an object.
Reducers
Open the reducers/todos.js
file, and add the actions to the reducer:
// reducers/todos.js
import _ from 'lodash'; // added
import { GET_TODOS, GET_TODO, ADD_TODO, DELETE_TODO } from '../actions/types'; // added GET_TODO and DELETE_TODO
export default (state = {}, action) => {
switch (action.type) {
// ...
case GET_TODO: // added
case ADD_TODO:
return {
...state,
[action.payload.id]: action.payload
};
case DELETE_TODO: // added
return _.omit(state, action.payload);
// ...
}
};
The GET_TODO
action is the same as the ADD_TODO
action, so we only need to set the case
. For the DELETE_TODO
action, use Lodash again as a shortcut.
Components
Let’s create the modal window I just mentioned. We will have it that looks like this:
Create a new file named Modal.js
in the components/layout
directory, and write as follows:
// components/layout/Modal.js
import React from 'react';
import ReactDOM from 'react-dom';
const Modal = props => {
return ReactDOM.createPortal(
<div onClick={props.onDismiss} className='ui active dimmer'>
<div onClick={e => e.stopPropagation()} className='ui active modal'>
<div className='header'>{props.title}</div>
<div className='content'>{props.content}</div>
<div className='actions'>{props.actions}</div>
</div>
</div>,
document.querySelector('#modal')
);
};
export default Modal;
To render the Modal
component outside the DOM hierarchy of the parent component, create a portal using createPortal()
. The first argument is the renderable child element and the second argument is the DOM element to render.
And then, open the index.html
file and add a container for the Modal inside the <body>
tag:
<!-- templates/frontend/index.html -->
<body>
<div id='app'></div>
<div id="modal"></div>
{% load static %}
<script src="{% static 'frontend/main.js' %}"></script>
</body>
Next, we will create a new component TodoDelete.js
in the components/todos
directory:
// components/todos/TodoDelete.js
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Modal from '../layout/Modal';
import history from '../../history';
import { getTodo, deleteTodo } from '../../actions/todos';
class TodoDelete extends Component {
componentDidMount() {
this.props.getTodo(this.props.match.params.id);
}
renderContent() {
if (!this.props.todo) {
return 'Are you sure you want to delete this task?';
}
return `Are you sure you want to delete the task: ${this.props.todo.task}`;
}
renderActions() {
const { id } = this.props.match.params;
return (
<Fragment>
<button
onClick={() => this.props.deleteTodo(id)}
className='ui negative button'
>
Delete
</button>
<Link to='/' className='ui button'>
Cancel
</Link>
</Fragment>
);
}
render() {
return (
<Modal
title='Delete Todo'
content={this.renderContent()}
actions={this.renderActions()}
onDismiss={() => history.push('/')}
/>
);
}
}
const mapStateToProps = (state, ownProps) => ({
todo: state.todos[ownProps.match.params.id]
});
export default connect(
mapStateToProps,
{ getTodo, deleteTodo }
)(TodoDelete);
The code is a bit long, but it is not so difficult. Define the helper functions that display the content and the action buttons on the modal window. Then, pass them as Props to the Modal component. onDismiss
is set to return to the index page when the dim part of the modal window is clicked.
We can retrieve the data from its own props by specifying ownProps
as the second argument to mapStateToProps
.
Let’s open the TodoList.js
file and put a delete button:
// components/todos/TodoList.js
import { Link } from 'react-router-dom'; // added
import { getTodos, deleteTodo } from '../../actions/todos'; // added deleteTodo
class TodoList extends Component {
// ...
render() {
return (
<div className='ui relaxed divided list' style={{ marginTop: '2rem' }}>
{this.props.todos.map(todo => (
<div className='item' key={todo.id}>
<div className='right floated content'> // added
<Link
to={`/delete/${todo.id}`}
className='small ui negative basic button'
>
Delete
</Link>
</div>
<i className='large calendar outline middle aligned icon' />
<div className='content'>
<a className='header'>{todo.task}</a>
<div className='description'>{todo.created_at}</div>
</div>
</div>
))}
</div>
);
}
}
// ...
export default connect(
mapStateToProps,
{ getTodos, deleteTodo } // added deleteTodo
)(TodoList);
Finally, we need to configure routing using React Router. Open the App.js
file and configure as follows:
// components/App.js
import { Router, Route, Switch } from 'react-router-dom'; // added
import history from '../history'; // added
import TodoDelete from './todos/TodoDelete'; // added
class App extends Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Header />
<Switch>
<Route exact path='/' component={Dashboard} />
<Route exact path='/delete/:id' component={TodoDelete} />
</Switch>
</Router>
</Provider>
);
}
}
The reason for using Router
instead of BrowserRouter is explained in the REACT TRAINING document as follows:
The most common use-case for using the low-level is to synchronize a custom history with a state management lib like Redux or Mobx. Note that this is not required to use state management libs alongside React Router, it’s only for deep integration.
Theexact
parameter specified inRoute
returns a route only if thepath
exactly matches the current URL.
That concludes this chapter. Try deleting some objects and see if it works.
This is the last chapter. We are almost done, so let’s keep going!
Actions
Open the actions/todos.js
file, and add a new action creator:
// actions/todos.js
import { GET_TODOS, GET_TODO, ADD_TODO, DELETE_TODO, EDIT_TODO } from './types'; // added EDIT_TODO
// EDIT TODO
export const editTodo = (id, formValues) => async dispatch => {
const res = await axios.patch(`/api/todos/${id}/`, formValues);
dispatch({
type: EDIT_TODO,
payload: res.data
});
history.push('/');
};
Reducers
Open the reducers/todos.js
file, and add the action to the reducer:
// reducers/todos.js
import {
GET_TODOS,
GET_TODO,
ADD_TODO,
DELETE_TODO,
EDIT_TODO // added
} from '../actions/types';
export default (state = {}, action) => {
switch (action.type) {
// ...
case GET_TODO:
case ADD_TODO:
case EDIT_TODO: // added
return {
...state,
[action.payload.id]: action.payload
};
// ...
}
};
Components
Create a new component TodoEdit.js
in the components/todos
directory:
// components/todos/TodoEdit.js
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTodo, editTodo } from '../../actions/todos';
import TodoForm from './TodoForm';
class TodoEdit extends Component {
componentDidMount() {
this.props.getTodo(this.props.match.params.id);
}
onSubmit = formValues => {
this.props.editTodo(this.props.match.params.id, formValues);
};
render() {
return (
<div className='ui container'>
<h2 style={{ marginTop: '2rem' }}>Edit Todo</h2>
<TodoForm
initialValues={_.pick(this.props.todo, 'task')}
enableReinitialize={true}
onSubmit={this.onSubmit}
/>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
todo: state.todos[ownProps.match.params.id]
});
export default connect(
mapStateToProps,
{ getTodo, editTodo }
)(TodoEdit);
Specify an object in initialValues
. We can get only the value of task
using the _.pick
function of Lodash. In addition, set enableReinitialize
to true
so that we can also get the value when the page is reloaded. Pass these optional properties to the TodoForm
.
Open the TodoList.js
file and update <a className='header'>{todo.task}</a>
as follows:
// components/todos/TodoList.js
<Link to={`/edit/${todo.id}`} className='header'>
{todo.task}
</Link>
Let’s add the new component to the App.js
file:
// components/App.js
import TodoEdit from './todos/TodoEdit'; // added
class App extends Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Header />
<Switch>
<Route exact path='/' component={Dashboard} />
<Route exact path='/delete/:id' component={TodoDelete} />
<Route exact path='/edit/:id' component={TodoEdit} /> // added
</Switch>
</Router>
</Provider>
);
}
}
Finally, let’s change the text of the button on the edit form from “Add” to “Update”. Open the TodoForm.js
file and update as follows:
// components/todos/TodoForm.js
class TodoForm extends Component {
// ...
render() {
const btnText = `${this.props.initialValues ? 'Update' : 'Add'}`; // added
return (
<div className='ui segment'>
<form
onSubmit={this.props.handleSubmit(this.onSubmit)}
className='ui form error'
>
<Field name='task' component={this.renderField} label='Task' />
<button className='ui primary button'>{btnText}</button> // updated
</form>
</div>
);
}
}
Now, click on any task on the index page and try editing:
The edit form
You should have been able to change the value from the form.
This tutorial ends here. The source code of this app is available on GitHub.
#django #reactjs #redux #web-development