【2日目】React初学者が1週間でどこまで出来るか挑戦(ひたすらドキュメント編)

はじめに

React学習1週間チャレンジ2日目、日曜日をReactに捧げました。ひたすらドキュメントを読み進めてました。

前回の振り返り:

教材:

今日の学び:

  • コンポーネントのImportとExport
  • JSXのマークアップ
  • JSXの構文ルール
  • コンポーネントにpropsを渡す
  • 条件付きレンダリング
  • リストをレンダリングするときの注意
  • コンポーネントをpureに保つ
  • イベントハンドリングの基礎
  • Stateの基礎の基礎

チャプター「Describing the UI」が完了しました!

Importing and Exporting Components

ドキュメント参考箇所:

root component fileとは

ドキュメントを参考に、App.jsというファイルにUser関数を定義して、それをUserList関数内で呼び出してみた。このApp.jsをレンダリングする際、このファイルはroot component fileとして扱われる。(Reactの公式ドキュメントには、ブラウザ上でコードを編集して結果を確認できるサンドボックスが提供されているためそれを利用している。)

// App.js
function User() {
  return <p>Name: Ryosuke</p>
}

export default function UserList() {
  return (
    <section>
      <h1>User</h1>
      <User />
      <User />
      <User />
    </section>
  );
}

componentのexportとimport

App.jsのように一つのファイル内でcomponentを定義し使用する場合、再利用性を高めるためにファイルを分けたくなる。その場合の手順は:

  1. componentを入れたいJSファイルを新規作成
  2. function componentをexportする
  3. componentを使いたいファイルでimportする

まず、JavaScriptのexportについて知識がなかったので、MDNで基礎を確認した。

exportには2種類ある。各モジュールの中で持てるexportは、

  • named export : 複数
  • default export : 1つのみ
// named export
export { myFunction2, myVariable2 };

// default export
export default myFunction;
export default function () { /* … */ }

前回のBreezeパッケージで生成されたresources/js下の.jsxファイルを一通り確認したところ、全てdefault exportが使われていた。

Reactドキュメントに戻り、サンプルコードを参考にApp.jsに定義した関数をUserList.jsに切り出してimportする形に変更。

// UserList.js
function User() {
  return <p>Name: Ryosuke</p>;
}

export default function UserList() {
  return (
    <section>
      <h1>User</h1>
      <User />
      <User />
      <User />
    </section>
  );
}
// App.js
import UserList from "./UserList";

export default function App()
{
  return (
    <UserList />
  );
}

同一ファイルから複数のcomponentをexportとimport

UserListではなくUserだけを表示したい場合はどうしたらいいか?という観点でコードを書き換えていく。

先に学んだnamed exportを使う。Userをexportとする。

// UserList.js
export function User() {
  return <p>Name: Ryosuke</p>;
}

export default function UserList() {
  // 省略
}

User{}で囲ってimportする。そして<User />をreturnする。

// App.js
import { User } from "./UserList";

export default function App()
{
  return <User />
}

使用上のTipsも書かれていた。defaultとnamedで混乱を招かないように以下のようなスタイルをとるのがよさそう。

  • defaultかnamedかどちらか一つに統一する
  • 単一ファイル内ではdefaultとnamedを混在させない

Writing Markup with JSX

ドキュメント参考箇所:

ポイント:

  • JSXはJavaScriptの拡張構文で、JavaScriptファイル内でHTML風にマークアップできる
  • JSXは拡張構文で、ReactはJavaScriptのライブラリである
  • JSXはHTMLに似ているが、より厳格なルールがある

The Rules of JSX

ルート要素は一つだけ

先のUserpタグを増やしてみた。同階層に複数のタグがあるとエラーになるため、親要素に<div>を使って単一のルート要素にした。

export function User() {
  return (
    <div>
      <p>Name: Ryosuke</p>
      <p>Learn: React</p>
      <p>Like: Coffee</p>
    </div>
    );
}

JSX構文に対応するために<div>を使ったが、本当はHTMLの構造に<div>タグを増やしたくない、という時はFragmentを使う。

Fragmentは、<> ... </>のように空タグで要素を囲う。

タグは閉じる

HTMLでは閉じタグがなくても記述できるタグであっても、

<!-- HTML -->
<input type="text" value="test">
<ul>
  <li>List1
  <li>List2
</ul>

JSXでは必ずタグを閉じること

// JSX
<input type="text" value="test" />
<ul>
  <li>List1</li>
  <li>List2</li>
</ul>

ほとんどcamelCase

  • JSX で記述された属性がJavaScript オブジェクトのキーになる
  • 属性はcamelCaseにする
  • classなどの予約語は使えない(class属性は代わりにclassNameにする)

