Reactアプリから Django Rest API を叩いてみる 


# １．Django Rest API と Reactアプリ

以前、[「DjangoのページをReactで作る - Webpack4」](https://qiita.com/sand/items/15da91117c680a618c2b)という記事を書きました。DjangoのページでReactを使うための、開発環境の構築を紹介したものですが、これはどちらかと言えば、Djangoの開発環境にReactの開発環境を「従わせた」ものでした。BabelやWebpackの設定はDjangoの環境に合わせる形で手動で行いました。


今回は**Django**と**React**の開発環境を完全に独立させます。特にReactではcreate-react-appを使いますので、簡単に開発環境を構築できます。

* （１）サーバは、**Django**プロジェクトでRest APIを開発・単体テスト
* （２）クライアントは、**create-react-app**で開発・単体テスト
* （３）サーバ側で**CORS**設定を行い、クライアントからRest APIにアクセスする


（１）と（２）はそれぞれ独立して開発を行い、それぞれに動作確認します。
その後（３）の**CORS**の設定を行い、クライアントとサーバの連結を確認します。

今回はTodoアプリを作成していきます。

環境としては、**todo-react**というディレクトリの下に、**djangotodo**というDjangoプロジェクトと、**frontend**というcreate-react-appのプロジェクトを作成します。


```
todo-react
│
├── djangotodo
│   ├── db.sqlite3
│   ├── djangotodo
│   ├── manage.py
│   └── todos
├── frontend
    ├── db.json
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    └── src
```



# ２．サーバサイド - djangotodo

サーバサイドでは、だいたい以下のような作業を行います。

* DjangoでTodoプロジェクトを作る
* **djangorestframework**をインストールし**Rest API**を構築する
* 単体テストを行う
* 来たるべき総合テストに備えて、**django-cors-headers**をインストールし**CORS**の設定を行っておく

##２－１．Djangoプロジェクト作成

venvで環境を作ってから、Djangoのプロジェクトを開始します。

```
python -m venv todo-react
source todo-react/bin/activate
cd todo-react
pip freeze
pip install django
django-admin startproject djangotodo
cd djangotodo/
```


私の環境は、DjangoはRemoteサーバに構築していますので、サーバのドメイン名を入力してアクセスを許可します。


```python:djangotodo/settings.py
---
ALLOWED_HOSTS = ["www.mypress.jp"]
---
```

DBを初期化します。

```
python manage.py migrate
```

ここまででDjangoのアプリを立ち上げます。


```
python manage.py runserver 0:8080
```

http://www.mypress.jp:8080/ で、Djangoの初期画面を確認できます。




##２－２．Todoアプリ作成

DjangoでTodoアプリを作成します

```
django-admin startapp todos
```

TodoアプリのModelを定義します。

```pyton:todos/models.py
# todos/models.py
from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    status = models.CharField(default='Unstarted', max_length=100)

    def __str__(self):
        """A string representation of the model."""
        return self.title
```


TodoアプリをINSTALLED_APPSに追加します。

```python:djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todos', # New
]
---
```

migrationファイルを作成して、DBに反映させます。

```
python manage.py makemigrations todos
python manage.py migrate todos
```

adminを設定して、管理画面からTodoテーブルの操作を行えるようにします。

```python:todos/admin.py
# todos/admin.py
from django.contrib import admin

from .models import Todo

admin.site.register(Todo)
```

管理者を追加します

```
python manage.py createsuperuser
```

サーバを起動します。

```
python manage.py runserver 0:8080
```

管理画面にアクセスします。

```
http://www.mypress.jp:8080/admin/
```

ログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/b42468a0-0de8-97df-c4a3-268a72f23198.png)


Todoを追加し、テストデータを作っておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/0913d3ee-d9e0-870a-9c5a-d59ca3dc723d.png)




##２－３．Django Rest Frameworkの設定

