Python×Django+RestFramework×ReactでWebアプリケーション➄ JavaScript・・・を踏まえてReact その➋ 動的なコンポーネント
1.前回の振り返り
前回は、JSXというReactにおけるHTMLファイルのようなタグを使い、入力欄・未購入欄・購入済欄の三つのコンポーネントを作成。
CSSファイルで見た目を整形して、JavaScriptバージョンの「お買い物リスト」と同様の見た目を作りました。(静的)
2.今回のゴール
今回は動的にコンポーネントを変化させる方法で、入力欄の内容を未購入欄に移す、購入済ボタンで購入済欄に移動させる等の動的な処理方法を記載していきたいきたいと思います。
JavaScriptバージョンと同様に動くことがゴールです。
3.Reactを動的に動かすには
Reactで動的な処理を行うには、state管理をする必要があります。
Reactにはクラスコンポーネントと関数コンポーネントがあり、もともとはクラスコンポーネントのみstate管理が可能でした。
Reactのバージョンアップにより、Hooksというものが追加され、関数コンポーネントでもstate管理が可能となっているのが現状です。
(1) クラスコンポーネントでのstate管理
まずはクラスコンポーネントでボタンを押してカウントアップする処理を書いてみます。Creat SandboxでReactを選択してApp.jsに以下のように記載します。
App.js
じゃんけんの手を画面に表示する処理です。
ボタンを押す度に手がランダムで変わります。
import "./styles.css";
import React from "react";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { select: 0 };
this.hands = ["ぐー", "ちょき", "ぱー"];
}
render() {
return (
<>
<p> {this.hands[this.state.select]} </p>
<button
onClick={() =>
this.setState({ select: Math.floor(Math.random() * 3) })
}
>
手を変える
</button>
</>
);
}
}
クラスコンポーネントの書き方として、まずはReact.Componentを継承(extends) してクラスを定義します。(今回はAppというクラス)
つぎにconstructorでAppクラスにどんな属性を持たせるかを定義します。
super(props)となっているのは、親クラス(Component)の属性を参照しています。
そのうえで、selectという変数をstateとして定義し、同時にhandsという「ぐー、ちょき、ぱー」の要素を持った配列型のプロパティを定義してます。
配列の何番目の要素を見に行くか?の「何番目」がselectとし、かつstate(状態)管理の対象としています。
なお、プロパティにアクセスする際はthisで「このクラスのプロパティ」だと言わないと、未定義と判断されてしまいます。
また、stateについては、this.state.変数としてアクセスします。
render要素内のpタグの中には、「hands配列のselect番目」を表示するよう記載し、buttonタグの中でクリック時に、JavaScriptのMathオブジェクトで0~2のランダムな数値を算出し、selectを更新するように記載しています。
setstateというのが、stateを更新する処理です。
・・・ちょっと面倒くさいといのが、直感的な感想ですね。
(2) 関数コンポーネントで同じコードを書いてみる
次に関数コンポーネントで同じ処理を書いてみます。
少しおさらいで、コンポーネントを作成して部品として利用するやり方でやってみます。
まず、componentsフォルダを作成し、先ほどのクラスコンポーネントをClassHands.jpというファイル・クラス名に変えたうえで保存します。
そして、関数コンポーネントを作成する用のFuncHands.jsファイルも用意しておきます。
次に、App.jsファイルの中身は以下のようにします。
フォルダ構造
App.js
import "./styles.css";
import ClassHands from "./components/ClassHands";
import FuncHands from "./components/FuncHands";
export default function App() {
return (
<>
<ClassHands />
</>
);
}
ClassHands.js
クラス名も直してください。
import React from "react";
export default class ClassHands extends React.Component {
constructor(props) {
super(props);
this.state = { select: 0 };
this.hands = ["ぐー", "ちょき", "ぱー"];
}
render() {
return (
<>
<p> {this.hands[this.state.select]} </p>
<button
onClick={() =>
this.setState({ select: Math.floor(Math.random() * 3) })
}
>
手を変える
</button>
</>
);
}
}
これでまずはちゃんと先ほど同様動作することを確かめてください。
次にFuncHands.jsを以下のように記載します。
FuncHands.js
import { useState } from "react";
export default function FuncHands() {
const [select, setSelect] = useState(0);
const hands = ["ぐー", "ちょき", "ぱー"];
return (
<>
<p> {hands[select]} </p>
<button onClick={() => setSelect(Math.floor(Math.random() * 3))}>
手を変える
</button>
</>
);
}
const [select, setSelect] = useState(0)
この一文だけで、useStateを利用しステートフックとして管理する対象と対象を更新する処理(setState)、初期値(0)を定義できちゃいます。
また、「this」を記載しなくてもOKです。
見比べてどうでしょうか・・・、関数コンポーネントの方が記載するコードが少なく済みますし(呪文的なものが減る)、とても読みやすいと思います。
そしてApp.jsの中を以下のように書き替えましょう。
App.js
import "./styles.css";
import ClassHands from "./components/ClassHands";
import FuncHands from "./components/FuncHands";
export default function App() {
return (
<>
<FuncHands />
</>
);
}
呼び出すコンポーネントを、FuncHandsに変更してますが、動いたでしょうか?
Problemsとして、ワーニングが出てますがimportしているClassHandsを使ってないよねーというものなので無視です。
4.変数や関数を他のコンポーネントに渡す方法(props)
(1) propsって何?
もう一つReactで押さえておくべき基本的なポイントとしてpropsがあります。
複数のコンポーネントに分割すると、あるコンポーネントで定義したstateや関数などを、別のコンポーネントに値を渡したりする必要が出てくると思います。
引き渡された値やオブジェクトをpropsとして、利用することができます。
(2) どう使えばいいの?
実際に今回の例で見てみましょう。
InputAreaコンポーネントには、入力欄や追加ボタンが用意されていますが、入力欄で拾った文字列も含めた未購入一覧はHaveNotBuyコンポーネントに追加する必要があります。
そこで、App.jsステートフックを定義し、各コンポーネントにステートフックや関数をpropsとして引き渡したいと思います。
(3) App.js
import "./styles.css";
//➊useStateをインポート
import { useState } from "react";
import InputArea from "./components/InputArea";
import HaveNotBuy from "./components/HaveNotBuy";
import HaveBuy from "./components/HaveBuy";
export default function App() {
//➋ステートフックを定義
//➌入力欄の文字列の状態を管理
const [addText, setAddText] = useState("");
//➍未購入一覧を管理
const [needToBuys, setNeedToBuys] = useState([]);
//➎入力欄の文字列を入力に応じて更新する関数
const onChangeText = (evt) => setAddText(evt.target.value);
//➏追加ボタンを押された際に、既存の未購入一覧にaddTextを加えて更新する関数
const addNeedToBuy = () => {
const newNeedToBuys = [...needToBuys, addText];
console.log(needToBuys);
setNeedToBuys(newNeedToBuys);
setAddText("");
};
return (
<div className="App">
<h1>お買い物リスト</h1>
{/* ➐ InputAreaに➌ステータスフックと➎・➏関数を引き渡します */}
<InputArea
addText={addText}
onChangeText={onChangeText}
addNeedToBuy={addNeedToBuy}
/>
{/* ➑HaveNotBuyにneedToBuys(配列)を引き渡す */}
<HaveNotBuy needToBuys={needToBuys} />
<HaveBuy />
</div>
);
}
➊ useStateをインポートします。
➋ ステータスフック➌・➍を定義していきます。
➌ 入力欄の文字列の状態を管理・更新します
➍ 未購入一覧の状態を管理・更新します
➎ 入力欄の文字列を入力に応じ更新する関数です
➏ 追加ボタンが押された際に、既存の未購入一覧(配列)に文字列を追加する関数です。needToBuysを更新したら、addTextをブランクに戻す処理も記載。
➐ InputAreaに➎・➏関数を引き渡します
➑ HaveNotBuyに➍ステートフックを引き渡します
(4) InputArea.js
//➊ propsを引数としてとる
export default function InputArea(props) {
//➋ propsを分割代入でそれぞれ、addText, onChangeText, addNeedToBuyを取得
const { addText, onChangeText, addNeedToBuy } = props;
return (
<>
<div className="input-area">
<h2 id="header">入力欄</h2>
{/*➌valueにaddTextをonChangeにonChangeText関数を渡す*/}
<input
id="input"
type="text"
placeholder="買うものを入力"
value={addText}
onChange={onChangeText}
/>
{/*➍onClickにaddNeedToBuy関数を渡す*/}
<button id="add" onClick={addNeedToBuy}>
追加
</button>
</div>
</>
);
}
➊ propsを引数としてInputArea関数コンポーネントでとる
➋ propsを分割代入でそれぞれ、addText, onChangeText, addNeedToBuyを取得
➌ valueにaddTextをonChangeにonChangeText関数を渡す
➍onClickにaddNeedToBuy関数を渡す
(5) HaveNotBuy.js
export default function HaveNotBuy(props) {
const { needToBuys } = props;
return (
<div className="have-not-buy">
<h2 className="header">未購入</h2>
<ul>
<div id="need-to-buy">
{/*➊needToBuys(配列)の要素をマップ関数で、一つずつliタグ内に記載していく */}
{needToBuys.map((needToBuy, index) => {
return <li key={index}>{needToBuy}</li>;
})}
</div>
</ul>
</div>
);
}
InputAreaで解説済の要素は省いて書きます。
➊ needToBuysに格納されている未購入一覧(配列)をmap関数で一つずつ、liタグで要素を表示させます。この時、liタグにはそれぞれ固有のKeyが必要となるのでkeyプロパティにmap関数内の引数で渡したindex※を渡します。
※第2引数においた変数に配列のインデックスが格納されます。
(6) 動作確認
それでは実際に動かしてみましょう。
インプット欄の文字列もクリアされています。
あとは、購入済ボタンと削除ボタン、それぞれの処理を実装すれば完成です。
5.お買い物リスト(React版)の整理
Reactのstate Hooksとpropsを大体理解したところで、買い物リストの動的な処理を実装済のものも含めまとめます。
お買い物リストにおいて、動的に変化するものは大きく以下3つと定義します。
➊ INPUTタグに入力される文字列 ・・・ 済
➋ 未購入エリアに表示される、未購入のもの ・・・一部済
➌ 購入エリアに表示される、購入済のもの・・・一部済
また、処理として関数を定義する必要があるものを以下4つと定義します。
INPUTタグに入力された文字列をstateとして更新・保持する処理・・・済
追加ボタンを押したら、INPUTタグに入力されStateとして保持される文字列を未購入一覧に追加・未購入エリアに表示する処理・・・済
未購入エリアで購入済ボタンを押したら、購入済一覧に追加・購入済エリアに表示する処理・・・未済
未購入エリアで削除ボタンを押したら、未購入一覧から削除する処理・・・未済
購入済エリアで削除ボタンを押したら、購入済一覧から削除する処理・・・未済
では、未済部分について実装していきましょう。
6.未実装部分の取込み① (購入済ボタン・処理)
処理としては、未購入一覧から消して、購入済一覧に表示をさせる。ただし、常に一つしかない入力欄の文字列に対し、こちらは複数の配列の中から対象を指定して追加や削除を行う必要があるというのが違い目となります。
(1) App.js
import "./styles.css";
//➊useStateをインポート
import { useState } from "react";
import InputArea from "./components/InputArea";
import HaveNotBuy from "./components/HaveNotBuy";
import HaveBuy from "./components/HaveBuy";
export default function App() {
//➋ステートフックを定義
//➌入力欄の文字列の状態を管理
const [addText, setAddText] = useState("");
//➍未購入一覧を管理
const [needToBuys, setNeedToBuys] = useState([]);
//➑購入済一覧を管理
const [buyAlreadys, setBuyAlreadys] = useState([]);
//➎入力欄の文字列を入力に応じて更新する関数
const onChangeText = (evt) => setAddText(evt.target.value);
//➏追加ボタンを押された際に、既存の未購入一覧にaddTextを加えて更新する関数
const addNeedToBuy = () => {
const newNeedToBuys = [...needToBuys, addText];
setNeedToBuys(newNeedToBuys);
setAddText("");
};
//➒購入済ボタンを押した場合に、未購入一覧から購入済一覧に要素を移動する処理
const addBuyAlready = (index) => {
//未購入一覧(配列)を定義し、購入済ボタンを押した要素を削除し、needToBuysを更新
const newNeedToBuys = [...needToBuys];
newNeedToBuys.splice(index, 1);
//購入済の配列を作成(既存の配列に、ボタンが押された未購入一覧(配列)の要素を追加)
const newBuyAlreadys = [...buyAlreadys, needToBuys[index]];
//購入済一覧を更新
setBuyAlreadys(newBuyAlreadys);
//未購入一覧を更新
setNeedToBuys(newNeedToBuys);
};
return (
<div className="App">
<h1>お買い物リスト</h1>
{/* ➐ InputAreaに➌ステータスフックと➎・➏関数を引き渡します */}
<InputArea
addText={addText}
onChangeText={onChangeText}
addNeedToBuy={addNeedToBuy}
/>
{/* ➓ HaveNotBuyに➍ステートフック・➑関数を引き渡します */}
<HaveNotBuy needToBuys={needToBuys} addBuyAlready={addBuyAlready} />
{/* ⓫ HaveBuyに➑ステートフックを引き渡します */}
<HaveBuy buyAlreadys={buyAlreadys} />
</div>
);
}
➑~⓫が追加したものです。(⓫がみえづらくてすいません。)
➑ 購入済一覧のステートフックを定義してます。
➒ 購入済ボタンを押した際の関数を定義してます。一旦、更新用の新しい配列を用意して、未購入一覧からはindexで要素を指定して購入済となったものを削除。購入済一覧には、indexで要素を指定して未購入一覧の要素を追加しています。
➓ HaveNotBuyにaddBuyAlredy関数(➒)を追加で渡すようにします
⓫ HaveBuyには➑を渡します
(2) HaveBuy.js
export default function HaveBuy(props) {
const { buyAlreadys } = props;
return (
<div className="have-buy">
<h2 className="header">購入済</h2>
<ul>
<div id="buy-already">
{buyAlreadys.map((buyalready, index) => {
return <li key={index}>{buyalready}</li>;
})}
</div>
</ul>
</div>
);
}
説明済の要素なので割愛しますが、簡単に記載すると渡されるbuyAlreadysをmap関数で展開して、liタグを一つずつ生成しています。
(3) 動作確認
それでは動かしみましょう。
ごりらの購入済ボタンを押すと・・・
ごりらを買うことはできませんが、ごりらのおもちゃでも買ったのでしょう。
7.未実装部分の取込み➁ (削除ボタン・処理)
未購入一覧と購入済一覧それぞれの要素に削除ボタンを表示し、削除ボタンが押されたら要素を消すようにします。
(1) App.js
import "./styles.css";
//➊useStateをインポート
import { useState } from "react";
import InputArea from "./components/InputArea";
import HaveNotBuy from "./components/HaveNotBuy";
import HaveBuy from "./components/HaveBuy";
export default function App() {
//➋ステートフックを定義
//➌入力欄の文字列の状態を管理
const [addText, setAddText] = useState("");
//➍未購入一覧を管理
const [needToBuys, setNeedToBuys] = useState([]);
//❼購入済一覧を管理
const [buyAlreadys, setBuyAlreadys] = useState([]);
//➎入力欄の文字列を入力に応じて更新する関数
const onChangeText = (evt) => setAddText(evt.target.value);
//➏追加ボタンを押された際に、既存の未購入一覧にaddTextを加えて更新する関数
const addNeedToBuy = () => {
const newNeedToBuys = [...needToBuys, addText];
setNeedToBuys(newNeedToBuys);
setAddText("");
};
//➑購入済ボタンを押した場合に、未購入一覧から購入済一覧に要素を移動する処理
const addBuyAlready = (index) => {
//未購入一覧(配列)を定義し、購入済ボタンを押した要素を削除し、needToBuysを更新
const newNeedToBuys = [...needToBuys];
newNeedToBuys.splice(index, 1);
//購入済の配列を作成(既存の配列に、ボタンが押された未購入一覧(配列)の要素を追加)
const newBuyAlreadys = [...buyAlreadys, needToBuys[index]];
//購入済一覧を更新
setBuyAlreadys(newBuyAlreadys);
//未購入一覧を更新
setNeedToBuys(newNeedToBuys);
};
//⓬削除済ボタンを押した場合に、未購入一覧から要素を削除する
const deleteNeedToBuy = (index) => {
const newNeedToBuys = [...needToBuys];
newNeedToBuys.splice(index, 1);
setNeedToBuys(newNeedToBuys);
};
//⓬削除済ボタンを押した場合に、未購入一覧から要素を削除する
const deleteBuyAlready = (index) => {
const newBuyAlreadys = [...buyAlreadys];
newBuyAlreadys.splice(index, 1);
setBuyAlreadys(newBuyAlreadys);
};
return (
<div className="App">
<h1>お買い物リスト</h1>
{/* ➐ InputAreaに➌ステータスフックと➎・➏関数を引き渡します */}
<InputArea
addText={addText}
onChangeText={onChangeText}
addNeedToBuy={addNeedToBuy}
/>
{/* ➒ HaveNotBuyに➍ステートフック・➑・⓬関数を引き渡します */}
<HaveNotBuy
needToBuys={needToBuys}
addBuyAlready={addBuyAlready}
deleteNeedToBuy={deleteNeedToBuy}
/>
{/* ➓ HaveBuyに❼ステートフック・⓭関数を引き渡します */}
<HaveBuy buyAlreadys={buyAlreadys} deleteBuyAlready={deleteBuyAlready} />
</div>
);
}
すいません、もはや見えないと思いますが⓬・⓭に削除処理を追加しており、➒・➓のHaveNotBuyコンポーネント、HaveBuyコンポーネントへの引き渡し対象に削除関数を追加しています。
そのうえで、各コンポーネントで以下のようにpropsで関数を受け取りボタン処理として追記すればOKです。
(2) HaveNotBuy.js
export default function HaveNotBuy(props) {
const { needToBuys, addBuyAlready, deleteNeedToBuy } = props;
return (
<div className="have-not-buy">
<h2 className="header">未購入</h2>
<ul>
<div id="need-to-buy">
{/*➊needToBuys(配列)の要素をマップ関数で、一つずつliタグ内に記載していく */}
{needToBuys.map((needToBuy, index) => {
return (
<li key={index}>
{needToBuy}
<button onClick={() => addBuyAlready(index)}>購入済</button>
<button onClick={() => deleteNeedToBuy(index)}>削除</button>
</li>
);
})}
</div>
</ul>
</div>
);
}
(3) HaveBuy.js
export default function HaveBuy(props) {
const { buyAlreadys, deleteBuyAlready } = props;
return (
<div className="have-buy">
<h2 className="header">購入済</h2>
<ul>
<div id="buy-already">
{buyAlreadys.map((buyalready, index) => {
return (
<li key={index}>
{buyalready}
<button onClick={() => deleteBuyAlready(index)}>削除</button>
</li>
);
})}
</div>
</ul>
</div>
);
}
(4) 動作確認
では実際に動かしてみましょう。
ごりらを未購入リストから削除します
次にりんごとらっぱを購入済にしたうえで、りんごのみ購入済から削除します
以上で、お買い物リストをReactでも作成できました。
8.ここまでで学んだこととこれから
(1) ここまでで何を学んだか
ここまでやってきたのはあくまでもフロントエンド(クライアントサイド)として、画面表示・画面上での動的な処理です。
ブラウザを閉じてしまうと、未購入一覧も購入済一覧も消えてしまうので、何を買うべきだったか分からなくなってしまいます。
これでは、実用的ではないですね。
なので、本格的なWebアプリケーションを作成する場合、バックエンド(サーバーサイド)処理として、未購入のものをデータベースに登録したり、データベースから抽出した未購入一覧を画面上に表示してという処理が必要なります。
(2) DjangoかDjango REST Frameworkいずれとするか
そして、その処理をDjangoでまるっと行うか、それとも今回のゴールとしているDjangoはあくまでDB管理や対象データをAPIで取得できるように用意するまでとするか・・・なのです。
Djangoでまるっと対応する場合、フロントエンドはJavaScriptバージョンのお買い物リストを使用する必要があり、Django REST FrameworkでAPIを実装する場合は、Reactバージョンのお買い物リストが使えるのです。
(3) これから
次回以降は、まずDjangoでフロント・バックエンド含めまるっと対応するバージョンを見ていきたいと思います。