JavaScript in JSX with Curly Braces

ドキュメント参考箇所:

文字列を渡す: constで変数を定義して、{ }内で使用できる。

export function User() {
  const name = "Ryosuke";
  return (
      <input type="text" value={name} />
    );
}

{ }内では、JavaScriptのメソッドを書くこともできる。例えば、

const today = new Date();

function formatDate(date) {
  const year  = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day   = String(date.getDate()).padStart(2, '0');

  return `${year}-${month}-${day}`;
}

return <h1>Today is {formatDate(today)}</h1>; // Today is 2023-10-08

JavaScriptのオブジェクトとしての使い方

export function User() {
  const UserInfo = {
    name: "Ryosuke",
    like: "coffee",
  };

  return (
    <>
      <p>Name: {UserInfo.name}</p>
      <p>Like: {UserInfo.like}</p>
    </>
  );
}

Passing Props to a Component

ドキュメント参考箇所:

ポイント:

  • props = 小道具
  • 親componentはpropsを使って子componentに情報を渡す
  • propsはオブジェクト、配列、関数などあらゆるJavaScriptを渡せる

componentとpropsを学んで、昨日見たInertia::renderメソッドの引数の意味が分かってきた。

// Inertia\Inertia::render
public static function render($component, $props = []) { }

propsの渡し方

  1. 子component(User)にpropsを渡す(ここではuserInfoオブジェクト)
  2. 子component(User)内でpropsを読み込む
function User({userInfo}) {
  return (
    <>
      <p>Name: {userInfo.name}</p>
      <p>Like: {userInfo.like}</p>
    </>
  );
}

export default function UserList() {
  return (
    <section>
      <h1>UserList</h1>
      <User 
        userInfo={{name: 'Ryosuke', like: 'coffee'}}
      />
    </section>
  );
}

ネストする方法

// App.js
import User from './User.js';

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

export default function UserList() {
  return (
    <Card>
      <User 
        userInfo={{
          name: 'Ryosuke', like: 'coffee'
        }}
      />
    </Card>
  );
}
// User.js

export default function User({ userInfo }) {
  return (
    <>
      <p>Name: {userInfo.name}</p>
      <p>Like: {userInfo.like}</p>
    </>
  );
}

以下のパートは後から出てくるstateに関わるようで軽く読むだけにした。

条件付きレンダリング

ドキュメント参照箇所:

JavaScriptの構文と同じようにif&&? :演算子が使える。

以下はTodoリストの例。isDoneの値で条件分岐させている。ただしDRYのため次で書き換える。

// App.js
function Task({ task, isDone }) {
  if (isDone) {
    return <li className="task">{task} ✅</li>;
  }
  return <li className="task">{task}</li>;
}

export default function ToDoList() {
  return (
    <section>
      <h1>Today's Tasks</h1>
      <ul>
        <Task 
          isDone={false} 
          task="Study React" 
        />
        <Task 
          isDone={false} 
          task="Write an article" 
        />
        <Task 
          isDone={true} 
          task="Buy some coffee" 
        />
      </ul>
    </section>
  );
}

三項演算子(? :)によってDRYを解消。

function Task({ task, isDone }) {
    return (
      <li className="task">
        { isDone ? task + ' ✅' : task }
      </li>
    );
}

条件式の中でさらにHTMLタグを使う例。JSX内のJavaScript式は{}で、複数行のJSXは()で囲む。

function Task({ task, isDone }) {
  return (
    <li className="task">
      { isDone ? (<del>{task + ' ✅'}</del>) : task }
    </li>
  );
}

論理積(&&)演算子を使う例。左から全てtrueで通過した場合は最後のオペランドが返される。falseがあった時点で評価されず終了。文法はMDNを参照: Logical AND (&&) - JavaScript | MDN

function Task({ task, isDone }) {
  return (
    <li className="task">
      {task} { isDone && '✅' }
    </li>
  );
}

Rendering Lists

ドキュメント参考箇所:

例えば「従業員 : 部署」というリストを表示したいとする。

<ul>
  <li>青木: 営業部</li>
  <li>井上: 商品部</li>
  <li>上田: 総務部</li>
  <li>江原: 人事部</li>
  <li>小田: 営業部</li>
</ul>

JavaScriptのmapを使って

const employees = [
  '青木 : 営業部',
  '井上 : 商品部',
  '上田 : 総務部',
  '江原 : 人事部',
  '小田 : 営業部',
];

export default function List() {
  const listItems = employees.map(employee =>
    <li>{employee}</li>
  );
  return <ul>{listItems}</ul>;
}

