1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

手を動かして覚えるReact入門

Last updated at Posted at 2024-12-03

ジョブカン事業部のアドベントカレンダー4日目です。

初めてReactを触るインターンの方にReactの独特なhooksなどについて説明する機会があったので、その内容を記事にしてみました。Reactを体験したことのない人でも、なんとなくReactが書けるようになることが目標です。

正確性や詳しいコードの解説よりも、実際にコードを触って体験することに重きを置いています。

対象とする読者

  • HTMLやCSSは理解している
  • JavaScriptやTypescriptに関する基本的な知識はある
  • Reactは全く触ったことがないか、コードをサラッとみたことがある程度

環境構築

npm create vite@latestでいい感じになります。
frameworkはReactを選択してください。

ここでは、Typescriptで説明を進めていきます。

npm create vite@latest

Need to install the following packages:
  create-vite@5.5.5
Ok to proceed? (y) y
✔ Project name: … vite-project
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

Scaffolding project in /vite-project...

Done. Now run:

  cd vite-project
  npm install
  npm run dev

そのまま指示に従って、cd vite-project npm install npm run devをそれぞれ実行してください。

  VITE v5.4.11  ready in 485 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

起動したらLocal:に書かれているURLにChromeなどでアクセスしてください。

ポートを3000などに固定する場合
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

// https://vite.dev/config/
export default defineConfig({
+  server: {
+    port: 3000,
+  },
  plugins: [react()],
})

アクセスすると、count is Xと書かれたボタンがあると思います。これをクリックすると、カウントを増やすことができます。

コードの解説

src/App.tsxを編集することで表示内容をいじれます。この記事では、特にReactの特殊な部分について説明します。

App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

大事なのはconst [count, setCount] = useState(0)<button onClick={() => setCount((count) => count + 1)}>count is {count}の部分です。

count is {count}{}って何?

Reactでは、JSX記法という記法が使われます。簡単に言えば、JavaScriptの中でHTMLみたいな書き方ができます。

{}は、HTMLみたいな記法の中でJavaScriptを呼び出したいときに使います。Rubyの<%= %>だったり、PHPの<?php ?>と似ています。

例えばcount = 3の時、count is {count}count is 3と表示されます。

useStateって何?

ReactではよくuseXXXという関数が出てきます。これらは、React hooksと呼ばれるものです。useStateは状態を管理するhooksです。

普段JSで変更可能な変数を定義するときはletを使うのではないかと思います。
ということで、letを使ってこのプログラムを書き換えてみましょう。

App.tsx
- import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
-  const [count, setCount] = useState(0)
+  let count = 0

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
-        <button onClick={() => setCount((count) => count + 1)}>
+        <button onClick={() => count++}>
          count is {count}
-        </button>
+        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

こうすると、count is 0と書かれたボタンをクリックしても数字が変わらなくなったのではないでしょうか。

これは、変数の値を変更したということがReactに伝わらず、レンダリングされないのが原因です。useState()を使うと、setCount()countを変更したときに自動的に再レンダリングされます。

実際、この状態でconsole.logなどでcountの数字を確認すると、countの数字自体は増えていることがわかります。

App.ts
-        <button onClick={() => count++}>
+        <button onClick={() => console.log(count++)>

では、useStateletを混在させるとどうなるのでしょうか。

App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)
+  let count2 = 0

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
+        <button onClick={() => count2++}>
+          count2 is {count2}
+        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

結果としては、count2のボタンを押しても0から増えず、countのボタンは押すたびに増えると思います。

では、letで変数を宣言する場所を変えてみましょう。

App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+ let count2 = 0

