🐥

DjangoとReactで自分用日報アプリを作る~②React編~

2020/12/29に公開
2

はじめに

Django Rest FrameworkとReactで自分用の日報アプリを作りました
その手順をまとめておきたいと思います。
長くなると思うので ①Django編 ②React編 の2つの記事に分けて書きます。
この記事は②React編になります。
①Django編を読んでない方はそちらを先に読んでいただけると嬉しいです!

完成させたもの

頑張って作ったのでよかったら完成品見てください!
完成品.png

この記事で説明すること

この日報アプリはDjango Rest Framework(DRF)でAPIを作ってReactでそのAPIを叩く、という形にしてあり、今回は「DRF作ったAPIをReactで叩いてデータを持ってくる」部分を説明します。さらに、マークダウン記法で表示させたいので、そこの実装も紹介しようと思います。

環境

  • django 2.2.16
  • djangorestframework 3.12.1
  • django-cors-headers 2.4.0
  • django-markdownx 3.0.1
  • node 14.11.0
  • npm 6.14.8
  • react 17.0.1
  • react-router-dom 5.2.0
  • marked 1.2.7

環境構築

今回はcreate-react-appでReactの環境構築をします。
今回フロントエンド用のフォルダ名は"frontend"として説明していきます。
また、マークダウン記法で書いたものをHTMLの記述に変えるためのライブラリmarked、ルーティングするためのライブラリreact-router-domもインストールしておきます。


$ mkdir frontend
$ cd frontend
$ npx create-react-app .
$ npm install marked 
$ npm install react-router-dom 

ちゃんとインストールされていることを確認するために次のコマンドを打って http://localhost:3000/ にアクセスします。

$ npm start

Reactのロゴがクルクルしてるページが表示されていたら成功です。

ディレクトリ構成

これから実際にコードを書いてきますが、その前にsrcのディレクトリ構成を示しておこうと思います。

src
├── App.js
├── App.test.js
├── Header.js  //ヘッダー
├── Top.js  //トップページ
├── daily
│   ├── api
│   │   └── getDaily.js  //DRFで作ったエンドポイントからデータを持ってくる
│   ├── components
│   │   ├── CategoryList.js  
│   │   ├── DailyCategory.js  
│   │   └── DailyContent.js
│   └── pages
│       ├── CategoryView.js  //カテゴリ別一覧ページ
│       ├── DailyDetail.js  //1日の詳細ページ
│       └── DailyTop.js  //日報の一覧ページ
├── images
│   └── daily  //1日の評価に使う画像
│      ├── hiyoko_bad.png
│      ├── hiyoko_good.png
│      ├── hiyoko_perfect.png
│      └── hiyoko_soso.png
│   
├── reportWebVitals.js
├── sass
│   └── index.scss  
└── setupTests.js

App.js

まずはApp.jsの編集から見ていこうと思います。
ここでは主にルーティングの設定をしています。

App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Header } from './Header';
import { Top } from './Top';
import { DailyTop } from './daily/pages/DailyTop';
import { CategoryView } from './daily/pages/CategoryView';
import {DailyDetail} from './daily/pages/DailyDetail';
import { Profile} from './profile/Profile';

export const App = () => {  
  return(
    <div>
      <Router>
        <Header />
        <div>
          <Switch>
              <Route exact path='/' component={Top} />
              <Route exact path='/daily' component={DailyTop} />
              <Route exact path='/daily/:id' component={DailyDetail} />
              <Route exact path='/daily/category/:cat' component={CategoryView}/>
              <Route path='/profile' component={Profile}/>
              <Route render={() => <h4>not found...</h4>} />
          </Switch>
        </div>
      </Router>
    </div>
  )  
}

http://localhost:3000/ というパスが指定されたらTopを表示しますよ〜、
http://localhost:3000/daily というパスが指定されたらDailyTopを表示しますよ〜、
http://localhost:3000/daily/3 とかのパスが指定されたらDailyDetailを表示しますよ〜、
http://localhost:3000/daily/category/univ とかのパスが指定されたらCategoryViewを表示しますよ〜、
どれにも当てはまらなかったら not found... と表示しますよ〜、
って感じです。
DailyTopとかはこれから作っていきます。
が、その前にそんなに労力かからないというかDRFと関わりのない部分を作っておきます。

