https://qiita.com/sand/items/80a67da0a44b042f0bc3


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

1.Django Rest API と Reactアプリ

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

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

  • (1)サーバは、DjangoプロジェクトでRest APIを開発・単体テスト
  • (2)クライアントは、create-react-appで開発・単体テスト
  • (3)サーバ側でCORS設定を行い、クライアントからRest APIにアクセスする

(1)と(2)はそれぞれ独立して開発を行い、それぞれに動作確認します。
その後(3)の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

2.サーバサイド - djangotodo

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

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

2-1.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サーバに構築していますので、サーバのドメイン名を入力してアクセスを許可します。

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の初期画面を確認できます。

2-2.Todoアプリ作成

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

django-admin startapp todos

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

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に追加します。

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テーブルの操作を行えるようにします。

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

Todoを追加し、テストデータを作っておきます。
image.png

2-3.Django Rest Frameworkの設定

Djangoには、djangorestframeworkというRest APIを簡単に構築できるライブラリがあります。
Django Rest Framework with React Tutorial

インストールします。

pip install djangorestframework

INSTALLED_APPSを更新します。

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はデバッグのために設定しました。これを使うためには、以下のコードも必要になります。

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

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

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の定義です

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の明示的な定義が必要になり複雑です。

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を定義します。

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

2-4.単体テスト

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

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

先ほど管理画面から入力したテストデータが表示されます。
image.png

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

HTTPieコマンドを使っても簡単にテストできます。
HTTPie—aitch-tee-tee-pie—is a command line HTTP client with an intuitive UI

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

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

2-5.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はトップに置く必要があります。

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アプリ完成後に行います。

3.フロントエンド - frontend

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

3-1.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は以下の通りです。

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"
    ]
  }
}

3-2.redux-devtools-extension

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

以下の画面のように、Chromeの拡張機能で専用ウィンドが開き、Reduxのactionやstateが可視化されます。
image.png

3-3.antd

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

React UI library の antd について (1) - Button

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

3-4.単体テスト

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

frontendディレクトリ直下にdb.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

3-5.ソースコード

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

(1)トップ

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

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はメイン画面の枠組みの定義です。

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);

(2)Reducer & Action

reducerの定義です

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の定義です

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の定数の定義です。

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

(3)API

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

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

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

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}/`);
}

(4)components

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

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

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を使っているので、とても簡潔に記述できています。

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でエラーが発生した場合に、表示されるメッセージです。

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

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

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

4.ReactとDjangoの結合

現在は以下の状況です

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

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

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

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

単体の時と同じですが、結合時の画面です。
image.png

今回は以上です。


+ Recent posts