function App() {
  const [count, setCount] = useState(0)
-   let count2 = 0

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <button onClick={() => count2++}>
          count2 is {count2}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

するとなんということでしょう、count2を押した時には何も変化が起きませんが、countを押すと同時にcount2も増えているではありませんか!

これは、setCount()が呼ばれた時にレンダリングされ、count2の変更も反映されたためです。

では、letfunction App()内にある場合には常に0だったのはなぜでしょうか?それは、レンダリングのたびにlet count2 = 0が評価され、0に戻っていたからです。

コンポーネントってどうやって作るの?

Reactにはクラスコンポーネントと関数コンポーネントがあります。ここでは、関数コンポーネントについて解説します。

srcディレクトリに、MyComponent.tsxというファイルを作成してください。

MyComponent.tsx
export function MyComponent() {
  return <p>コンポーネントです。</p>
}

そして、App.tsxでこのコンポーネントを呼び出します。

App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
+ import { MyComponent } from './MyComponent'

function App() {
  const [count, setCount] = useState(0)
  let count2 = 0

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <button onClick={() => count2++}>
          count2 is {count2}
        </button>
+        <MyComponent />
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

これで、自作のコンポーネントを描画することができました。

コンポーネントにデータを渡すには?

Reactのコンポーネントは、何度も再利用できるのが利点です。再利用するために、親コンポーネントから子コンポーネントにデータを渡したい時があります。そんなときは、HTMLの属性のようにデータを渡すことができます。

<MyComponent value={"何かの値"} />

このデータを子コンポーネントで受け取るには、次のようにします。

MyComponent.tsx
export function MyComponent(props: { value: string }) {
  return <p>valueは{props.value}です。</p>;
}

こうすると、MyComponentを配置した場所にvalueとして渡した値が表示されます。value={count.toString()}とすると、子コンポーネントにカウントの値を渡すことができます。

渡されるpropsが変化すると、その度にコンポーネントの計算が行われます。

useEffectってのを見かけたんだけど…?

こちらもReact hooksの一種です。レンダリングが完了した後に実行したいものや、特定の変数が変化した場合のみに実行したい場合に使われます。

先ほどのMyComponentの例を使って説明します。

MyComponent.tsx
import { useEffect } from 'react';

export function MyComponent(props: { value: string }) {
  console.log('not in useEffect', document.getElementById("myComponent"))

  useEffect(() => {
    console.log('in useEffect', document.getElementById("myComponent"))
  })

  return <p id="myComponent">{props.value}</p>;
}

するとconsoleに次のように出力されます。

MyComponent.tsx:4 not in useEffect null
MyComponent.tsx:4 not in useEffect null
MyComponent.tsx:7 in useEffect <p id=​"myComponent">​value​</p>​
MyComponent.tsx:7 in useEffect <p id=​"myComponent">​value​</p>​

これは、直接呼び出した場合にはReactが要素をレンダリングする前に実行されるのに対し、useEffect内で呼び出した場合にはReactが要素をレンダリングした後に実行されるため発生する違いです。

この後にcount isボタンを押すと、not in useEffectの場所でも要素を取得できるようになります。これは、countが増えたために再レンダリングが走り、その時には要素が存在しているためです。

ただし、MyComponentのレンダリングは走っていないため、propsにcountを渡していたとしても変化前の値が入っている状態となります。

App.tsx
<MyComponent value={count.toString()} />
MyComponent.tsx
import { useEffect } from 'react';

export function MyComponent(props: { value: string }) {
  console.log('not in useEffect', document.getElementById("myComponent")?.innerHTML)

  useEffect(() => {
    console.log('in useEffect', document.getElementById("myComponent")?.innerHTML)
  })

  return <p id="myComponent">{props.value}</p>;
}

not in useEffectではin useEffectでの値に対して1少ない値が出力されます。

MyComponent.tsx:4 not in useEffect undefined
MyComponent.tsx:7 in useEffect 0

MyComponent.tsx:4 not in useEffect 0
MyComponent.tsx:7 in useEffect 1

MyComponent.tsx:4 not in useEffect 1
MyComponent.tsx:7 in useEffect 2

useEffectはpropsが変化した場合にstateと同期するのに使わたり、fetchなどと組み合わせてサーバーと通信するのに使われる場合が多いです。

useEffectでpropsの変更をstateと同期する

ここではMyComponentで入力をもち、それを管理するstateがある場合について考えます。

MyComponent.tsx
import { useEffect, useState } from "react";

export function MyComponent(props: { value: string }) {
  const { value } = props;
  const [text, setText] = useState(() => value);

  useEffect(() => {
    setText(value);
  }, [value]);

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setText(e.currentTarget.value);
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "0.5rem",
        padding: "0.5rem",
      }}
    >
      <input type="text" value={text} onChange={handleChange} />
      <p>{text}</p>
    </div>
  );
}