Top.js ・ Header.js ・ Category.js

ただのトップページとただのヘッダーとただのリストなので特に説明はなしでいこうと思います。

Top.js
import React from 'react'

export const Top = () => {
    return(
        <div>
            <h1>Topです</h1>
        </div>
    )
}
Header.js
import React from 'react';
import { Link } from 'react-router-dom';

export const Header = () => {
    return(
        <div>
            <Link to='/'>Top</Link>
            <ul>
                <li><Link to='/daily'>日報</Link></li>
            </ul>
        </div>
    )
}
CategoryList.js
import React from 'react'
import { Link } from 'react-router-dom'

export const CategoryList = () => {
    return(
        <div>
        <ul>
            <li><Link to='/daily/category/univ'>大学</Link></li>
            <li><Link to='/daily/category/study'>勉強</Link></li>
            <li><Link to='/daily/category/other'>その他</Link></li>
            <li><Link to='/daily/category/first_meet'>はじめましてだったこと</Link></li>
            <li><Link to='/daily/category/wanna_do'>やりたいこと</Link></li>
            <li><Link to='/daily/category/summary'>1日のまとめ</Link></li>
        </ul>
        </div>
    )
}

getDaily.js (DRFで作ったAPIを叩く)

getDaily.jsはDRFで作ったAPIを叩く関数をまとめたファイルです。
async/awaitを使って非同期でJSONデータを持ってきます。

api/getDaily.js
const toJson = async (res) => {
    const json = await res.json();
    if(res.ok){
        return json;
    }else{
        throw new Error(json.message);
    }
}

//日報一覧を取得
export const getDaily = async () =>{
    const res = await fetch('http://localhost:8000/daily/', {
        method: 'GET',
    })
    return await toJson(res);
}

//1日の詳細を取得
export const getDailyDetail = async (id) => {
    const res = await fetch(`http://localhost:8000/daily/${id}`, {
        method : 'GET',
    })
    return await toJson(res);
}

//カテゴリ別一覧を取得
export const getCategory = async (cat) => {
    const res = await fetch(`http://localhost:8000/daily/${cat}`, {
        method: 'GET',
    })
    return await toJson(res)
}

getDaily, getDailyDetail, getCategoryは他のファイルで呼び出すのでexportしてあります。
methodオプションはデフォルトがGETなので、 method : 'GET' は書かなくてもいいけどわかりやすさのために書きました。
流れとしては、
fetchで取ってきたデータをresに入れる→resを引数として関数toJsonに渡す→Jsonに解決してそれを返す
って感じです。

http://localhost:8000/daily/ とか http://localhost:8000/daily/${id} とかって何だっけ?ってなった人は、①Django編の記事のここら辺を見て欲しいなと思います!

次は、ここで作った関数を呼び出し、実際にデータを表示させることをしていきます。

DailyTop.js

DailyTop.jsは日報の一覧を表示するページです。
↓こんな感じ
スクリーンショット 2020-12-29 1.29.44.png
とりあえずコードを見てみます。

pages/DailyTop.js
import React, { useState, useEffect } from 'react'
import { getDaily } from '../api/getDaily'
import { DailyContent } from '../components/DailyContent'
import { CategoryList } from '../components/CategoryList'


export const DailyTop = () => {
    const initialState = {
        id: '',
        date: '',
        evaluation: '',   
    }

    const[daily, setDaily] = useState(initialState);
    const[loading, setLoading] = useState(true);

    useEffect(() => {
        getDaily()
        .then(d => {
            setDaily(d)
            setLoading(false)
        })
        .catch(e => {
            throw new Error(e)
        })
    },[])

    return(
        <div>
            {
                loading ?
                <h1>loading...</h1>
                :
                <div>
                {daily.map( d => <DailyContent {...d}  /> )}
                </div>
            }
            <CategoryList />
        </div>
    )

}

initialStateで初期値を設定しています。
useEffect内で先ほど作った関数 getDaily() を呼び出してデータを取ってきています。それが成功したらdailyにデータを入れる & loadingをfalseにします。
return内ではloadingがtrueの時はloading...と表示されるようになっています。
map関数でDailyContentというコンポーネントにpropsを設定しています。
余計かもしれないことを言っておくと、

