ジョブカン事業部のアドベントカレンダー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などに固定する場合
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の特殊な部分について説明します。
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
を使ってこのプログラムを書き換えてみましょう。
- 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の数字自体は増えていることがわかります。
- <button onClick={() => count++}>
+ <button onClick={() => console.log(count++)>
では、useState
とlet
を混在させるとどうなるのでしょうか。
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
で変数を宣言する場所を変えてみましょう。
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の変更も反映されたためです。
では、let
がfunction App()
内にある場合には常に0だったのはなぜでしょうか?それは、レンダリングのたびにlet count2 = 0
が評価され、0に戻っていたからです。
コンポーネントってどうやって作るの?
Reactにはクラスコンポーネントと関数コンポーネントがあります。ここでは、関数コンポーネントについて解説します。
src
ディレクトリに、MyComponent.tsx
というファイルを作成してください。
export function MyComponent() {
return <p>コンポーネントです。</p>
}
そして、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={"何かの値"} />
このデータを子コンポーネントで受け取るには、次のようにします。
export function MyComponent(props: { value: string }) {
return <p>valueは{props.value}です。</p>;
}
こうすると、MyComponent
を配置した場所にvalue
として渡した値が表示されます。value={count.toString()}
とすると、子コンポーネントにカウントの値を渡すことができます。
渡されるpropsが変化すると、その度にコンポーネントの計算が行われます。
useEffectってのを見かけたんだけど…?
こちらもReact hooksの一種です。レンダリングが完了した後に実行したいものや、特定の変数が変化した場合のみに実行したい場合に使われます。
先ほどのMyComponent
の例を使って説明します。
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
を渡していたとしても変化前の値が入っている状態となります。
<MyComponent value={count.toString()} />
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がある場合について考えます。
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>
);
}
このプログラムで期待される動作は、
-
count
を変化させると、input
およびp
内の文字列も変わる - input内のテキストを変化させると、
p
内の文字列も変わる -
count
を変化させると、input
およびp
内の文字列がcountで上書きされる
という動作です。
ここで今までと違うのは、useEffect
の第2引数に[value]
が渡されていることです。これが渡されると、value
が変化した場合にのみuseEffectの第1引数に渡した関数が実行されます。この第2引数をuseEffectの依存といいます。
第2引数の[value]
を取り除いて実行すると、input
に文字を入力しても変化しなくなります。これは、毎回useEffectに渡した関数が実行され、text
がvalue
で上書きされてしまうためです。
逆に、第2引数に空配列[]
を渡すと、コンポーネントのマウント時、1回のみ実行されるようになります。その場合、count
が変化しても、text
はそれに追従しなくなります。
useEffectでfetchしてみる
次にuseEffect内でデータを取得してみましょう。今回は/public
内にfetch.txt
を用意し、その内容を表示してみます。
fetchに成功しました!!
今回、propsは不要なので、消します。
<MyComponent />
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では関数やオブジェクトは中身が同じでも定義のたびに違うオブジェクトとして生成されます。
例えば以下の場合:
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
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.tsx
のuseEffect
はcount
が変化するたびに実行されます。(開発者ツールのネットワークタブなどで確認できます。)
そこで、この関数logMessage
をメモ化するために使われるのがuseCallback
です。
+ 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では新卒中途問わず積極的に採用活動を行っています。
札幌での新卒やインターンの募集もあるので、ご興味あればぜひご覧ください