1.Django4で認証周りを実装した解説動画がYouTubeにすでに投稿されていた
Django4とReactを連携させて認証機能付きのブログとか実装している方がいないかなあと思って探してみたらこちらの動画へと辿り着きました。投稿者の方がソースコードをGitHubに公開してくださっているおかげで実装できずに困っていたDjango4とReactでの認証周りの実装やトークンの飛ばし方、その他JSONデータのやり取りやCRUD一式の実装方法についてやり方がわかったため今後の開発の際に役立てられるよう備忘録として残すほか、私と同様Django4とReactとのAPI認証の実装に際してやり方がわからない方のための助けになればと思い、記事を執筆することにしました。
2.環境
- MacOS, Linux, Windows
- Homebrewインストール済み(Mac, Linuxの場合)
- Python3.9インストール済み
- Django4
- Node.jsインストール済み
3.仕組みはどうでもいいからとりあえず動かしてみたい方は
こちらの動画の概要欄にGitHub公開レポジトリへのリンクが貼ってありますので移動してターミナルでgit cloneするかzipファイルを落としてきて展開しましょう。この際、わかりやすい場所(MacならHomeの直下、Linuxならルートディレクトリ直下等)にクローンしておけば環境構築がやりやすいです。Windowsの場合もやり方はたいして変わりません。任意のディレクトリまで移動し、コマンドプロンプトでgit cloneするだけです。
レポジトリのクローンができたらプロジェクトのディレクトリに移動します。肝心のディレクトリの中にはbackendとfrontendの2つのディレクトリがあります。backendがDjango, frontendがReactのアプリになります。フロントエンドとバックエンドの両方のサーバーを起動しないとUIは表示されないですのでターミナルは必ず2つ用意し、一つはfrontend、もう一つはbackendのディレクトリまで移動させておきましょう。
次に、フロントエンドとバックエンドそれぞれに必要なライブラリを入れて行きます。ライブラリが既に入っている方でしたらこの作業は必要ありませんが、まっさらな状態から環境構築するならそのままサーバーを起動してもエラー文を返されるだけです。
・フロントエンド(ターミナルではfrontendのディレクトリに移動していること前提)
npm install react-script react-bootstrap react-router-dom
npm start
・バックエンド(ターミナルではbackendのディレクトリに移動していること前提)
(Mac, Linuxの操作)
pip3 install django django-cors-headers pytz
python3 manage.py migrate
python3 manage.py runserver
(Windowsの操作)
pip install django django-cors-headers pytz
python manage.py migrate
python manage.py runserver
以上の操作をしてサーバー起動状態にしておき、ターミナルでエラーが返されない場合、http://localhost:3000へとアクセスしてみて、以下のページが表示されているかどうかを確認しましょう。
少なくとも私の環境ではこの操作でローカルで動作を確認できるようになりましたが、ターミナル又はブラウザで何かしらのエラー文が表示された場合は必要なライブラリをインストールできてないわけですので、エラー文に従って足りていないモジュールを適宜インストールしましょう。
4.CRUD部分の解析
4-1.バックエンド編
VScode又は各自のお気に入りのエディターでbackendディレクトリの中身を見て行きます。
backendが設定用ファイルその他諸々が入っているディレクトリ、coreがブログのアカウント情報以外の基本的なCRUD操作や認証用のAPIに関わるアプリサイドと考えられます。rest_frameworkのディレクトリはオープンソースのdjango-rest-frameworkで、https://github.com/encode/django-rest-framework/tree/masterからソースコードをダウンロードしてrest_frameworkのディレクトリを複製しているようです。公式ドキュメントではpip install djangorestframeworkで手に入れるかgit cloneして欲しいと書いてますが、いちいちオープンソースをクローンして意図的にディレクトリを複製してbackendディレクトリの直下に配置したということは、pipでライブラリをインストールするやり方ではうまくいかないのかもしれないと推測しています。特に、認証トークンを発行してユーザーと紐づける処理のために意図的に配置しているのではないかと考えています。backend/core/serializers.pyでTokenモデルを呼び出している箇所からしてpipでライブラリを落としてくるのではダメなのかもしれないです。
ReactとAPIのやり取りをするに際してバックエンドで重要になってくるのはまずはsettings.pyになります。特に深く関わっている箇所を抜粋していくと、下記の場所になります。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core', # アプリ名
'rest_framework', # django-rest-framework
'rest_framework.authtoken', # django-rest-framework認証トークン
'corsheaders', # django-cors-headers
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware', # 追記
'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',
]
# 追記
CORS_ORIGIN_ALLOW_ALL = True
INSTALLED_APPSでアプリ名を読み込む操作についてはDjangoを普段から触っている方なら基本中の基本だと思いますし、'rest_framework',や'rest_framework.authtoken',、'corsheaders',についてもdjango-rest-frameworを使ってAPI開発をしたことのある方ならまず知っていて当たり前かと思います。ここについてはまだdjango-rest-frameworkでAPI開発をするなら基礎になってくるところかなと思います。該当箇所を書き換えたり追記したりすれば少なくとも動きはするはずです。corsが気になる方は各自でググっていただければと思います。
また、backendディレクトリのurls.pyにてcoreアプリを読み込むようにコードを書き換えておかないと、coreのurls.pyで定義したurlが読まれません。ここについてもDjangoを触る方からしたら基礎中の基礎ですが、結構忘れられやすいですので触れました。前回の記事でも触れたことですが、Django4がリリースされてからはurl関数が廃止されてしまっていますので、このような書き方になっているようです。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
]
Djangoに限らず、RailsやLaravel、その他何かしらのバックエンドWebフレームワークを触ったことのある方ならそれらが共通してModelやController,Viewによってテンプレートを操作するMVCフレームワークだと認識されていると思います(DjangoはMVTでは?とも言われていますがここではそのことについては考えません)。ここで言いたいことはDBと連携するためのModelの定義は必須であること、それだけです。Laravelを使う方でしたらもしかしたら全部クエリビルダーで書くからModelは使わないとかいう方もいるかもしれませんが、DjangoやRailsならばModelの定義は基本中の基本です。CRUD操作を実装するならmodels.pyをいじらないことには話になりません。
from pydoc import describe
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
def __str__(self):
return self.title
タイトルと本文だけのシンプルなブログアプリでしたらこれだけで十分ですが、本格的な業務用システムとかWebサービスを開発する場合はDB設計のところはしっかりとやりましょう。要件定義の段階から将来的な開発を見越して徹底的にやりましょう。急な設計変更とかすると技術的な負債が溜まり込みます。マイグレーションのロールバックとかやる羽目になった時なんかは悲劇です。時間のロスも出ますしコード修正の手間もかかって実装担当のエンジニアが疲弊します(私も実際に疲弊しました)。
Djangoで開発をされたことのある方でしたらmodels.py, admin.py, urls.py, views.pyは必ずと言っていいほど触ります。あとはMPAだったらforms.pyを作成してそっちでフォームの定義やバリデーションチェックしたりとかAPI開発でserializers.pyを作成してserializerを定義したりとかが定番かなと思います。Reactなどのフロントエンドフレームワークと連携させる場合、バックエンドフレームワークの役割はAPIのみになってきますので、APIを開発するにあたってはいじる箇所はアプリ内のmodels.py, admin.py, urls.py, views.py, serializers.pyになってくると思います。あとは機械学習とか絡める場合は同じアプリ内にさらに別途で独自のライブラリを作ったりとかもありそうですがそういった例外でもない限りは上記の5つのファイルにAPIを実装していくと思います。
ですので、CRUD操作を実装するに際して開く必要があるのはmodels.pyの他には、admin.py, urls.py, views.py, serializers.pyを見る必要があります。今回解析したブログのサンプルではmodels.py以外のそれぞれのファイルの中身はブログ記事投稿の機能だけに着目してみるとこうなっていました。
from django.contrib import admin
from .models import Article
@admin.register(Article)
class ArticleModel(admin.ModelAdmin):
list_filter = ('title','description')
list_display = ('title','description')
from django.urls import path, include
from .views import ArticleViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='artilces')
urlpatterns = [
path('api/', include(router.urls)),
]
from rest_framework import serializers
from rest_framework import viewsets
from .serialziers import ArticleSerializer
from .models import Article
from rest_framework.authentication import TokenAuthentication
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
authentication_classes = (TokenAuthentication,)
from dataclasses import field
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id','title','description']
API開発をDjangoでする際はserializers.pyがforms.pyの代わりにネームフィールドの定義やバリデーションチェックを行なってくれます。また、django-rest-frameworkでAPI開発をする場合、viewsetsを使うことでCRUDの実装を簡潔に行うことができるようです。authentication_classesの部分はユーザー認証されている場合にのみこの処理を有効にするという制約をつけていると推測しています。従って、通常のCRUDのAPI実装をする分には以上のテンプレートを適宜書き換えればカスタマイズが可能と言えます。
4-2.フロントエンド編
Reactを触ったことのある方でしたら以下の画像だけでツリー構造に見覚えがあるのではないかと思います。
npx create-react-app frontendとターミナルで打ち込んで実行した場合の作成されるReactアプリのツリー構造を全くいじらずにそのまま使ってありますので、サーバー起動時にindex.jsが読まれ、index.jsで呼び出されているApp.jsが呼ばれ、App.jsからさらにcomponentsディレクトリ内の各種コンポーネントが呼ばれるという構造になっています。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {Route, BrowserRouter,Routes} from 'react-router-dom';
import Login from './components/Login'; // 認証
function Router(){
return(
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Login />} />
{/* 認証 */}
<Route exact path="/articles" element={<App />} />
</Routes>
</BrowserRouter>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Router />
</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();
サーバー起動時に真っ先に読み込まれるのがこれになります。function Router(){}内に書かれているルーティングの書き方は2022年12月時点では最新のreact-router-domのバージョン6の書き方になっているため、react-router-domのバージョン5を使っている方はバージョンアップをする必要があります。ブログのデータを操作する機能自体はログインしないと使えないので、認証で使うであろう箇所についてはコメントアウトで認証と書き足してあります。
ブログ記事の主要な処理の中でも特にAPI通信を行なっている箇所はApp.jsと推測しています。こちらも認証が絡んでいると思われる箇所についてはコメントアウトして認証と書き足しています。
import './App.css';
import ArticleList from './components/ArticleList';
import {useState,useEffect} from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import NavBar from './components/NavBar';
import Form from './components/Form';
import {useNavigate} from 'react-router-dom'
import {useCookies} from 'react-cookie' // 認証
function App() {
const [articles, setArticles] = useState([])
const [editArticle, setEditArticle] = useState('')
const [token, setToken, removeToken] = useCookies(['mytoken']) // 認証
let navigate = useNavigate()
// 全ての記事を取得する処理
useEffect(() =>{
fetch('http://localhost:8000/api/articles/', {
method:'GET',
headers:{
'Content-Type': 'application/json',
'Authorization': 'a2a76bcaca32becedbd9fc8542dc293f9c98b92b'
}
})
.then(resp => resp.json())
.then(resp => setArticles(resp))
.catch(error => console.log(error))
}, [])
// ステートとして値を保持
const editBtn = (article) =>{
setEditArticle(article)
}
// 情報の更新
const updatedInformation = (article) => {
const new_article = articles.map(myarticle => {
if(myarticle.id === article.id){
return article
}else{
return myarticle
}
})
setArticles(new_article)
}
// 入力フォームの呼び出し
const articleForm = () =>{
setEditArticle({title:'', description:''})
}
// 新規記事の登録
const insertedInformation = (article) => {
const new_articles = [...articles,article]
setArticles(new_articles)
}
// 記事の削除
const deleteBtn = (article) =>{
const new_article = articles.filter(myarticle => {
if(myarticle.id === article.id){
return false
}
return true
})
setArticles(new_article)
}
// 認証 user_tokenがundefinedならログインページに遷移させる
useEffect(()=> {
var user_token = token['mytoken']
console.log('User token is',user_token)
if(String(user_token) === 'undefined'){
navigate('/')
}else{
navigate('/articles')
}
}, [token])
// 認証(ステートに保持しているトークンを破棄してログアウト)
const logoutBtn = () => {
removeToken(['mytoken'])
}
return (
<div className="App">
<NavBar />
<br />
<div className="row">
<div className="col">
<button className="btn btn-primary" onClick={articleForm}>Create Post</button>
</div>
</div>
<ArticleList articles={articles} editBtn ={editBtn} deleteBtn ={deleteBtn}/>
<Form article = {editArticle} updatedInformation= {updatedInformation} insertedInformation= {insertedInformation}/>
</div>
);
}
export default App;
このコードを眺めていて意外に思ったのはaxiosを使っていない点です。APIのやり取りをする際にフロントエンドでaxiosは必ずと言っていいほど使われたりします。使う必要がなかったか、何らかの理由で使えないのかは不明ですが、認証トークンを照合して、有効なトークンだったらDBに保存された全ての記事の情報を呼び出したり、新規に追加、編集、削除といった操作を有効にできるようにしていると考えられます。
App.jsで認証トークンのステートを確認し、有効なトークンだったらArticleList.jsやForm.jsのコンポーネントを呼び出しているという設計になっていると思われます。
import React from 'react'
import APISerive from './APIService'
import {useCookies} from 'react-cookie';
function ArticleList(props) {
const [token] = useCookies(['mytoken'])
const editBtn = (article)=>{
props.editBtn(article)
}
const deleteBtn = (article)=>{
APISerive.DeleteArticle(article.id,token['mytoken'])
.then(()=> props.deleteBtn(article))
.catch(error => console.log(error))
}
return (
<div>
{props.articles && props.articles.map(article =>{
return(
<div key={article.id}>
<h1>{article.title}</h1>
<p>{article.description}</p>
<div className="row">
<div className="col-md-1">
<button className="btn btn-primary" onClick= {()=>editBtn(article)}>Update</button>
</div>
<div className="col-md-1">
<button className="btn btn-danger" onClick= {()=>deleteBtn(article)}>Delete</button>
</div>
</div>
<hr className="post_line" />
</div>
)
})}
</div>
)
}
export default ArticleList
export default class APISerive {
// 記事の更新
static UpdateArticle(article_id,body,token){
return fetch(`http://127.0.0.1:8000/api/articles/${article_id}/`,{
method:'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token}`
},
body:JSON.stringify(body)
}).then(resp => resp.json())
}
// 記事の追加
static InsertArticle(body,token){
return fetch(`http://127.0.0.1:8000/api/articles/`,{
method:'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token}`
},
body:JSON.stringify(body)
}).then(resp => resp.json())
}
// 記事の削除
static DeleteArticle(article_id,token){
return fetch(`http://127.0.0.1:8000/api/articles/${article_id}/`,{
method:'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token}`
}
})
}
// ユーザー認証
static LoginUser(body){
return fetch(`http://127.0.0.1:8000/auth/`,{
method:'POST',
headers: {
'Content-Type': 'application/json'
},
body:JSON.stringify(body)
}).then(resp => resp.json())
}
// 新規ユーザー登録
static RegisterUser(body){
return fetch(`http://127.0.0.1:8000/api/users/`, {
method:'POST',
headers: {
'Content-Type': 'application/json',
},
body:JSON.stringify(body)
}).then(resp => resp.json())
}
}
import React, {useState,useEffect} from 'react'
import APISerive from './APIService'
import {useCookies} from 'react-cookie';
function Form(props) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [token] = useCookies(['mytoken'])
// ステートで保持された値のセット
useEffect(()=>{
setTitle(props.article.title)
setDescription(props.article.description)
}, [props.article])
// 記事の更新時に走らせる処理
const updateArticle = ()=>{
APISerive.UpdateArticle(props.article.id, {title,description},token['mytoken'])
.then(resp => props.updatedInformation(resp))
setTitle('')
setDescription('')
}
// 記事の追加事に走らせる処理
const insertArticle = ()=>{
APISerive.InsertArticle({title,description},token['mytoken'])
.then(resp => props.insertedInformation(resp))
setTitle('')
setDescription('')
}
return (
<div>
{props.article ? (<div>
<div className="col-md-7">
<div className="col-md-6">
<div className="form-group">
<label htmlFor="title">Title</label>
<input type="text" value={title} className="form-control" placeholder="Enter Post Title" onChange ={e=> setTitle(e.target.value)}/>
</div>
</div>
<div className="col-md-6">
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea type="text" value={description} className="form-control" placeholder="Enter Post Description" rows="3" onChange ={e=> setDescription(e.target.value)}/>
</div>
</div>
<br />
{props.article.id ?
<button onClick ={updateArticle} className="btn btn-success">Update</button>
:
<button onClick ={insertArticle} className="btn btn-primary">Post</button>
}
</div>
</div>) : null}
</div>
)
}
export default Form
追加してあるコメントアウトは後々私自身が何らかのコンポーネントを作る際に是非とも参考にしたいと思ったため追加しました。index.jsやApp.jsで認証トークンが有効かどうかの判定と記事のデータの取得処理が一元化されているためにできる処理だと感じました。より複雑なシステムを作っていこうと考える場合は、App.jsにデータ取得用の関数ないしは処理をまとめて記述するか、別ファイルにデータ取得用のファイルを作成してApp.js側で呼びだすかといったやり方で行く必要がありそうに感じました。
5.認証部分の解析
5-1.バックエンド編
認証周りのバックエンドで関わってくる箇所はbackendディレクトリのurls.pyとcoreアプリ内のserializers.py, urls.py, views.pyになってきます。それぞれのコードの認証に関わる部分について見て行きますとこうなります。
from django.contrib import admin
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token # 認証
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
path('auth/', obtain_auth_token), # 認証
]
from dataclasses import field
from rest_framework import serializers
from .models import Article
from django.contrib.auth.models import User # 認証
from rest_framework.authtoken.views import Token # 認証
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id','title','description']
# 認証
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id','username','password']
extra_kwargs = {'password':{
'write_only':True,
'required':True
}}
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
Token.objects.create(user=user)
return user
from django.urls import path, include
from .views import ArticleViewSet,UserViewSet # 認証
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='artilces')
router.register('users', UserViewSet) # 認証
urlpatterns = [
path('api/', include(router.urls)),
]
from rest_framework import serializers
from rest_framework import viewsets
from .serialziers import ArticleSerializer,UserSerializer # 認証
from .models import Article
from django.contrib.auth.models import User # 認証
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated # 認証
# 認証
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
authentication_classes = (TokenAuthentication,)
コメントアウトで認証と書き足している部分がAPI認証に関与している箇所と推測しています。django-rest-frameworkの方で用意している標準のユーザーモデルをもとにしてクラスを設計していると思われますが、もしカスタムユーザーでAPI認証を設計するとしたらどうなるんでしょうね…。カスタムユーザーで実装する場合のやり方も気になるところですが、ユーザー認証の部分はこれだけで実装できると思われます。ですのでコメントアウトを書き足している部分のコードを各ファイルにコピペしていけば少なくともバックエンドのAPI認証の部分については実装できるのではないかと推測しています。
5-2.フロントエンド編
index.jsを読み込み後、各コンポーネントを呼び出す際に、特徴的だと感じたのはreact-cookieを使っていること、そして、有効なトークンをステートに保持しておくことでログイン状態とみなしている点です。simple-jwtとDjango3系とReactを連携させる際にreduxを使ったりするケースもありますが、Django4とReactを連携させるときにはcookieを使うやり方が最もやりやすいのかもしれないと思いました。
認証に関連するコンポーネントで特に興味深かったのはLogin.jsだけで新規ユーザー作成もログインも実装できてしまっている点です。
import React, {useState,useEffect} from 'react'
import {useNavigate} from 'react-router-dom'
import {useCookies} from 'react-cookie'
import APIService from './APIService'
function Login() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [token, setToken] = useCookies(['mytoken'])
let navigate = useNavigate()
const [isLogin, setLogin] = useState(true)
// ログイン状態か否かをステートとして保持
useEffect(()=> {
var user_token = token['mytoken']
console.log('Login User token is',user_token)
console.log('Data type',typeof(token['mytoken']))
if(String(user_token) === 'undefined'){
navigate('/')
}else{
navigate('/articles')
}
}, [token])
// ログインページのステートとして保持されているフォームの値をサーバーサイドに送り込み、DBと照合
const loginBtn = () => {
if(username.trim().length !==0 && password.trim().length){
console.log('Username And Password Are Set')
APIService.LoginUser({username,password})
.then(resp => setToken('mytoken', resp.token))
.catch(error => console.log(error))
}else{
console.log('Username And Password Are Not Set')
navigate('/')
}
}
// ログインページでregisterのボタンが押された際に走る処理
const RegisterBtn = () => {
if(username.trim().length !== 0 && password.trim().length !== 0){
console.log('Username and password are set');
APIService.RegisterUser({username, password})
.then(() => loginBtn())
.catch(error => console(error))
}else{
navigate('/')
console.log('Username and password are not set');
}
}
const loginStyle={
backgroundImage:`url(${process.env.PUBLIC_URL+ "img/18.jpg"})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
'min-height': '100%',
height:'77vh',
backgroundPosition:' center',
margin:0,
};
return (
<div className="App">
<div className="container-fluid">
<div className="row">
<h1 className="alert alert-danger">Learn Python Blog</h1>
<br />
<br />
<div className="col-sm-4">
{isLogin ? <h3>Please Login Here</h3> : <h3>Please Register Here</h3>}
<div className="form-group">
<label htmlFor="username">Username</label>
<input type="text" value={username} className="form-control" placeholder="Enter Username" onChange ={e=> setUsername(e.target.value)}/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input type="password" value={password} className="form-control" placeholder="Enter Password" onChange ={e=> setPassword(e.target.value)}/>
</div>
<br />
<div>
{isLogin ?
<div>
<button onClick={loginBtn} className="btn btn-primary">Login</button>
<p>If You Don't Have Account, Please</p><button onClick={() => setLogin(false)} className="btn btn-primary">Register</button></div>
:
<div>
<button onClick={RegisterBtn} className="btn btn-primary">Register</button>
<p>If You Have Account, Please <button className="btn btn-primary" onClick={() => setLogin(true)}>Login</button></p></div>
}
</div>
</div>
<div className="col-sm-8 full-img" style={loginStyle}>
</div>
</div>
</div>
</div>
)
}
export default Login
このコンポーネントを見ていて思ったことは無駄がなくてすっきりしていてすごいということです。普段私は仕事でフロントエンドを構築する際にいちいちLoginとRegisterを別々のファイルに書くことでわかりやすくしている反面、保守運用やコードの解析にかかる時間も2倍になってしまうので、今回参考にしたサンプルコードのようにステートで状態管理をし、極力同じコンポーネントを使い回そうとする思想がまさにReactならではだなと感じました。ただ、普段からLoginとRegisterを区別して書いてしまっている私からしたらコードを解析するのにやたらと時間がかかってしまったかなあと思います。
6.まとめ
探していたDjango4とReactの認証周りの連携方法が理解できましたので私としては満足だったと言えますが、実際の開発で真似をしたいかと考えると、大規模開発やチーム開発向けにReactコンポーネントの書き方をもう少しわかりやすくしたいなあと思いました。
中堅以上のエンジニアであれば解読するのは難しくないでしょうけれども、現場に入りたての新人や若手にこういう無駄のないコードを見せて保守運用してくれとか実装してくれと指示しても対応できるのはごく少数になるかもしれないからです。
無駄のないコードを重視するか、無駄があるけど経験の浅い人でも読みやすいコードを書くかは考えが分かれるところになりますが、一個人としては経験年数とか関係なく、例え現場に入りたてのエンジニアが見たとしてもすぐにわかるコードとかを書きたいなあと思いました。最も、「すぐにわかるコード==無駄が全くないコード」とかでしたら今回解析したサンプルプロジェクトを真似てより短いコードで実装することを目標にしようとは思います(笑)。