{daily.map( d => <DailyContent {...d}  /> )}

{daily.map( d => <DailyContent id={d.id} date={d.date} evaluation={d.evaluation} /> )} 

と同じことです。(公式ドキュメント)

DailyContent.js

DailyContentは一覧ページに表示されてる一個一個のコンポーネントです。

components/DailyContent.js
import React from 'react';
import { Link } from 'react-router-dom';

import img1 from '../../images/daily/hiyoko_perfect.png';
import img2 from '../../images/daily/hiyoko_good.png';
import img3 from '../../images/daily/hiyoko_soso.png';
import img4 from '../../images/daily/hiyoko_bad.png';

export const DailyContent = (daily) => {
    let eva;
    if(daily.evaluation === 'perfect'){
        eva = img1;
    }else if(daily.evaluation === 'good'){
        eva = img2;
    }else if(daily.evaluation === 'soso'){
        eva = img3;
    }else{
        eva = img4;
    }

    return(
        <div>
            <Link to={`daily/${daily.id}`}> <h1>{daily.date}</h1> </Link> 
            <img src={eva}/>
        </div>
    )
}

日付と画像をセットにして表示しています。
また、

<Link to={`daily/${daily.id}`}>

は、idが3だった場合、 http://localhost:8000/daily/3 となり、App.jsで書いたようにDailyDetailを表示することになります。
次はDailyDetailの中身を見ていきます。

DailyDetail.js

1日の詳細を表示するページです。
↓こんな感じ
スクリーンショット 2020-12-29 2.06.47.png

コードを見てみます。

pages/DailyDetail.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { getDailyDetail } from '../api/getDaily';
import marked from 'marked';
import { CategoryList } from '../components/CategoryList'


export const DailyDetail = () => {
    const initialState = {
        date: '',
        univ: '',
        study: '',
        other: '',
        first_meet: '',
        wanna_do: '',
        summary: '',
    };

    const [detail, setDetail] = useState(initialState)
    const [loading, setLoading] = useState(true);
    const { id } = useParams();

    useEffect(()=>{
        getDailyDetail(id)
        .then(d => {
            setDetail(d)
            setLoading(false)
        })
        .catch(e => {
            throw new Error(e)
        })
    },[])
    
    return(
        <div>
            {loading ?
                <h1>loading....</h1>
                :
                <div>
                    <h1>{detail.date}</h1>
                    <h1>大学のこと</h1>
                    <div dangerouslySetInnerHTML={{ __html: `${marked(`${detail.univ}`)}` }} className="detail-content"></div>
                    <h1>勉強</h1>
                    <div dangerouslySetInnerHTML={{ __html: `${marked(`${detail.study}`)}` }} className="detail-content"></div>
                    <h1>その他</h1>
                    <div dangerouslySetInnerHTML={{ __html: `${marked(`${detail.other}`)}` }} className="detail-content"></div>
                    <h1>初めましてだったこと</h1>
                    <div dangerouslySetInnerHTML={{ __html: `${marked(`${detail.first_meet}`)}` }} className="detail-content"></div>
                    <h1>やりたいこと</h1>
                    <div dangerouslySetInnerHTML={{ __html: `${marked(`${detail.wanna_do}`)}` }} className="detail-content"></div>
                    <h1>1日のまとめ</h1>
                    <div dangerouslySetInnerHTML={{ __html: `${marked(`${detail.summary}`)}` }} className="detail-content"></div>
                </div>
            }
            <CategoryList />
        </div>
    )
    
}

initialStateを初期値にしてloadingをfalseにして、っていうところはさっきと同じです。
getDailyDetail(id)というようにidを引数として渡して、そのidを持つ日報の1日の詳細データを持ってきます。
usePramsはURLパラメータを取得します。
例えば、 http://localhost:8000/daily/3 だった場合 id には3が入ります。
次にreturnの中をみていきます。
markedはマークダウンをHTMLに変換するライブラリです。そして detail.univ や detail.study はマークダウンで書かれています。なので、markedの引数にそれらを渡してHTMLに変換してあげます。また、コードから HTML を設定するとクロスサイトスクリプティング攻撃に晒してしまう可能性があるため危険です(今回の場合はdetail.univとかに入っているのは自分で書いたものなので大丈夫)。危険を承知の上でやるよってことでdangerouslySetInnerHTMLという属性に __htmlプロパティを持たせて、そこにmarkedを渡すという構造になっています。 参考

