先日、大岡由佳さんの「りあクト! 第3.1版 (2020年12月26日 初版第1刷発行)」を読みました。書籍では理由や経緯などが書かれており、とてもためになりました。ありがとうございました。
そこで、Reactの理解を深めるために何か作ろうと考え、よくあるToDo管理アプリを作ってみたことから、その時のメモを残します。
目次
環境
React 17.0.2
TypeScript 4.1.2
Python 3.9.5
Django 3.2.2
Django REST framework 3.12.4
PyCharm Professional 2021.1.1
今回は各段階を追ってアプリを作成していきます。
アプリのイメージははこんな感じで、段階ごとの見た目はほとんど変わりません。
ToDo管理アプリ
やることとやらないこと
やること
Reactまわり
TypeScriptで書く
React Router の5系で書く
Hooksを使う
Django REST frameworkまわり
やらないこと
React & TypeScriptに慣れるため、以下をやらないことにしました。
環境構築
最初のうちは Django REST frameworkを使いませんが、忘れないよう venv で環境を作っておきます。
% python --version
Python 3.9.5
% python -m venv env
CRAによる、React環境構築
% npx create-react-app frontend --template typescript
We suggest that you begin by typing:
cd frontend
yarn start
Happy hacking!
ESLint
CRAでESLintはインストールされているため、init します。
% yarn eslint --init
? How would you like to use ESLint? …
❯ To check syntax, find problems, and enforce code style
? What type of modules does your project use? …
❯ JavaScript modules (import/export)
? Which framework does your project use? …
❯ React
? Does your project use TypeScript? › Yes
? Where does your code run? … (Press <space> to select, <a> to toggle all, <i> to invert selection)
✔ Browser
? How would you like to define a style for your project? …
❯ Use a popular style guide
? Which style guide do you want to follow? …
❯ Airbnb: https://github.com/airbnb/javascript
? What format do you want your config file to be in? …
❯ JavaScript
Checking peerDependencies of eslint-config-airbnb@latest
Local ESLint installation not found.
The config that you've selected requires the following dependencies:
eslint-plugin-react@^7.21.5 @typescript-eslint/eslint-plugin@latest eslint-config-airbnb@latest eslint@^5.16.0 || ^6.8.0 || ^7.2.0 eslint-plugin-import@^2.22.1 eslint-plugin-jsx-a11y@^6.4.1 eslint-plugin-react-hooks@^4 || ^3 || ^2.3.0 || ^1.7.0 @typescript-eslint/parser@latest
? Would you like to install them now with npm? › No
エラーが出ました。
ESLint couldn't find the config "airbnb" to extend from. Please check that the name of the config is correct.
The config "airbnb" was referenced from the config file in "".
If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.
error Command failed with exit code 2.
足りていないものを、yarnでインストールします。
% yarn add eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest eslint-config-airbnb@latest eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react-hooks @typescript-eslint/parser@latest
info All dependencies
├─ @typescript-eslint/eslint-plugin@4.22.1
├─ @typescript-eslint/parser@4.22.1
├─ eslint-config-airbnb-base@14.2.1
├─ eslint-config-airbnb@18.2.1
├─ eslint-plugin-import@2.22.1
├─ eslint-plugin-jsx-a11y@6.4.1
├─ eslint-plugin-react-hooks@4.2.0
└─ eslint-plugin-react@7.23.2
設定ファイルを追加・更新します。
全部載せると長いので、リンクをはっておくだけにします。
Prettier
インストールします。
% yarn add -D prettier eslint-config-prettier
info All dependencies
├─ eslint-config-prettier@8.3.0
└─ prettier@2.2.1
» TypeSync v0.8.0
✔ 2 new typings added.
📦 frontend — package.json (2 new typings added, 0 unused typings removed)
├─ + @types/prettier
└─ + @types/testing-library__jest-dom
✨ Go ahead and run npm install or yarn to install the packages that were added.
% yarn
success Saved lockfile.
設定ファイルを追加します。
ESLintとPrettierが衝突していないか確認します。
% npx eslint-config-prettier 'src/**/*.{js,jsx,ts,tsx}'
No rules that are unnecessary or conflict with Prettier were found.
package.json のscriptにeslintやprettierを追加します。
"fix": "npm run -s format && npm run -s lint:fix",
"format": "prettier --write --loglevel=warn 'src/**/*.{js,jsx,ts,tsx,gql,graphql,json}'",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}",
"lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'",
"lint:conflict": "eslint --print-config .eslintrc.js | eslint-config-prettier-check",
"preinstall": "typesync"
PyCharmの設定
今回はProfessional版を使うので、prettier pluginを追加します。
その後、以下の設定を行います。
ESLintの設定
Languages & Frameworks > JavaScript > Code Quality Tools > ESLint
[x] Automatic ESLint configuration
Run for files: (デフォルトのまま)
{**/*,*}.{js,ts,jsx,tsx,html,vue}
[ ] Run eslint --fix on save
Prettierの設定
Languages & Frameworks > JavaScript > Prettier
Prettier package: <プロジェクトの node_modules にある prettierを指定>
Run for files: (デフォルトのまま)
{**/*,*}.{js,ts,jsx,tsx}
[x] On code reformat
[x] ON save
Prettierの設定ができているかは、index.tsx の
ReactDOM.render(
< React.StrictMode>
< App />
< /React.StrictMode>,
document .getElementById ( 'root' ),
);
をワンライナー にした状態で保存した時に、上記のように修正されればOKです。
huskyとlint-staged
インストールします。
% yarn add -D husky lint-staged
package.json の devDependencies
の下に追加します。
"husky ": {
"hooks ": {
"pre-commit ": "lint-staged "
}
} ,
"lint-staged ": {
"src/**/*.{js,jsx,ts,tsx} ": [
"prettier --write --loglevel=warn ",
"eslint --fix "
] ,
"src/**/*.{json} ": [
"prettier --write --loglevel=warn "
]
}
今回は段階を追ってアプリを作成していくため、
srcの下に component
を作る
componentの下に、 v1
のような各段階ごとのcomponentを入れるディレクト リを作る
のようなディレクト リ構造としました。
第1段階:入力フォームとリスト表示を1つのコンポーネント に入れたバージョン
まずは入力フォームとリスト表示を1つのコンポーネント に入れてみます。
React Routerと使用するバージョンについて
各段階でURLを変えるため、React Routerを使います。
りあクト!では v5 と v6 の話がありましたが、今のところまだ v6 はリリースされていないため、今回は v5 系を使います。
インストール
% yarn add react-router react-router-dom
% yarn
App.tsx の実装
App.tsx にReact Routerの実装を行います。
今回は
な書き方とします。
import React, { VFC } from 'react' ;
import './App.css' ;
import { Switch, Route, Redirect } from 'react-router' ;
import V1Home from 'components/v1/V1Home' ;
import Home from './components/Home' ;
const App: VFC = () => (
< div className= "container" >
< Switch>
< Route exact path= "/" >
< Home />
< /Route>
< Route exact path= "/v1" >
< V1Home />
< /Route>
< Redirect to= "/" />
< /Switch>
< /div>
);
export default App;
V1Home.tsx
React Routerから呼ばれるコンポーネント です。この中で V1FormList
コンポーネント を呼び出しています。
import { VFC } from 'react' ;
import V1FormList from './V1FormList' ;
const V1Home: VFC = () => (
<>
< h1> タスクリスト< /h1>
< V1FormList />
< />
);
export default V1Home;
全体像はこんな感じです。以降で詳しく見ていきます。
import { FormEvent, useEffect, useRef, useState, VFC } from 'react' ;
type Task = {
id: number ;
content: string ;
} ;
const V1FormList: VFC = () => {
const [ content, setContent] = useState( '' );
const [ tasks, setTasks] = useState< Task[] >( [] );
const taskRef = useRef< HTMLInputElement | null >( null );
useEffect(() => {
taskRef.current?.focus();
} );
const addTask = ( e: FormEvent) => {
e.preventDefault();
setTasks( [ ...tasks, { id: tasks.length , content }] );
setContent( '' );
} ;
return (
<>
< form onSubmit = { addTask} >
< input
type= "text"
name= "task"
ref= { taskRef}
onChange= { ( e) => setContent( e.target .value) }
value= { content}
/>
< button type= "submit" > 登録< /button>
< /form>
< ul>
{ tasks.map(( task) => (
< li key= { task.id } > { task.content} < /li>
)) }
< /ul>
< />
);
} ;
export default V1FormList;
State Hookの利用
上記では、input要素で入力した値を保管するために State Hook を使っています。
ステートフックの利用法 – React
まずは入力値を入れておく State Hookを用意します。
const [ content, setContent] = useState( '' );
次に、入力した値はリストとして表示したいため、リスト表示用の State Hook も用意します。
Stateの型は上記の content を配列で持つ string[]
でも良いのですが、リスト表示する時の key を与えたいため、型を用意します。
リストと key – React
type Task = {
id: number ;
content: string ;
} ;
この型を使った State を用意します。
const [ tasks, setTasks] = useState< Task[] >( [] );
フォームで登録ボタンを押した時に、リスト表示用の State に値を追加する関数を用意します。
const addTask = ( e: FormEvent) => {
e.preventDefault();
setTasks( [ ...tasks, { id: tasks.length , content }] );
setContent( '' );
} ;
ここで、addTask関数の引数は FormEvent型 を想定しています。
Forms and Events | React TypeScript Cheatsheets
また、 preventDefault()
を使ってデフォルトのイベントをキャンセルし、バックエンドにリクエス トが飛ばないようにします。
また、配列のStateでは、Array.pushが使えません。
そのため、スプレッド構文を使って、Stateに値を追加しています。
setTasks( [ ...tasks, { id: tasks.length , content }] );
最後の setContent('');
は、リスト表示用Stateに値を入れたら画面のinput要素をクリアするために用意しています。
あとは
formの onSubmit
に、追加用関数 addTask
を割り当て
inputの onChange
に、content
Stateの更新処理を追加
inputの value
に、 onChangeで更新した値を割り当て
ulの中で、 tasks
Stateのデータを元に表示
とします。
return (
<>
< form onSubmit = { addTask} >
< input
type= "text"
name= "task"
onChange= { ( e) => setContent( e.target .value) }
value= { content}
/>
< button type= "submit" > 登録< /button>
< /form>
< ul>
{ tasks.map(( task) => (
< li key= { task.id } > { task.content} < /li>
)) }
< /ul>
< />
);
フォーカスの指定
ここで、「登録ボタンを押した後、input要素にフォーカスを戻したい」という要望がある場合、どのように実装すればよいかを調べてみました。
りあクト!Ⅱの p192 より、Reactでは、 useRef
と useEffect
を使うと良さそうでした。
const taskRef = useRef< HTMLInputElement | null >( null );
useEffect(() => {
taskRef.current?.focus();
} );
ちなみに、型定義や useRefの引数から null
を抜くと、以下のエラーとなりました。
TS2322: Type 'MutableRefObject<HTMLInputElement | undefined>' is not assignable to type 'LegacyRef<HTMLInputElement>| undefined'.
Type 'MutableRefObject<HTMLInputElement | undefined>' is not assignable to type 'RefObject<HTMLInputElement>'.
Types of property 'current' are incompatible.
Type 'HTMLInputElement | undefined' is not assignable to type 'HTMLInputElement | null'.
Type 'undefined' is not assignable to type 'HTMLInputElement | null'.
この taskRef
を input に紐付けます。
< input
..
ref= { taskRef}
..
/>
動作確認
以下でReactが起動しますので、タスクを色々いれてみます。
% yarn start
これで第1段階が完成しました。コミットは こちら です。
第1段階では ListForm.tsx の中に Form と List が含まれていました。
次はそれらを分離してみます。
Stateをどこに持たせるか
分離するにあたり、 State をどこに持たせるかを考えます。
content
は Form でしか使っていませんが、 tasks
は Form と List の両方で使っています。
Reactの流儀によると、
共通の親コンポーネント か、その階層構造でさらに上位の別のコンポーネント が state を持っているべきである
https://ja.reactjs.org/docs/thinking-in-react.html#step-4-identify-where-your-state-should-live
とのことなので、今回の共通の親である V2Home.tsx
に tasks
Stateを持たせます。
const V2Home: VFC = () => {
const [ tasks, setTasks] = useState< Task[] >( [] );
return (
<>
< h1> タスクリスト ( v2)< /h1>
< V2Form tasks= { tasks} setTasks= { setTasks} />
< V2List tasks= { tasks} />
< />
);
} ;
型定義
ここで、 Task
型はどのコンポーネント でも使うので、 src/components/Task.ts
に型定義を置いておきます。
export type Task = {
id: number ;
content: string ;
} ;
なお、Task typeをV2Home.tsx に持たせ、
import V2Form from './V2Form' ;
import { Task } from './V2Home' ;
のように import したところ
ESLint: Dependency cycle detected.(import/no-cycle)
というESLintのエラーが出ました。
あとは、FormとListを分離します。
このコンポーネント がpropsで受け取る型は
type Props = {
tasks: Task[] ;
setTasks: Dispatch< SetStateAction< Task[] >>;
} ;
とします。
なお、setTasksの型 Dispatch<SetStateAction<Task[]>>
については、PyCharmで提案されたものをそのまま入れました。
Formの全体像はこちら。
type Props = {
tasks: Task[] ;
setTasks: Dispatch< SetStateAction< Task[] >>;
} ;
const V2Form: VFC< Props> = ( { tasks, setTasks } ) => {
const [ content, setContent] = useState( '' );
const taskRef = useRef< HTMLInputElement | null >( null );
useEffect(() => {
taskRef.current?.focus();
} );
const addTask = ( e: FormEvent) => {
e.preventDefault();
setTasks( [ ...tasks, { id: tasks.length , content }] );
setContent( '' );
} ;
return (
<>
< form onSubmit = { addTask} >
< input
type= "text"
name= "task"
ref= { taskRef}
onChange= { ( e) => setContent( e.target .value) }
value= { content}
/>
< button type= "submit" > 登録< /button>
< /form>
< />
);
} ;
V2List.tsx
Listも、分離したもの + propsの型定義を用意します。
type Props = {
tasks: Task[] ;
} ;
const V2List: VFC< Props> = ( { tasks } ) => (
<>
< ul>
{ tasks.map(( task) => (
< li key= { task.id } > { task.content} < /li>
)) }
< /ul>
< />
);
これで第2段階が完成しました。コミットは こちら です。
第3段階:Django REST framework製バックエンドと連携する
ここまでは入力した値は State にしか保存していませんでした。そのため、リロードしたら消えてしまいます。
そこで次はDBを持つバックエンドを用意して、データを永続化します。
Django REST framework アプリの作成
今回はReactの習得がメインなので、バックエンドは慣れている Django REST framework (以降、DRF ) で作ります。
今回はCRUD ができれば良いので、ModelViewSetを使ってお手軽に作ります。
詳しい実装は省略しますが、気になる場合は以下の書籍を参考にしてください。
現場で使える Django REST Framework の教科書(第2版) - あきよこブログ(akiyoko blog) - BOOTH
DRF をインストールします。
(env) % pip install django djangorestframework
...
Successfully installed asgiref-3.3.4 django-3.2.2 djangorestframework-3.12.4 pytz-2021.1 sqlparse-0.4.1
Django プロジェクトとアプリを作ります。
(env) % django-admin startproject config .
(env) % python manage.py startapp task
(env) % python manage.py startapp apiv1
なお、DRF とReactを連携する際、DRF とReactが
Reactはポート3000で起動
DRF はポート8000で起動
するため、別オリジンとなります。
そこで、 django-cors-headers
を追加でインストールと設定を行い、ReactからのDRF アクセスを許可します。
adamchainz/django-cors-headers: Django app for handling the server headers required for Cross-Origin Resource Sharing (CORS)
(env) % pip install django-cors-headers
ちなみに、手元にあるDRF の教科書(2019/9/22 v1.0.0)で使っていた 3.1.0では、別オリジンの設定名は CORS_ORIGIN_WHITELIST
でしたが、今回使った 3.7.0 では CORS_ALLOWED_ORIGINS
に変わっていました。
CORS_ALLOWED_ORIGINS = (
'http://localhost:3000' ,
'http://127.0.0.1:3000' ,
)
PyCharmにDjango を認識させます。
# Languages & Frameworks > Django
[x] Enable Django support
Django project root : <root dir>
Settings: config/settings.py
Manage script: manage.py
準備ができたため、 このコミット のようなアプリを作ります。
アプリを作り終えたらマイグレーション します。
(env) % python manage.py makemigrations
(env) % python manage.py migrate
あとはDRF を起動し、Browsable API で http://localhost:8000/api/v1/tasks/
にアクセス、データの登録などができることを確認します。
axiosのインストール
ReactからDRF へリクエス トを飛ばすために、axiosをインストールします。
% yarn add axios
% yarn
連携するため、
初期値のロード
登録ボタンを押すと、DRF 側へリクエス トが飛んで保存
の2つを実装します。
初期値のロード
初期値をロードする Effect Hook を用意します。
React Hooksでデータを取得する方法 - Qiita
useEffect(() => {
console.log( 'called!' );
const fetchData = async () => {
const response: AxiosResponse< ApiGetResponse> = await axios.get( API_URL);
setTasks( response.data);
} ;
void fetchData();
} , [ setTasks] );
登録ボタンを押すと、DRF 側へリクエス トが飛んで保存
ボタンを押したらリクエス トを飛ばすため、addTask関数の中に処理を追加します。
reactjs - How to POST request using axios with React Hooks? - Stack Overflow
axiosを使って非同期でリクエス トを飛ばすため、 async/awaitも使います。
const addTask = async ( e: FormEvent) => {
e.preventDefault();
const requestData = { content } ;
try {
const response: AxiosResponse< ApiPostResponse> = await axios.post(
API_URL,
requestData,
);
setTasks( [ ...tasks, { id: response.data.id , content }] );
setContent( '' );
} catch ( error) {
alert( 'エラーでした' );
}
} ;
なお、axiosを使う時の型ですが、AxiosResponseは
import axios, { AxiosResponse } from 'axios' ;
と、axios から importします。
また、axiosのレスポンスのdataの型は、
type ApiGetResponse = {
id: number ;
content: string ;
}[] ;
type ApiPostResponse = {
id: number ;
content: string ;
} ;
と用意し、
const response: AxiosResponse< ApiPostResponse> = await axios.post()
のように渡します。
これで第3段階も完成です。コミットは3つに分かれています。
第4段階:削除ボタンを付ける
今までは作成するだけだったので、削除ボタンを付けてみます。
Listの方に付けるため、まずは削除する関数を用意します。
removeTask関数の中では、
axiosで削除
tasks Stateに、引数で渡された task.id と一致しないものだけセット
します。
const removeTask = async ( taskId: number ) => {
try {
await axios.delete( `${API_URL}${taskId}/` );
setTasks( tasks.filter(( task) => task.id !== taskId));
} catch ( error) {
alert( 'エラーでした' );
}
} ;
そして、Listのコンポーネント の中でも setTasks Hookを使うようになったため、型定義とpropsを追加します。
type Props = {
tasks: Task[] ;
setTasks: Dispatch< SetStateAction< Task[] >>;
} ;
const V4List: VFC< Props> = ( { tasks, setTasks } ) => { ... }
これで完成です。コミットは こちら 。
Github に上げました。
https://github.com/thinkAmi-sandbox/todolist_by_react_ts_drf
また、今回実装した機能がまとまっているプルリクはこちらです。
https://github.com/thinkAmi-sandbox/todolist_by_react_ts_drf/pull/1