自動化無しに生活無し

WEBとかAIとかLinux関係をひたすら書く備忘録系ブログ

【Restful】Django+Reactビギナーが40分で掲示板アプリ(SPA)を作る方法【axios】

thumbnail

Djangoビギナーが40分で掲示板アプリを作る方法 』を終え、DjangoとReactを組み合わせ、SPAを作りたいと思った方向け。

40分はあくまでも目安。

内部構造に関しては、『DjangoとReactを組み合わせる方法論と問題の考察 』に解説がある。

2023年5月3日現在、編集機能の実装を考慮中。近日追記予定。

2024年4月30日追記。モーダルダイアログを使用して編集機能を搭載。

流れ

  1. React、Djangoの各プロジェクトを作る
  2. Djangoの必要なライブラリをインストール
  3. Djangoのsettings.pyの編集
  4. Djangoのmodels.pyの編集
  5. Djangoのserializers.pyの編集
  6. Djangoのviews.pyの編集
  7. Djangoのurls.pyの編集
  8. Reactの必要なライブラリをインストール
  9. Reactのpackage.jsonの編集
  10. Reactのindex.jsの編集
  11. Reactのsrc/App.jsの編集
  12. Reactのsrc/components/Modal.jsの編集
  13. Reactのsrc/App.cssの編集
  14. DjangoとReactの開発用サーバーをそれぞれ起動

React、Djangoの各プロジェクトを作る

mkdir django_react_todo
cd django_react_todo

mkdir backend

npx create-react-app frontend

Djangoの必要なライブラリをインストール

cd backend
virtualenv venv 

source ./venv/bin/activate 

pip install django djangorestframework django-cors-headers
django-admin startproject config .

Djangoのsettings.pyの編集

アプリを作る。

python3 manage.py startapp bbs

インストールする。

"""
Django settings for config project.

Generated by 'django-admin startproject' using Django 4.1.5.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-8%$blv$7^^$pm(s#r(!tr94k9ch7p@wissbgbz!jep%@xlsced'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    "bbs.apps.BbsConfig",
    'corsheaders',
    'rest_framework',

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

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',
]
CORS_ORIGIN_WHITELIST = [
     'http://localhost:3000'
]


ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Djangoのmodels.pyの編集

from django.db import models

class Topic(models.Model):

    comment     = models.CharField(verbose_name="コメント",max_length=2000)

マイグレーションしておく

python3 manage.py makemigrations 
python3 manage.py migrate

Djangoのserializers.pyの編集

from rest_framework import serializers
from .models import Topic

class TopicSerializer(serializers.ModelSerializer):
    class Meta:
        model = Topic
        fields = ("id","comment")

Djangoのviews.pyの編集

from django.shortcuts import render
from rest_framework import viewsets
from .serializers import TopicSerializer
from .models import Topic

class TopicView(viewsets.ModelViewSet):
    serializer_class    = TopicSerializer
    queryset            = Topic.objects.all()

Djangoのurls.pyの編集

"""config URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path,include


from rest_framework import routers
from bbs import views

router = routers.DefaultRouter()
router.register(r"topics", views.TopicView, "topic")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
]

Reactの必要なライブラリをインストール

npm install axios@1.4.0
npm install bootstrap@5.2.3
npm install reactstrap@9.1.9

Reactのpackage.jsonの編集

proxyを追加する。