Djangoには、djangorestframeworkというRest APIを簡単に構築できるライブラリがあります。
[Django Rest Framework with React Tutorial](
https://wsvincent.com/django-rest-framework-react-tutorial/)


インストールします。

```
pip install djangorestframework
```

INSTALLED_APPSを更新します。


```python:djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # New
    'todos',
]

# New
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'EXCEPTION_HANDLER': 'djangotodo.todos.utils.custom_exception_handler'
}
---
```

**EXCEPTION_HANDLER**はデバッグのために設定しました。これを使うためには、以下のコードも必要になります。

```python:todos/utils.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    # Call REST framework's default exception handler first,
    # to get the standard error response.
    response = exception_handler(exc, context)

    # Now add the HTTP status code to the response.
    if response is not None:
        response.data['status_code'] = response.status_code

    return response
```


Django Rest Frameworkの設定のため、以下の3つを定義します。

* **urls.py**　：URLルート
* **serializers.py**　：dateをJSONに変換
* **views.py**　：APIエンドポイントにロジックを適用


この辺を詳しく知るためには、以下の公式ドキュメントを最初に読みましょう。
**[Tutorial 1: Serialization](https://www.django-rest-framework.org/tutorial/1-serialization/)**



URL Pathの定義です。リクエスト時のパスは末尾がスラッシュ(/)で終わっている必要があります。

```python:djangotodo/urls.py
from django.urls import path, include  # New
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todos.urls')), # New
]
```

todoのURL Pathの定義です

```python:todos/urls.py
# todos/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.ListTodo.as_view()),
    path('<int:pk>/', views.DetailTodo.as_view()),
]
```

serializersとは、ざっくり言って、modelデータをJSONで出力するための機能です。ここでは**ModelSerializer** classを利用しているので、とてもシンプルに定義できます。**SnippetSerializer** classを利用する方法もありますが、この場合createやupdateの明示的な定義が必要になり複雑です。


```python:todos/serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        fields = (
            'id',
            'title',
            'description',
            'status',
        )
        model = Todo
```




rest_frameworkを使って、viewsを定義します。


```python:todos/views.py
# todos/views.py
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from todos.models import Todo
from todos.serializers import TodoSerializer


@csrf_exempt
def todo_list(request):
    """
    List all todos, or create a new todo.
    """
    if request.method == 'GET':
        todos = Todo.objects.all()
        serializer = TodoSerializer(todos, many=True)
        return JsonResponse(serializer.data, safe=False)

    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = TodoSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)


@csrf_exempt
def todo_detail(request, pk):
    """
    Retrieve, update or delete a todo.
    """
    try:
        todo = Todo.objects.get(pk=pk)
    except Todo.DoesNotExist:
        return HttpResponse(status=404)

    if request.method == 'GET':
        serializer = TodoSerializer(todo)
        return JsonResponse(serializer.data)

    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = TodoSerializer(todo, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)

    elif request.method == 'DELETE':
        todo.delete()
        return HttpResponse(status=204)
```


今回はロジックを明示的に記述する仕方でviews.pyを定義しましたが、慣れたら**generic class-based views**を使った方が良いでしょう。コーディング量を劇的に減らすことが可能です。
**[Tutorial 3: Class-based Views](https://www.django-rest-framework.org/tutorial/3-class-based-views/)**



##２－４．単体テスト


ブラウザからアクセスしてみます。

```
http://www.mypress.jp:8080/api/
```

先ほど管理画面から入力したテストデータが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/bfefdb85-6234-3e08-1de9-bbe2771ee9ba.png)


rest_frameworkはTodoの追加フォームも表示してくれます。便利です。

HTTPieコマンドを使っても簡単にテストできます。
[HTTPie—aitch-tee-tee-pie—is a command line HTTP client with an intuitive UI](https://httpie.org/)


例えば以下のコマンドで「タスク追加」を確認できます。

```
http POST http://www.mypress.jp:8080/api/  title=a description=b status=Unstarted
```



##２－５．CORS

これは本来なら、frontendのreactアプリ作成後の、最後に設定し確認するものです。しかしサーバでの設定ですので、ここでやっておきます。

また、**CORSの確認テストで試行錯誤する時には、その都度必ずブラウザのキャッシュをクリアーすることを強くお勧めします。私はこれを怠り嵌りました！**

```
Access to XMLHttpRequest at 'http://www.mypress.jp:8080/api' from 
origin 'http://www.mypress.jp:3000' has been blocked by 
CORS policy: No 'Access-Control-Allow-Origin' header is present on 
the requested resource.
```

以上のエラーを回避するためにサーバ側でCORSの設定を行う必要があります。**django-cors-headers**をインストールします。

```
pip install django-cors-headers
```

settings.pyを更新します。newコメントがついている4か所が修正箇所です。CorsMiddlewareはトップに置く必要があります。


```python:djangotodo/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders', # new
    'todos',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'EXCEPTION_HANDLER': 'djangotodo.todos.utils.custom_exception_handler'
}

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # new topに置く
    'django.middleware.common.CommonMiddleware', # new
    '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',
]


# new
CORS_ORIGIN_WHITELIST = [
    'http://www.mypress.jp:3000',
]
# CORS_ORIGIN_ALLOW_ALL = False
---
```

以上でCORSの設定は終わりです。動作確認はReactアプリ完成後に行います。




# ３．フロントエンド - frontend

Reactプログラムは、reduxと**redux-thunk**を使い、action（非同期関数）から、Rest APIを叩きます。APIは**axios**で実装します。また最低限のUIを実装し、CSSを含めたコーディング量を減らすため、**antd**も利用します。一応最後に、全ソースを掲載します。少し長くなるのですが、不明な点を無くすため。


## ３－１．Reactプロジェクト作成

create-react-appを使って、Reactプロジェクトを作成します。必要なパッケージをインストールします。

```
create-react-app frontend
cd frontend
npm install --save axios react-redux redux redux-thunk redux-devtools-extension antd
```


package.jsonは以下の通りです。


```JSON:package.json
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "antd": "^3.20.5",
    "axios": "^0.19.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.1.0",
    "react-scripts": "3.0.1",
    "redux": "^4.0.4",
    "redux-devtools-extension": "^2.13.8",
    "redux-thunk": "^2.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
```


## ３－２．redux-devtools-extension

**redux-devtools-extension**はReduxアプリの開発ツールの一つで、ブラウザの拡張機能からReduxの状態管理を可視化してくれます。別途、[Chromeの拡張機能](https://github.com/zalmoxisus/redux-devtools-extension)を設定する必要があります。

以下の画面のように、Chromeの拡張機能で専用ウィンドが開き、Reduxのactionやstateが可視化されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/1bad9e0d-4e75-3b66-9a22-2cccc452d36a.png)


## ３－３．antd

React のUI libraryである**antd**を使います。特に**List**コンポネントを使うことで、ソースコードをとても簡潔にすることができました。入力フォームには**Form**コンポーネントを使いました。

[React UI library の antd について (1) - Button](https://qiita.com/sand/items/35422a669e6021cccb4f)


個人的には、antdを使うことにより、面倒なstyleを指定することが少なくなるので助かります。



## ３－４．単体テスト 

Djangoと結合する前に、**json-server**を使って単体テストを行います。

frontendディレクトリ直下に**db.json**ファイルを作ります。


```JSON:db.json
{
  "api": [
    {
      "id": 1,
      "title": "Reduxのお勉強",
      "description": "特に非同期actionについて",
      "status": "In Progress"
    },
    {
      "id": 2,
      "title": "ES6のお勉強",
      "description": "Promiseについて",
      "status": "In Progress"
    },
    {
      "id": 3,
      "title": "朝食",
      "description": "忘れずに食べること",
      "status": "Completed"
    },
    {
      "title": "掃除",
      "description": "要らない本は捨てる",
      "status": "In Progress",
      "id": 4
    },
    {
      "title": "草刈り",
      "description": "夏草に要注意！",
      "status": "Unstarted",
      "id": 5
    }
  ]
}
```

frontendディレクトリ直下で、json-serverを起動します。

```
json-server --host www.mypress.jp --watch db.json -p 3003
```

状態「Unstarted」、「In Progress」、「Completed」毎にTodo一覧が表示されます。以下の画面になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/aa4d7f45-b3fc-a01c-bc33-3bf941c0c56e.png)






## ３－５．ソースコード

frontend/src直下のソースツリーです。

```
├── App.js
├── actions
│   └── index.js
├── api
│   └── index.js
├── components
│   ├── FlashMessage.js
│   ├── TaskList.js
│   └── TasksPage.js
├── constants
│   └── index.js
├── index.js
└── reducers
    └── index.js
```

###（１）トップ


主に、Reduxの設定を行い、App.jsを呼びます。

```JSX:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import tasksReducer from './reducers';
import App from './App';

const rootReducer = (state = {}, action) => {
  return {
    tasks: tasksReducer(state.tasks, action),
  };
};

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
```


App.jsはメイン画面の枠組みの定義です。

```JSX:src/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import TasksPage from './components/TasksPage';
import FlashMessage from './components/FlashMessage';
import { createTask, editTask, deleteTask, fetchTasks } from './actions';
import 'antd/dist/antd.css';

class App extends Component {
  componentDidMount() {
    this.props.dispatch(fetchTasks());
  }

  onCreateTask = ({ title, description }) => {
    this.props.dispatch(createTask({ title, description }));
  };

  onStatusChange = (id, status) => {
    this.props.dispatch(editTask(id, { status }));
  };

  onDeleteTask = (id) => {
    this.props.dispatch(deleteTask(id));
  };

  render() {
    return (
      <div>
        {this.props.error && <FlashMessage message={this.props.error} />}
        <div>
          <TasksPage
            tasks={this.props.tasks}
            onCreateTask={this.onCreateTask}
            onStatusChange={this.onStatusChange}
            isLoading={this.props.isLoading}
          />
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  const { tasks, isLoading, error } = state.tasks;
  return { tasks, isLoading, error };
}

export default connect(mapStateToProps)(App);
```


###（２）Reducer & Action

reducerの定義です

```JSX:src/reducers/index.js
const initialState = {
  tasks: [],
  isLoading: false,
  error: null,
};

export default function tasks(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_TASKS_STARTED': {
      return {
        ...state,
        isLoading: true,
      };
    }
    case 'FETCH_TASKS_SUCCEEDED': {
      return {
        ...state,
        tasks: action.payload.tasks,
        isLoading: false,
      };
    }
    case 'FETCH_TASKS_FAILED': {
      return {
        ...state,
        isLoading: false,
        error: action.payload.error,
      };
    }
    case 'CREATE_TASK_SUCCEEDED': {
      return {
        ...state,
        tasks: state.tasks.concat(action.payload.task),
      };
    }
    case 'EDIT_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.map(task => {
        if (task.id === payload.task.id) {
          return payload.task;
        }

        return task;
      });
      return {
        ...state,
        tasks: nextTasks,
      };
    }
    case 'DELETE_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.filter(task => task.id !== payload.id)
      return {
        ...state,
        tasks: nextTasks,
      };
    }
    default: {
      return state;
    }
  }
}
```


actionの定義です

```JSX:src/actions/index.js
import * as api from '../api';

function fetchTasksSucceeded(tasks) {
  return {
    type: 'FETCH_TASKS_SUCCEEDED',
    payload: {
      tasks,
    },
  };
}

function fetchTasksFailed(error) {
  return {
    type: 'FETCH_TASKS_FAILED',
    payload: {
      error,
    },
  };
}

function fetchTasksStarted() {
  return {
    type: 'FETCH_TASKS_STARTED',
  };
}

export function fetchTasks() {
  return dispatch => {
    dispatch(fetchTasksStarted());

    api
      .fetchTasks()
      .then(resp => {
        dispatch(fetchTasksSucceeded(resp.data));
      })
      .catch(err => {
        dispatch(fetchTasksFailed(err.message));
      });
  };
}

function createTaskSucceeded(task) {
  return {
    type: 'CREATE_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function createTask({ title, description, status = 'Unstarted' }) {
  return dispatch => {
    api.createTask({ title, description, status }).then(resp => {
      dispatch(createTaskSucceeded(resp.data));
    });
  };
}

function editTaskSucceeded(task) {
  return {
    type: 'EDIT_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function editTask(id, params = {}) {
  return (dispatch, getState) => {
    const task = getTaskById(getState().tasks.tasks, id);
    const updatedTask = Object.assign({}, task, params);
    api.editTask(id, updatedTask).then(resp => {
      dispatch(editTaskSucceeded(resp.data));
    });
  };
}


function getTaskById(tasks, id) {
  return tasks.find(task => task.id === id);
}


function deleteTaskSucceeded(id) {
  return {
    type: 'DELETE_TASK_SUCCEEDED',
    payload: {
      id,
    },
  };
}

export function deleteTask(id) {
  return (dispatch, getState) => {
    api.deleteTask(id).then(resp => {
      console.log(resp)
      dispatch(deleteTaskSucceeded(id));
    });
  };
}
```

statusの定数の定義です。

```JSX:src/constants/index.js
export const TASK_STATUSES = ['Unstarted', 'In Progress', 'Completed'];
```



###（３）API


Rest APIのインターフェースモジュールです。ここで注意が必要なのは、**DjangoのPOSTの場合、パスの末尾に、'/api/' のように、スラッシュが必要だということです。 '/api' ではだめです。**

'/api' のGETの場合、自動的に末尾にスラッシュを付け直してリダイレクトしてOKになります。POSTでもリダイレクトしてくれますが、リダイレクト時にPOST dataが落ちてしまい、結果的にエラーとなります。
**json-server**ではテストが通ってもDjangoではNGになるので注意が必要です。

POSTでBAD Requestエラーとなる場合は、paramsの中身もチェックしてみましょう。私はここで躓いて、actionが正しいデータを渡してくれているのかを確認せずに、半日も悩んでしまいました。


```JSX:src/api/index.js
import axios from 'axios';

// const API_BASE_URL = 'http://www.mypress.jp:3003'; // json-server用
const API_BASE_URL = 'http://www.mypress.jp:8080'; // Django用

const client = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json'
  },
});

export function fetchTasks() {
  return client.get('/api/');
}

export function createTask(params) {
  console.log(params)
  return client.post('/api/', params);
}

export function editTask(id, params) {
  return client.put(`/api/${id}`, params);
}

export function deleteTask(id) {
  return client.delete(`/api/${id}/`);
}
```



###（４）components

TasksPage.jsはTasksPageクラスの他に。タスク追加フォームであるAddTaskFormクラスを定義しています。別ファイルにすべきかと思いましたが、面倒なので一緒にしました。

タスク追加フォームには、**antd**の**Form** componentを使っています。validateが統一的に行えるので便利ですが、少しコードが複雑になります。詳しくは[「React UI library の antd について (3) - redux-form」](https://qiita.com/sand/items/a0cf3fa3a1a17741c06b)も参照してください。


```JSX:src/components/TasksPage.js
import React, { Component } from 'react';
import { Form, Input, Icon, Button } from 'antd';

import TaskList from './TaskList';
import { TASK_STATUSES } from '../constants';

class TasksPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      showNewCardForm: false,
    };
  }

  toggleForm = () => {
    this.setState({ showNewCardForm: !this.state.showNewCardForm });
  };

  render() {
    if (this.props.isLoading) {
      return (
        <div>
          Loading...
        </div>
      );
    }

    return (
      <div>
        <div>
          <Button type="primary" onClick={this.toggleForm}>＋タスク追加</Button>
        </div>
        {this.state.showNewCardForm && <WrappedAddTaskForm onCreateTask={this.props.onCreateTask} />}
        <div>
          {TASK_STATUSES.map(status => {
            const statusTasks = this.props.tasks.filter(
              task => task.status === status
            );
            return (
            <div style={{ margin: "25px 20px 25px 20px" }}>
              <h2>{status}</h2>
              <TaskList
                key={status}
                status={status}
                tasks={statusTasks}
                onStatusChange={this.props.onStatusChange}
                onDeleteTask={this.props.onDeleteTask}
              />
            </div>
            );
          })}
        </div>
      </div>
    );
  }
}

export default TasksPage;



class AddTaskForm extends React.Component {
  componentDidMount() {
    // To disabled submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
        this.props.onCreateTask(values)
      }
    });
  };

  render() {
    const { getFieldDecorator, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const taskError = isFieldTouched('task') && getFieldError('task');
    const descriptionError = isFieldTouched('description') && getFieldError('description');
    const buttonDisable = getFieldError('task') || getFieldError('description')

    return (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <Form.Item validateStatus={taskError ? 'error' : ''} help={taskError || ''}>
          {getFieldDecorator('task', {
            rules: [{ required: true, message: 'taskを入力してください！' }],
          })(
            <Input
              prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="task"
            />,
          )}
        </Form.Item>
        <Form.Item validateStatus={descriptionError ? 'error' : ''} help={descriptionError || ''}>
          {getFieldDecorator('description', {
            rules: [{ required: true, message: 'descriptionを入力してください！' }],
          })(
            <Input
              prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="description"
            />,
          )}
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" disabled={buttonDisable}>
            タスク追加
          </Button>
        </Form.Item>
      </Form>
    );
  }
}

const WrappedAddTaskForm = Form.create({ name: 'add_task_form' })(AddTaskForm);
```



タスク一覧の表示です。antdの**List** componentを使っているので、とても簡潔に記述できています。



```JSX:src/components/TaskList.js
import React from 'react';
import { List, Card } from 'antd';
import { TASK_STATUSES } from '../constants';


const TaskList = props => {
  return (
    <List
      grid={{ gutter: 16, column: 4 }}
      dataSource={props.tasks}
      renderItem={item => (
        <List.Item>
          <Card title={item.title}>{item.description}</Card>
          <select value={item.status} onChange={(e) => {onStatusChange(e, item.id)}}>
            {TASK_STATUSES.map(status => (
              <option key={status} value={status}>{status}</option>
            ))}
          </select>
          <Button type="danger" onClick={()=>{props.onDeleteTask(item.id)}}>
            タスク削除
          </Button>
        </List.Item>
      )}
    />
  );

  function onStatusChange(e, id) {
    props.onStatusChange(id, e.target.value);
  }
};

export default TaskList;
```



actionでエラーが発生した場合に、表示されるメッセージです。

```JSX:src/components/FlashMessage.js
import React from 'react';

export default function FlashMessage(props) {
  return (
    <div>
      {props.message}
    </div>
  );
}

FlashMessage.defaultProps = {
  message: 'An error occurred',
};
```



#４．ReactとDjangoの結合

現在は以下の状況です

* 【サーバサイド】Django単体での動作を確認済み
* 【フロントエンド】React単体での動作を確認済み

最後にサーバサイドとフロントエンドを結合して動作を確認します。
単体で成功しても、結合で失敗し時間を費やすことになるのは、よくあることです。今回も以下の2点でだいぶ時間を浪費してしまいました。

* CORSの設定のデバッグに時間を要した（ブラウザキャッシュの問題）
* POSTリクエストエラーに時間を要した（リクエストパスの末尾のスラッシュを忘れた ＆ POSTデータの属性"title"が間違っていた）

特にjson-serverはサーバ側は、特にチェック無しで通りますが、Djangoの場合はデータの属性名が違っていたりすると、当然はじかれます。この点を忘れて迷路に迷うことにならないように注意します。

単体の時と同じですが、結合時の画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/f2ce09f7-0b20-10ea-e9c1-6cb3f77aa9b3.png)



今回は以上です。