React のリストの表示を条件によって変えるには?
Vue の公式チュートリアル(英語版)のステップ8では、「Computed Property」で事前処理したデータを表示する課題があります。React ではどのようにするのかをこの記事で紹介しています。
React の基本は Typescript!
この連載では何回も触れていますが、React の表示の基本は全てプログラムです。つまり、Typescript (Javascript)で処理することになります。 したがって、Vue の「Computed property」のような概念はありません。 基本は、
* 表示の更新は、「ステート」を利用して管理する
* データの処理は基本的に関数(method)を使う
この例では、前回に引き続き、「Todo(やる事)」の一覧の表示を、完了したものを表示するかしないかを切り替えるわけですが、元々のやる事リスト(todos)をステートで管理して、実際の表示は、条件によって選別したデータのみを表示するような形で実現します。
したがって、「_filteredTodos()_」という関数で条件によって表示するデータを抜き出して、この関数が返すデータを表示するようにします。 ただし、「条件」が変わった場合にも、表示内容を更新する必要があるので、「条件」もステートで管理します。この場合の条件は、「_hideCompleted_」です。
条件によって、表示データを選別する関数は以下のようになります。
function filteredTodos(): Array<Todo> {
return todos.filter((todo: Todo): boolean => {
return !hideCompleted || (hideCompleted && !todo.done);
});
}
「_hideCompleted_」の値によって、表示するデータが変わるようになっています。
* hideCompletedがtrue:完了した項目のみ
* hideCompletedがfalse: 全ての項目
表示の部分は以下のようになります。
<ul>
{filteredTodos().map((todo): JSX.Element => (
<li key={todo.id}>
<input
id={todo.id}
type="checkbox"
defaultChecked={todo.done}
onChange={(e) => makeDone(e)}
/>
<span className={todo.done ? "done" : "wip"}>{todo.text}</span>
<button className="btn btn-secondary" onClick={() => removeTodo(todo)}>
X
</button>
</li>
))}
</ul>
これが、条件によって必要なデータを抜き出して表示を変える基本部分です。
他の追加機能
今回は、課題として上に挙げた以外の機能が実装されています。
* 新しい「やる事」を追加する機能
* 「やる事」の状態(完了か未完了)を「チェックボックス」を使って切り替える
* 完了した項目を表示する場合は、横線で消したように表示する
* 完了した項目を表示するか表示しないかを切り替える
まずは、やる事を追加する部分は、入力フォームでやる事を入力してボタンをクリックして追加するようになっています。 Vue の公式チュートリアルと変えている点があって、今回は、文字が入力されていない場合には追加しないようにしています。
まずは、新しい項目を入力する JSX の記述です。
<form className="form-group">
<input
className="form-control"
type="text"
ref={todoField}
placeholder="Enter a new task"
/>
<button className="btn btn-primary" onClick={() => addTodo()}>
Add Todo
</button>
</form>
ボタンがクリックされたら、「_addTodo()_」を呼び出すようにしています。 「_ref_」を使って、入力された文字を取り込めるように記述しています。この「リファレンス」は Typescript(Javascript)の定数として別に宣言しています。
const todoField: React.RefObject<HTMLInputElement> =
React.createRef<HTMLInputElement>();
こうすることで、JSX の「部品」とこの定数を紐づけて、入力したデータを取り込む事ができます。
項目を追加する関数の例です。
function addTodo(): void {
if (todoField.current) {
const value: string = todoField.current.value;
// Ignore if the task is empty
if (value !== "") {
const updatedTodos: Array<Todo> = produce(todos, (draft: Array<Todo>) => {
const newTodo = { id: uuidv4(), text: value, done: false };
draft.push(newTodo);
return draft;
});
setTodos(updatedTodos);
}
}
}
入力フィールドのリファレンスは、この「部品」が組み込まれた状態で有効になるため、あらかじめ「_todoField.current_」が有効であることをチェックした上で値を取得します。有効でない場合には無視するようになっています。このあたりが、Typescript の特徴的な記述方法ですが、あらかじめデータの有効性をチェックして処理するような記述が可能なため、無効な値の場合の処理で発生する例外を除外する事が可能になるため変数などの初期化の問題を最小限にする事ができます。 基本的に表示されている状態では、この値は有効なのでこの処理で問題ありません。
あとは、既存の「_todo_」の中身に変更を加えるので、複製を作った上でデータを追加して、リストごと(「箱」ごと)入れ替えることで、ステートの更新が検出できるようにしています。
項目の状態(完了・未完了)の切り替えは、「チェックボックス」で行います。
<input
id={todo.id}
type="checkbox"
defaultChecked={todo.done}
onChange={(e) => makeDone(e)}
/>
HTML の「_input_」のタグに相当する仮想 DOM を使います。チェックされた状態が「完了」で、チェックされた状態が「未完了」になっています。 やる事の項目には、前回の項目(_id/text_)に加えて、「_done_」が追加されていて、この状態によって最初の表示がチェックされているかどうかを決めています。 この「チェック」の状態が変化した場合に、状態を更新するようにしています。
function makeDone(e: React.ChangeEvent<HTMLInputElement>): void {
const value: boolean = e.target.checked;
const id: string = e.target.id;
const updatedTodos = produce(todos, (draft: Array<Todo>): Array<Todo> => {
const selected: Todo | undefined = draft.find((todo: Todo): boolean => {
return id === todo.id;
});
if (selected) {
selected.done = value;
}
return draft;
});
setTodos(updatedTodos);
}
処理は、JSX の仮想 DOM に「_id_」のアトリビュートをつけておきます。これが、やる事の項目の「_id_」と同じになるようにしておくと、状態が変わった項目を特定する事ができます。Typescript(Javascript)の「_find()_」を使って、「_todos_」の中から、相当する項目を見つけてデータを更新します。前回同様、リスト(配列)の中身を更新するので、複製を作って更新した後に、リストごと(「箱」ごと)入れ替える処理をします。
基本的には、相当する項目が見つかるはずですが、Typescript では、「_find()_」の結果が「_undefined_」になる場合があるので、その場合は無視するようにしてあります。
「やる事」の状態によって表示を変えるのは、仮想 DOM の CSS のためのタグを切り替えることで行います。
<span className={todo.done ? "done" : "wip"}>{todo.text}</span>
完了している場合と、完了していない場合で、「className(HTML の class に相当)」のタグの名前を変えて、CSS で表示を変えるようにします。
「_./styles/step8.css_」の例です。
.done {
text-decoration: line-through;
}
「wip(Work In Progress:処理中)」の場合は、何もしないので、特に記述はしていません。
完了した項目を表示するかしないかを切り替えるのは、ボタンのクリックで切り替わるようにします。
.....
const [hideCompleted, setHideCompleted]: [
boolean,
React.Dispatch<React.SetStateAction<boolean>>
] = useState(false as boolean);
.....
<button
className="btn btn-success"
onClick={() => setHideCompleted(!hideCompleted)}
>
{hideCompleted ? "Show all" : "Hide completed"}
</button>
「hideCompleted_」もステートで管理して、クリックされるたびに、「_true/_false_」が入れ替わるような処理にしています。
リストを表示する場合の注意
Vue の場合は、リストの各項目には「_:key_」で項目を区別するための「キー」を入れる必要があります。 前回は、特に触れていませんでしたが、React でも同じような処理が必要です。これを入れて行いと正しく表示が更新されない場合があります。特に、今回は、条件によって表示を変えているので、どの項目が同じ項目かを正しく指定しないと表示が正しく行われない場合があります。
今回の場合、各項目に「_id_」をつけているのでこれを利用しています。React の場合「_key={todo.id}_」のようにアトリビュートで指定します。 これを指定していないと、項目の完了・未完了を示すチェックなどが正しく表示されません。
idを指定する場合の注意
既に、お気づきの方もいらっしゃると思いますが、前回の例とは「_id_」の指定のやり方を変えています。 前回は、新たに項目を追加する必要がなかったため、「let id: number = 0」で変数を作って指定すれば特に問題がありませんでした。 ところが、今回の例では、新しい「やる事」を追加する機能を付けたために、追加の度に「_id_」を取得する必要があります。この「_id_」を「_id++_」で取得すると、連続して追加のボタンを押した場合、重複した「_id_」になってしまう場合があります。
これは、連続してボタンをクリックした場合の処理が「非同期」で呼び出されるために発生します。上で説明しているように、この「_id_」を表示の際の項目を特定する目的で利用しているので、重複している「_id_」が発生すると表示に問題が出る可能性が出てきます。
そこで、今回は、インターネット上で公開されている「_uuid_」というモジュールを使って ID を取得する方法に変更しています。 これを使うと、このモジュールの関数(_v4_)を呼び出す度に異なる ID を取得できるのでこうした問題が起きません。
(*)Vue の場合は、上の処理を非同期で行わないようになっていると考えられます。ソースコードを見て検証したわけではありませんが、実際に問題は起きていません。
まとめ
今回は、Vue の「Computed property」に関する課題を React ではどう処理するかを紹介してみました。 もちろん、React でも同じような機能を実現できますが、そのやり方は結構違っていることがお分かり頂けたかと思います。
一見同じような機能を提供しているように見えるフロントエンドのフレームワークですが、実際に使ってみると、コーディングのやり方はかなり変わってきます。 React の場合は、全てプログラムで処理できるのは大きな魅力だと思いますが、その分プログラムの部分(Typescript/JavaScript)の少し深いレベルでの理解と知識が必要になります。
この連載を読んで Vue と React の違いを細かいレベルで感じることで、どちらのフレームワークを選んで利用していくか考えて頂ければと思います。