{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:8000",
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^1.4.0",
    "bootstrap": "^5.2.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Reactのindex.jsの編集

bootstrapを読み込むように編集する。

import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.css'; // ←追加
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

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

Reactのsrc/App.jsの編集

import React, { Component } from "react";

import Modal from "./components/Modal";
import axios from "axios";

// リクエスト送信用のaxiosとモーダルをimport
class App extends Component {

    // Stateの設計からやり直す。
    constructor(props) {
        super(props);
        this.state = {
            topicList: [],
            modal: false,
            activeItem: {
                comment: "",
            },
        };
    }
    componentDidMount() {
        this.refreshList();
    }

    refreshList     = () => {
        axios
            .get("/api/topics/")
            .then((res) => this.setState({ topicList: res.data }))
            .catch((err) => console.log(err));
    };

    // モーダルダイアログを表示させ、idがあれば編集処理のダイアログを、なければ新規作成のダイアログを表示させる
    handleSubmit    = (item) => {

        if (item.id){
            axios
                .put(`/api/topics/${item.id}/`, item)
                .then((res) => {
                    this.refreshList();
                })
                .catch((err) => console.log(err));
        }
        else{
            axios
                .post("/api/topics/", item)
                .then((res) => {
                    this.refreshList();
                })
                .catch((err) => console.log(err));
        }

        this.closeModal();
    };

    handleDelete    = (item) => {
        axios
            .delete(`/api/topics/${item.id}/`)
            .then((res) => this.refreshList());
    };


    openModal       = (item) => {
        // 編集時はコメントをセット
        if (item.id){
            this.setState({ activeItem: item, modal: true });
        }
        else{
            this.setState({ activeItem: { comment:"" }, modal: true });
        }
    };
    closeModal      = () => {
        this.setState({ activeItem: { comment:"" }, modal: false });
    };


    // 改行をする
    linebreaksbr    = (string) => {

        // React.Fragment は <></> と同じであるが、今回はkeyを指定する必要があるため、React.Fragmentとする 
        return string.split('\n').map((item, index) => (
            <React.Fragment key={index}>
                {item}
                {index !== string.split('\n').length - 1 && <br />}
            </React.Fragment>
        )); 
    };

    renderItems     = () => {
        return this.state.topicList.map((item) => (
            <div className="border" key={item.id}>
                <div>{item.id}</div>
                <div>{ this.linebreaksbr(item.comment) }</div>
                <div className="text-end">
                    <input type="button" className="mx-1 btn btn-success" value="編集" onClick={ () => this.openModal(item) } />
                    <input type="button" className="mx-1 btn btn-danger" value="削除" onClick={ () => this.handleDelete(item) } />
                </div>
            </div>
        ));
    };
    render() {
        return (
            <>
                <h1 className="bg-primary text-white text-center">簡易掲示板</h1>
                <main className="container">

                    <input className="btn btn-primary" type="button" onClick={ () => this.openModal(this.state.activeItem) } value="新規作成" />
                    { this.state.modal ? ( <Modal activeItem    = {this.state.activeItem}
                                                  handleSubmit  = {this.handleSubmit}
                                                  closeModal    = {this.closeModal} /> ): null }
                    { this.renderItems() }

                </main>
            </>
        );
    };
}

export default App;

Reactのsrc/components/Modal.jsの編集

import React, { Component } from "react";

export default class CustomModal extends Component {
    constructor(props) {
        super(props);
        this.state = {
            activeItem: this.props.activeItem,
        };
    }

    handleChange    = (e) => {
        let { name , value }    = e.target;
        const activeItem        = { ...this.state.activeItem, [name]: value };
        this.setState({ activeItem });
    }

    render() {
        const { handleSubmit, activeItem, closeModal } = this.props;

        return (
            <>
            <div className="modal_area" >
                <div className="modal_bg_area" onClick={closeModal}></div>
                <div className="modal_content_area">
                    <form>
                        
                        { activeItem.id ? ( <h2>編集</h2> ) : ( <h2>新規作成</h2> ) }
                        <textarea className="form-control" name="comment" onChange={this.handleChange} value={this.state.activeItem.comment}></textarea>
                        <input className="btn btn-success" type="button" onClick={ () => handleSubmit(this.state.activeItem) } value="保存" />
                    </form>
                </div>
            </div>

            </>
        );
    }
}

Reactのsrc/App.cssの編集

/* モーダルダイアログの装飾 */
.modal_area{
    position:fixed;
    top:0;
    left:0;
    width:100vw;
    height:100vh;
}

.modal_bg_area{
    position:absolute;

    top:0;
    left:0;

    width:100vw;
    height:100vh;

    background:rgba(0,0,0, 0.5);
}
.modal_content_area{

    position:absolute;
    top:50%;
    left:50%;
    transform:translate(-50%, -50%);

    background: bisque;
    padding:2rem;

    z-index:100;

    border-radius:1rem;
}

DjangoとReactの開発用サーバーをそれぞれ起動

python3 manage.py runserver
npm start

動かすとこうなる。

新規作成用のボタンを押して、モーダルダイアログが表示される。テキストに入力をして投稿する。

結論

説明不足感があるので、また後日説明を追記する予定。

ソースコード

https://github.com/seiya0723/react-django-startup-bbs/

関連記事

DjangoとReactのTodoアプリ(SPA)を解析する

スポンサーリンク

シェアボタン

Twitter LINEで送る Facebook はてなブログ