このプログラムで期待される動作は、

  1. countを変化させると、inputおよびp内の文字列も変わる
  2. input内のテキストを変化させると、p内の文字列も変わる
  3. countを変化させると、inputおよびp内の文字列がcountで上書きされる

という動作です。

ここで今までと違うのは、useEffectの第2引数に[value]が渡されていることです。これが渡されると、valueが変化した場合にのみuseEffectの第1引数に渡した関数が実行されます。この第2引数をuseEffectの依存といいます。

第2引数の[value]を取り除いて実行すると、inputに文字を入力しても変化しなくなります。これは、毎回useEffectに渡した関数が実行され、textvalueで上書きされてしまうためです。

逆に、第2引数に空配列[]を渡すと、コンポーネントのマウント時、1回のみ実行されるようになります。その場合、countが変化しても、textはそれに追従しなくなります。

useEffectでfetchしてみる

次にuseEffect内でデータを取得してみましょう。今回は/public内にfetch.txtを用意し、その内容を表示してみます。

/public/fetch.txt
fetchに成功しました!!

今回、propsは不要なので、消します。

tsx.App.tsx
<MyComponent />
MyComponent.tsx
import { useEffect, useState } from "react";

export function MyComponent() {
  const [text, setText] = useState("");

  useEffect(() => {
    fetch("/fetch.txt").then(async (res) => {
      setText(await res.text());
    });
  }, []);

  return <p>{text}</p>;
}

こうすることで、レンダリング後に1度だけfetchを実行することができます。ボタンの下にfetchに成功しました!!という文言が表示されれば成功です。

useEffectの兄弟的なuseLayoutEffectというものも存在するので興味のある方は調べてみてください。

レンダリング回数を減らしたいときは?

先ほど、コンポーネントに渡されるpropsが変化すると、その度に計算が行われると書きました。

そして、JavaScriptでは関数やオブジェクトは中身が同じでも定義のたびに違うオブジェクトとして生成されます。

例えば以下の場合:

App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { MyComponent } from './MyComponent'

function App() {
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState('')

  const logMessage = (text: string) => {
    setMessage(text)
  }

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <MyComponent log={logMessage} />
        <pre>
          {message}
        </pre>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App
MyComponent.tsx
import { useEffect, useState } from "react";

export function MyComponent(props: { log: (text: string) => void }) {
  const { log } = props;
  const [text, setText] = useState("");

  useEffect(() => {
    fetch("/fetch.txt").then(async (res) => {
      const resText = await res.text()
      setText(resText);
      log(resText)
    });
  }, [log]);

  return <p>{text}</p>;
}

App.tsxにて、countが変化するたびにコンポーネントの計算が行われ、logMessageは新しいオブジェクトとして認識されます。そのため、MyComponent.tsxuseEffectcountが変化するたびに実行されます。(開発者ツールのネットワークタブなどで確認できます。)

そこで、この関数logMessageをメモ化するために使われるのがuseCallbackです。

App.tsx
+ import { useCallback, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { MyComponent } from './MyComponent'

function App() {
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState('')

+  const logMessage = useCallback((text: string) => {
    setMessage(text)
+  }, [])

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <MyComponent log={logMessage} />
        <pre>
          {message}
        </pre>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

こうしてメモ化することで、不要なuseEffectの発火を防ぐことができます。

[log]をuseEffectの第2引数に渡さなければいいのでは?と思った方もいるかもしれません。実際、今回の例ではそれでも動いてしまいます。

しかし、setMessage関数が変化する場合や、他の変数の影響を受ける可能性がある場合(例えば、同時にcountをpostする役割があった場合など)に古い関数を呼び出すことになってしまい、不具合の元となってしまいます。

useEffectの兄弟には、useMemoがいます。また、コンポーネントを丸ごとメモ化する、memo()という関数もいます。

終わりに

以上、手を動かして覚えるReact入門でした。

実際に文章でこういう仕組みだよ、とかこういうルールがあるよ、って言われてもピンとこないこともあると思うので(自分はよくあります)、実際にコードを書いて動かしてみると理解しやすいのではないかと思い書いてみました。

少しでもReactに対する理解が深まったなぁと感じていただけると嬉しいです。

お知らせ

DONUTSでは新卒中途問わず積極的に採用活動を行っています。
札幌での新卒やインターンの募集もあるので、ご興味あればぜひご覧ください

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?