mapメソッドによってlistItemsは以下のような配列になるはず。それを<ul>タグの中でレンダリングさせている。

// listItemsの中身
[
  <li>青木 : 営業部</li>,
  <li>井上 : 商品部</li>,
  <li>上田 : 総務部</li>,
  <li>江原 : 人事部</li>,
  <li>小田 : 営業部</li>
]

表示はされるがコンソールに次のようなエラーが出る。

Warning: Each child in a list should have a unique "key" prop.

これは、配列の各要素にkeyが必要だから(ユニークキー)

<li key={employee.id}>...</li>

次のように各リストにユニークキーを持たせるように書き換える。

const employees = [
  { id: 1, name: '青木', department: '営業部' },
  { id: 2, name: '井上', department: '商品部' },
  { id: 3, name: '上田', department: '総務部' },
  { id: 4, name: '江原', department: '人事部' },
  { id: 5, name: '小田', department: '営業部' }
];

export default function List() {
  const listItems = employees.map(employee =>
    <li key={employee.id}>
      {employee.name} : {employee.department}
    </li>
  );
  return <ul>{listItems}</ul>;
}

filterメソッドを使って「営業部」だけに絞り込んだ例を書いてみる。

const employees = [
  { id: 1, name: '青木', department: '営業部' },
  { id: 2, name: '井上', department: '商品部' },
  { id: 3, name: '上田', department: '総務部' },
  { id: 4, name: '江原', department: '人事部' },
  { id: 5, name: '小田', department: '営業部' }
];

export default function List() {
  const sales = employees.filter(employee => 
    employee.department === '営業部'
  );
  
  const listItems = sales.map(employee => 
    <li key={employee.id}>
      {employee.name} : {employee.department}
    </li>
  );
  
  return <ul>{listItems}</ul>;
}

Keeping Components Pure

ドキュメント参考箇所:

Describing the UIという最初のチャプターの最後のパート

ポイント:

  • componentは常に同じ結果を返すべき
  • componentを呼び出す度に結果が変わるような実装をすべきでない

例えば、以下のコードはコンポーネント外部で宣言した変数を内部で使っているため、コンポーネントを呼び出す度に異なるJSXを生成してしまう。

// 悪い例
let itemId = 0;

function Item() {
  itemId++;
  return <div>Item #{itemId}</div>;
}

export default function List() {
  return (
    <>
      <Item />
      <Item />
      <Item />
    </>
  );
}

// レンダリング結果は以下ようになる
// Item #2
// Item #4
// Item #6

解決方法として次のようにpropを使うこと。

function Item({ itemId }) {
  return <div>Item #{itemId}</div>;
}

export default function List() {
  return (
    <>
      <Item itemId={1} />
      <Item itemId={2} />
      <Item itemId={3} />
    </>
  );
}

また、コンポーネントの内部またはpropsを通してデータを渡すと、予期せぬバグや副作用を避けるこができる。

function Item({ itemId }) {
  return <li>Item #{itemId}</li>;
}

export default function List() {
    let items = [];
    for (let i = 1; i <= 5; i++) {
      items.push(<Item key={i} itemId={i} />);
    }
    return items;
}

Responding to Events

チャプター: LEARN REACT > ADDING INTERACTIVITY

ドキュメント参照箇所:

Event handler functionsは通常、

  • コンポーネントの中に定義する
  • handleから始まるイベントの名を命名する
    • 例えば、onClick={handleClick}, onMouseEnter={handleMouseEnter}など

注意点:

  • 正)<button onClick={handleClick}>は、ボタンクリック時に関数が呼び出される
  • 誤)<button onClick={handleClick()}>は、レンダリング時に即時呼び出される

Event propagation(イベントの伝播):

  • イベントは子から親要素に向かって伝播していく
  • 伝播を止めるにはイベントオブジェクト(e)に対して、e.stopPropagation()を呼ぶ
  • formの送信イベントを止めるには、e.preventDefault()

State: A Component's Memory

ドキュメント参照箇所: State: A Component's Memory – React

ポイント:

  • ローカル変数はレンダリング間で保持されない
  • ローカル変数が更新されても新しいデータで再度レンダリングされるわけではない

解決するには:

  • useStateを使う
  • import { useState } from 'react';でインポートする

Hookとは:

  • useStateのように、useから始まる関数をHookと呼ぶ

連載記事

  1. 【1日目】React初学者が1週間でどこまで出来るか挑戦
  2. 【2日目】React初学者が1週間でどこまで出来るか挑戦(ひたすらドキュメント編)★今回