Open21

React学習記録🔰

つるもとつるもと

概要

React初学者の学習記録📝
自分用の振り返りメモとして。

現在の自分

HTML
CSS、SCSS
JavaScript少し(読めてデバッグできるけどAIが無いと書けない)
Vue.js少し(v-bindのあたり)

環境

React + Astro + SCSS
Stackblitz

つるもとつるもと

「Hello World!」を表示させる

つるもとつるもと

Astroを書くときに注意することメモ

  • <(コンポーネント名) />を使うためにimportが必要。

    • <(コンポーネント名) />・・・別ファイルにまとめた部品。Vue.jsと同じ。
      Foo.jsxファイルならコンポーネント名は<Foo />となる。
    • import・・・Astroファイルの先頭で宣言。---で前後を囲む必要がある。
      ---
      import MyFirstReact from '../components/MyFirstReact';
      ---
      <!-- ↑これ -->
      <html lang="ja">
      <head>
      ⋮
      以下略
      
  • <meta name="generator" content={Astro.generator} /><meta>タグに入れる

  • コンポーネントの<>単独で書く場合は最後に/が必要。間にテキストなどが挟まる場合は/の代わりに閉じタグを使う。(<MyComp>foo</MyComp>

  • 動的な要素が含まれるコンポーネントにはディレクティブ[1]を付ける。

    <body>
      <main>
      	<Foo client:load />
      </main>
    </body>
    
脚注
  1. Astroで、React等のフレームワークのコンポーネントをクライアントで動かすにはclient:load(JSで動かす)と明示的に付ける必要がある。付けていないところではサーバーサイドで描画されるため、WEBサイトの軽快な動作につながる。client:には他にも種類あり。 ↩︎

つるもとつるもと

「Hello World!」の表示

できた

astro
---
import MyFirstReact from '../components/MyFirstReact';
---

<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
  </head>
  <body>
    <main>
      <MyFirstReact client:load />
    </main>
  </body>
</html>
jsx
import './MyFirstReact.css';

export default function MyFirstReact() {
  return <h1 className="title">Hello World!</h1>;
}

つるもとつるもと

ボタン押下でテキストの挿入

「build from...」と書いてあるボタンを押すと、「Astro + React」のテキストが挿入される

実装のポイント(詳しい説明は次のスレッドで)

index.jsx

  1. Reactでは直接DOMを操作する書き方(document.queryselectorなど)はせず、「状態(state)やpropsに応じて画面が変わるように書く」。
  2. returnの中身が複数行になる場合は、()で囲む。
  3. HTMLでいうclassclassNameと書く。
  4. 状態変化useStateの起こる記述が含まれているなら、import { useState } from 'react';を記述する。

index.astro

  1. ↑で作ったjsxが動くよう、コンポーネント名にclient:loadを記述する。
index.astro
---
import MyFirstReact from '../components/MyFirstReact';
---

<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
  </head>
  <body>
    <main>
      <MyFirstReact client:load />
    </main>
  </body>
</html>
index.jsx
import { useState } from 'react';
import './MyFirstReact.css';

export default function MyFirstReact() {
  const [message, setMessage] = useState('');

  return (
    <div>
      <hgroup>
        <h1 className="title">Hello World!</h1>
        <p className="subtitle">{message}</p>
      </hgroup>
      <input
        type="button"
        value="build from..."
        onClick={() => setMessage('Astro + React')}
      />
    </div>
  );
}

つるもとつるもと

<div>の代わりに<>を使用する

return (で囲むのは単一のコンポーネントでなくてはならないため、<hgroup><input><div>で囲ったけれど、これでは余計な<div>が増えてしまう。

この解決のため、<div>の代わりに 「フラグメント」とよばれる空タグ<>で囲むことができる。

使い方は単純に<div> </div>を置き換えるだけ。

jsx
- <div>
+ <>
  <hgroup>
    <h1 className="title">Hello World!</h1>
    <p className="subtitle">{message}</p>
  </hgroup>
  <input
    type="button"
    value="build from..."
    onClick={() => setMessage('Astro + React')}
  />
- </div>
+ </>
つるもとつるもと

💡Reactのキモ! 状態管理(state)について

これまでのJavaScriptとReactの違い

Reactでは直接DOMを操作する書き方(document.queryselectorなど)はせず、状態(state)やpropsに応じて画面が変わるように書く。

解説

useStateとは
次のように、「状態変数(state)」を作成。

jsx
const [message, setMessage] = useState('');
名前 役割
message 表示するメッセージの現在の値(状態)
setMessage message値を変えるための関数
useState('') 最初に何も表示しないのであれば空にする

②どこで画面に反映される?

jsx
<p className="subtitle">{message}</p>

ここでmessagesetMessageされた結果を表示している。
初期状態はuseState('')で値が空なので、<p>には何も表示されていない。

③トリガーの発火

jsx
<input
  type="button"
  value="build from..."
  onClick={() => setMessage('Astro + React')}
/>
  • ボタンクリックでsetMessage関数が発火し、
  • useState('')の値にAstro + Reactの文字列が入る。
  • React は「ここの値(=状態)が変わったから、表示も更新しなきゃ!」と判断し、
  • <p className="subtitle">{message}</p>{message}部分を表示(=再描画)してくれる。
  • 結果、「ボタンクリックで<p>内にAstro + Reactと表示」される。

💡この、状態が変わると自動で再描画するのがReactのすごいところ。

普通のJavaScript(DOM操作)とどう違うの?

今回のコードは、通常のJavaScriptだと次のように書く。

<!-- js -->
<script>
  const button = document.querySelector('.button');
  button.addEventListener("click", () => {
    document.querySelector('.subtitle').textContent = 'Astro + React';
  });
</script>
<!-- html -->
<body>
  <p class="subtitle"></p>
  <input type="button" value="build from..." class="button">
</body>

しかしこの場合だと、

  • どこを(document.querySelector('.subtitle')
  • どう(.textContent = 'Astro + React)
  • どのタイミングで(button.addEventListener("click", () =>
    変更するのか、を全部自分で管理(指定)する必要があり、コンポーネントが増えると処理が複雑でバグが出やすくなる

Reactがやっていることまとめ

  1. 画面を初期レンダリング(最初の表示)
  2. 状態(state)が変わったときに、
  3. Reactが仮想DOMで差分を比較して、
  4. 必要な部分だけを書き換え
    この仕組みによって、無駄なDOM操作のない、効率的で安定した画面更新ができる。

総まとめ(状態管理のメリット)

  • useStateを使うことで、変化に強いUIが作れる
  • 状態がかわるとUIが自動で再描画される
  • DOM操作を書かなくてもReactが裏でやってくれる
  • コンポーネントが増えても一貫した方法で状態を管理できる
つるもとつるもと

補足

先に挙げたコードだとメリットがわかりづらいのでさらに補足。

<p>がふたつの場合...

バニラJavaScript
<p class="subtitle"></p>
<p class="subtitle-02"></p>

<script>
  document.querySelector('.subtitle').textContent = 'Astro + React';
  document.querySelector('.subtitle-02').textContent = 'Astro + React'; //要素が増えると追記が必要
</script>

<!-- またはforEachでループ処理 -->
<script>
  const classes = ['subtitle', 'subtitle-02'];
  classes.forEach(className => {
    const el = document.querySelector(`.${className}`);
    if (el) el.textContent = 'Astro + React';
  });
</script>

.subtitle.subtitle-02は別の要素なので、それぞれをDOM操作で取得する必要がある。
そのため、.subtitle-02用の記述が増える。
forEachでループ処理をするとまとめられるが、今回の場合はがっつり記述が増えるし、今後さらに.subtitle-03 .subtitle-04 .subtitle-05と増えていくなら配列にこれらを加える必要がある。

React
export default function MyFirstReact() {
  const [message, setMessage] = useState('');

  return (
    <div>
      <p>{message}</p> {/* {message}が複数使える */}
      <p>{message}</p> {/* 状態管理時にclassを使用しないので、cssでのスタイリング不要ならclassNameも不要 */}
      <input
        type="button"
        value="build from..."
        onClick={() => setMessage('Astro + React')}
      />
    </div>
  );
}

Reactの場合は「何がどうなる」という状態管理の宣言をしておくことで、状態の変化を起こしたい場所それぞれに同じ宣言の変数を入れるだけで同じように表示される。
さらに、状態管理にはclassNameを使用しないため、cssでスタイリングをするのでなければclassNameも不要。

つるもとつるもと

【children】親コンポーネント側でテキストを追加して使いまわす

やりたいこと

<h2>の見出しのような、cssやコードは共通でテキストだけ違うコンポーネントを都度作成するのではなく、親コンポーネントのところにテキストを入れて使ったりできたらなあ。Vue.jsはできたなあ。

方法:childerenを使う

もちろんReactでもできるやで。

jsx
export default function MySecondReact({ children }) {  
    return <h2>{children}</h2>;
}
astro
<MySecondReact>第一章</MySecondReact>
<MySecondReact>第二章</MySecondReact>
htmlへビルドされた結果
<h2>第一章</h2>
<h2>第二章</h2>
つるもとつるもと

【props】親コンポーネント側でいろいろ追加して使いまわす

やりたいこと

childrenでテキストが渡せるのはわかったけど、テキストの他にも渡せたらなあ。
この時に「build from...」って書いてあるボタンを押したら「Astro + React」って表示されるボタンを作ったけど、この部分のテキスト、両方とも親コンポーネントから指定出来たらボタンの使いまわしが効くんだけどなア。

方法:propsを使う

jsx
import { useState } from "react";

{/* buttonLabel と messageText が 「props」 */}
export default function MyThirdReact ({ buttonLabel, messageText}) {
const [text, setMessage] = useState('');

  return (
    <>
      <p>{text}</p>
      <input
      type="button"
      value={buttonLabel}
      onClick={() => setMessage(messageText)}
      />
    </>
  ); 
}
astro
<MyThirdReact buttonLabel="クリック" messageText="どうも" client:load />
<MyThirdReact buttonLabel="押す" messageText="こんちわ" client:load />
htmlへビルドされた結果
<p>どうも</p>
<input type="button" value="クリック">
<p>こんちわ</p>
<input type="button" value="押す">
<!-- ※ボタンを押す前は<p>の中身は表示されていません -->

💡つまり、propsは子コンポーネントに空けておく“穴”
親コンポーネント側で埋めてやる必要がある。

つるもとつるもと

補足:propsについて深堀り

props周りの書き方を分割代入[1]せずに書くと以下のようになる。(コード一部省略あり)

jsx
export default function MyThirdReact (props) {
    <>
      <input
      type="button"
      value={props.buttonLabel}
      onClick={() => setMessage(props.messageText)}
      />
    </>

つまりpropsプロパティ(値)をひとまとめにした袋のようなオブジェクトであり、buttonLabelmessageTextといったプロパティがまとめて梱包されて、それが関数(ここではMyThirdReact)の引数として指定される。
その後JSXタグとしてレンダリング、つまり親コンポーネントを記述する際に、props袋に入っているプロパティを属性名として値を入れることで、その値がMyThirdReact関数の引数として子コンポーネントに渡される。

html
<MyThirdReact buttonLabel="クリック" messageText="どうも" client:load />
脚注
  1. 「分割代入」は最初に挙げた書き方。ふつうはこちらで書く。引数にコンポーネント内で使用しているpropsが羅列されるためコードの風通しが良い。 ↩︎

つるもとつるもと

補足の補足

  • 関数の引数として渡されるならpropsという名前じゃなくても動くのでは?
    • 動く。でも混乱の元になるのでpropsを使うこと。
  • プロパティ名って決まっているの?
    • 任意で名付け可能。ただし、使えなかったり避けるべき単語があるので注意。
      1. ”Reactが内部的に使うprops”の名前を使用
        keyrefchildrenなど
      2. 予約語
        JavaScriptの予約語の使用はNG(使おうと思えば使える)
      3. HTMLの属性名との名前被り
        typevalueidなど、HTMLでも出てくる属性は読み間違えの元なので使用しない。
astro
<!--- この「type」と「value」は子コンポーネントで指定したプロパティ名 --->
<MyInput type="text" value="hello" />

<!--- この「type」と「value」はHTMLのinputタグの属性名 --->
<input type="text" value="hello" />
つるもとつるもと

【props】中間コンポーネントのpropsを通過させる

やりたいこと

jsxタグ記述 - コンポーネント1 - コンポーネント2
このような階層のコンポーネントで、コンポーネント2のpropsの”穴”をjsxタグのところで埋めたい。

方法:propsを”袋”ごと渡す

まずはコンポーネント2。
titledescriptionというpropsがある。

コンポーネント2
export default function MetaTags({ title, description }) {  
  return (
    <>
      <title>{title}</title>
      <meta name="description" content={description} />
    </>
  );
}

そしてコンポーネント1。
コンポーネント2のpropsの”穴”を埋めるのだが、次の通り引数にpropsを取り、複数の”穴”をまとめて {...props}の形で記述する。

コンポーネント1
import MetaTags from './../MetaTags' {/* コンポーネント2をインポート */}

- export default function HeadContent() {
+ export default function HeadContent(props) {
  return (
    <>
-      <MetaTags title="タイトル" description="ディスクリプション" />
+      <MetaTags {...props} />
    </>
  );
}

あとはjsxタグを書くときに”穴”を埋めればOK。

astro
<head>
-  <HeadContent />
+  <HeadContent title="タイトル" description="ディスクリプション" />
</head>
つるもとつるもと

【module】SCSSでスタイリングする

やりたいこと

コンポーネントにcssを適用させる。できればscssを使いたい。

解決:moduleを使用してスコープ化させつつ書く

①moduleを使わない場合のメリットデメリット

スコープさせずにimportでscssを読み込めば、それだけでスタイリングされる。

jsx
import './MyFourthReact.scss';

export default function MyFourthReact() {
  return <p>こんにちは!</p>;
}
scss
p {
  color: red;
}

メリット

  • 記述が簡潔。

デメリット

  • ph2などのタグセレクタに直接指定すると、コンポーネントの外にも影響が出てしまう。

②moduleを使う場合のメリットデメリット

jsx
import styles from './MyFourthReact.module.scss';

export default function MyFourthReact() {
  return <p className={styles.message}>こんにちは!</p>;
}

scss
.message {
  color: red;
}

メリット

  • そのコンポーネント内のみにスタイルが適用される(スコープ化)。
  • クラス名の命名に迷わなくてよい[1]

デメリット

  • class名を付けることが必要(pなどタグセレクタだとやっぱり全体に影響が出てしまう)。
脚注
  1. class名にユニークな値が自動で足されるので、簡単なクラス名であっても他と絶対に被らない仕組み。 ↩︎

つるもとつるもと

パターン別チートシート

jsx
{/* ノーマル */}
<p className={styles.message}>こんにちは!</p>;

{/* ハイフンが含まれる場合 */}
<p className={styles['message-text']}>こんにちは!</p>;

{/* 複数クラス */}
<p className={`${styles.message} ${styles.large}`}>こんにちは!</p>;

{/* 複数+ハイフン含 */}
<p className={`${styles.message} ${styles['message-text']}`}>こんにちは!</p>;
jsx
{/* 状態変化(true/false)で動的に出し分ける */}
export default function MyFourthReact() {
  const [state, setState] = useState(true)
  return (
    {/* 状態:true で noon が適用 */}
    <p className={state ? styles.noon : styles.night}>
      {state ? "こんにちは!" : "こんばんは!"}
    </p>
  );
}

--------------------------------------------------------------------
{/* 三項演算子おさらい */}
条件式 ? Trueの処理 : Falseの処理

{/* 元はif文 */}
if (条件式) {
  Trueの処理
} else {
  Falseの処理
}
つるもとつるもと

<meta> <link>などの汎用タグをまとめる

<head>の中身をまとめてスッキリ

SNSカード用の<meta>やらGoogleフォントやらの<link>でごちゃごちゃになる<head>内。
それぞれでまとめればスッキリ。

フォルダ構成

src/
└── components/
    └── head/
        ├── HeadContent/
        │   └── index.jsx       ← <title><meta><link>全まとめコンポーネント
        ├── MetaTags/
        │   └── index.jsx       ← <meta>用
        └── link/
            ├── LinkTags/
            │   └── index.jsx   ← <link>用
            └── Fonts/
                └── index.jsx   ← Google Fontsなどの <link> ※フォントだけ記述が長いので別


<meta><link>をそれぞれコンポーネント化

MetaTags/index.jsx
export default function MetaTags({ title, description }) {  
  return (
    <>
      <title>{title}</title>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <meta name="description" content={description} />
    </>
  );
}
LinkTags/index.jsx
export default function LinkTags() {  
  return (
    <>
      <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    </>
  );
}


これらをHeadContentにまとめる

HeadContent/index.jsx
import MetaTags from './../MetaTags'
import LinkTags from './../link/LinkTags'

export default function HeadContent() {  
  return (
    <>
      <MetaTags title="タイトル" description="ディスクリプション" />
      <LinkTags />
    </>
  );
}


index.astro内に記述

<html lang="ja">
  <head>
    <meta name="generator" content={Astro.generator} />
    <HeadContent /> 👈
  </head>
  <body>
つるもとつるもと

🆙ライトモード/ダークモードの切り替えとそれ用のボタンを作る

できそうな気がしてきたのでチャレンジ

やりたいこと:ライトモード/ダークモード切り替え

  • ボタンで動作
  • 押すたびにtrue/falseが切り替わり、
  • それによりライト用cssとダーク用cssを切り替え
    • bodybackground-colorcolorをライトとダークで変更
つるもとつるもと

チェックボックスがチェックされたら文字色が変わる

ライト/ダークの前の基本機能として、表題のものを作っていく。
いまの知識で書いてみたものがコチラ。

jsx
import { useState } from "react";
import styles from './SwitchLightDark.module.scss';

export default function SwitchLightDark () {
  const [check_state, setState] = useState(true);
  return (
    <>
      <input type="checkbox" checked={setState()} />
      <p className={check_state ? styles.aaa : styles.bbb}>わーい</p>
    </>
  );
}

悲しいけど当然動かなかったので、ChatGPTくんに相談して修正。

jsx
import { useState } from "react";
import styles from './SwitchLightDark.module.scss';

export default function SwitchLightDark () {
  const [check_state, setState] = useState(true);

+  const handleChange = () => {
+    setState(prev => !prev);
+  };

  return (
    <>
-      <input type="checkbox" checked={setState()} />
+      <input type="checkbox" checked={check_state} onChange={handleChange} />
      <p className={check_state ? styles.aaa : styles.bbb}>わーい</p>
    </>
  );
}

解説

useStateについておさらい

  • コンポーネント内で「状態」を扱う際にはuseStateを使って状態変数(state)とその更新関数setStateを定義する。
  • check_stateuseStateを使って作った状態変数。
    今回の初期値はuseStateの引数にあるtrue

handleChangeとは

  • チェックボックスの状態が変わったときに呼ばれる関数。自作なので命名可能。
  • prevは変更前の状態(直前のstate)で、setState関数でその状態を切り替えている。
    setState(prev => !prev):true ⇄ false。

onChangeとは

  • イベントハンドラ[1]

  • onChangeチェック状態が変わったときに実行したい処理(関数)を指定する属性

  • つまり、onChange={handleChange} = チェック状態が変わるたびにhandleChange関数を実行

脚注
  1. 特定のイベントが発生した際に自動的に実行される処理。Reactで定義済みの属性値(イベント) ↩︎

つるもとつるもと

チェックボックスがチェックされたら背景色が変わる

今度は一歩踏み込んで、bodyの背景色が変わるようにする。
同じコンポーネント内にあるタグではなく、外にあるbodyにスタイルを適用させるのが今回のポイント。まずはインラインでスタイルを当てる。

ネットで調べつつ、今の知識で書いたものがコチラ

jsx
import { useState } from "react";
import styles from './SwitchLightDark.module.scss';

export default function SwitchLightDark() {
  const [check_state, setState] = useState(false);
  const handleChange = () => {
    setState(prev => !prev);
    if (check_state === true) {
      body.setAttribute("style", "background-color: #444;")
    }else {
      body.setAttribute("style", "background-color: #fff;")
    }
  };

  return (
    <>
      <input type="checkbox" checked={check_state} onChange={handleChange} />
    </>
  );
}

やっぱり当然動かなかったので、ChatGPTくんに相談して修正。

jsx
- import { useState } from "react";
+ import { useState, useEffect } from "react";
import styles from './SwitchLightDark.module.scss';

export default function SwitchLightDark() {
  const [check_state, setState] = useState(false);

+  {/*状態が変わるたびにbodyの背景色を変える*/}
+  useEffect(() => {
+    document.body.style.backgroundColor = check_state ? "#444" : "#fff";
+  }, [check_state]);

  const handleChange = () => {
    setState(prev => !prev);
-    if (check_state === true) {
-      body.setAttribute("style", "background-color: #444;")
-    }else {
-      body.setAttribute("style", "background-color: #fff;")
    }
  };

  return (
    <>
      <input type="checkbox" checked={check_state} onChange={handleChange} />
    </>
  );
}
↑に加えてtransitionも追加
useEffect(() => {
  document.body.style.backgroundColor = check_state ? "#444" : "#fff";
+ document.body.style.transition = [check_state ? "background .3s" : "background .3s";
}, [check_state]);

解説

useEffectとは

  • Reactの持つ機能(React Hooks)のひとつ。useEffectはコンポーネントの外にある要素に対して作用する。
  • <body>styleを適用させる――つまり、DOMを直接操作するために使用する。
useEffect
useEffect(() => {
  // ここに副作用の処理を書く
}, [依存する変数]);

「副作用(side effect)」とは

  • 「レンダリング以外の処理」のことで、今回のDOMの直接操作もこれにあたる。
  • Reactは基本的に「状態を受け取ってUIを描画する」ライブラリだが、それ以外の処理(=副作用)は useEffect に分離して書くのが原則。

副作用の例:

  • DOMの直接操作(今回のようなスタイル変更など)
  • API通信(fetchなど)
  • タイマー処理(setTimeoutやsetInterval)
  • イベントリスナーの登録・解除
つるもとつるもと

変数や関数の命名について

チェックボックスがチェックされたら背景色が変わるで書いたコードについて、可読性が悪かったのでリファクタリングした。

SwitchLightDark
import { useState, useEffect } from "react";
import styles from './SwitchLightDark.module.scss';

function SwitchLightDark() {
- const [check_state, setState] = useState(false);
+ const [colorMode, setColor] = useState(false);

  useEffect(() => {
    document.body.style.backgroundColor = colorMode ? "#444" : "#f7f7f7";
    document.body.style.transition = "background .3s";
  }, [colorMode]);

- const handleChange = () => {
+ const toggleColor = () => {
    setColor(prev => !prev);
  };

  return (
    <>
      <input type="checkbox" checked={colorMode} onChange={toggleColor} />
    </>
  );
}

export default SwitchLightDark;
before after
状態 check_state colorMode
状態の切り替え setState setColor
切り替えの発火 handleChange toggleColor

check_statehandleChangeしたらsetStateされる」

colorModetoggleColorしたらsetColorされる」
具体性が増して良くなった!

プログラミング初学者だとどこを独自の名前にしていいかもわからんのんよ。
変数、関数が大丈夫ってことね、、、
あとは変数iとか、”おなじみのやつ”の名前を変えちゃうと自分も他人もわからなくなるから、汎用的なのも変えない。よし。

つるもとつるもと

Astoroメモ
https://www.remedia.co.jp/blog/posts/2023-04-astro-blog/

サブディレクトリにページをbuildしたい
このブログサイトではドメイン直下の/blog/ ディレクトリにページを生成したいので、以下のようにbaseを設定することで解決できました。同じようなことを考えている方は多いようで、Web上に情報が多かったので助かりました。

// astro.config.mjs
const subfolder = '/blog/'
export default defineConfig({
 base: subfolder,
 outDir: `./dist${subfolder}`,
});

これで生成後のファイルがhttps://www.remedia.co.jp/blog/以下に配置されるようになります。