CategoryView.js

カテゴリ別の一覧を表示するページです。
↓こんな感じ
スクリーンショット 2020-12-29 2.45.02.png

コードを見ます。

pagas/CategoryView.js
import React, {useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'

import { getCategory } from '../api/getDaily'
import { DailyCategory } from '../components/DailyCategory'
import { CategoryList } from '../components/CategoryList'

export const CategoryView = () => {

    const initialState = {
        date: '',
    }

    const [category, setCategory] = useState(initialState)
    const [loading, setLoading] = useState(true)
    const { cat } = useParams();


    useEffect(()=>{
        getCategory(cat)
        .then(c =>{
            setCategory(c)
            setLoading(false)
        })
        .catch(e => {
            throw new Error(e)
        })
    },[cat])

    let title;

    if(cat === "univ"){
        title = "大学のこと";
    }else if(cat === "study"){
        title = "勉強";
    }else if(cat === "other"){
        title = "その他";
    }else if(cat === "first_meet"){
        title = "はじましてだったこと";
    }else if(cat === "wanna_do"){
        title = "やりたいこと";
    }else{
        title = "1日のまとめ";
    }

    return(
        <div className="main">
        {
            loading ?
            <h1>loading....</h1>
            :
            <div className="categoryView-container">
                <h1 className="category-title">{title}</h1>
                { category.map(c => <DailyCategory {...c}  />) }
            </div>
        }
        <CategoryList />
        </div>
    )
}

initialStateで初期値設定、useStateでgetCategory(cat)でカテゴリ別の一覧のデータをとってきてloadingをfalseに、という大体の流れは今までと一緒です。
ただ1つ違うのは、useEffectの第二引数です。
DailyTopやDailyDetailでは空の引数を渡していましたが、このように第二引数に値の配列を渡すと、その値が変更された時に第一引数の関数が実行されます。
今回の場合で言うと、catが変わるたびにgetCategoryが呼び出されてカテゴリ別の一覧のデータをとってくる、ということになります。

画像使って説明します。
第二引数を空の配列にした場合は以下のようなことが起きてしまいます。
スクリーンショット 2020-12-29 2.56.55.png
スクリーンショット 2020-12-29 3.05.04.png
スクリーンショット 2020-12-29 2.58.38.png

このようになっちゃうので第二引数にcatを指定して表示されるデータがちゃんと変わるようにしましょう。

DailyCategory.js

これが最後!
カテゴリ別一覧で表示する、日付と内容をひとまとめにしたコンポーネントです。
コードを見ます。

components/DailyCategory.js
import React from 'react'
import marked from 'marked'

export const DailyCategory = (category) => {
    return(
        <div className="category-content">
            <h1 className="category-date">{category.date}</h1>
            <div dangerouslySetInnerHTML={{ __html: `${marked(`${category.content}`)}` }}></div>
        </div>
    )
    
}

日付と内容ひとまとめにして、内容はマークダウンで書かれてるからmarkedを使って、って感じです。

完成形

最初の方にも紹介しましたがCSSをいい感じに書いていって、最終的にはこんな感じになりました。
完成形

終わりに

2つの記事を通して、DjangoとReactを使った自分用日報アプリの作り方を紹介しました。
自分で作ると愛着も湧いてしっかりしたことを書こうと思うので進捗も出たり出なかったりします。
ほぼ自分用に書いたようなものですが誰かの役に立つことがあったら嬉しいです。
最後までお読みいただきありがとうございました!

参考

Discussion

kikkomankikkoman

とても参考になる記事をありがとうございます。
この先となる、デプロイ方法についてとても気になるのですが。
今後方法について記事を投稿される予定はございますでしょうか。

PiyopanmanPiyopanman

こんにちは、嬉しいお言葉ありがとうございます。
デプロイ方法についての記事を投稿する予定はありませんが、ReactはVercel, Djangoはpythonanywhereにデプロイしています。vercelの方は手順に従っていけばお手軽にデプロイできるのでおすすめです。pythonanywhereへのデプロイはDjango Girlsというチュートリアルが参考になるのでよろしければ見てみてください。
参考