VueユーザーがReactに入門してみる
業務、個人開発含めてVueばかりを使ってきましたがNext.jsを使用する機会が生まれたのでReactに入門します。
VueのCompositionAPIと書き方を比べながらまとめていきたいと思います。
コンポーネントの作成
React アプリはコンポーネントで構成されています。コンポーネントとは、独自のロジックと外見を持つ UI(ユーザインターフェース)の部品のことです。コンポーネントは、ボタンのような小さなものである場合も、ページ全体を表す大きなものである場合もあります。
React におけるコンポーネントとは、マークアップを返す JavaScript 関数です。
Reactは基本的にJSXで記述していく。
Vueの場合はSFCが主流(?)(少なくとも自分はそれ以外を使ったことがない)
勘違いしてはいけないのは、JSXはあくまでもJavaScriptの拡張記法であり、React専用の機能ではないということ。(VueもJSX使えます!使ったことないけど)
サンプルコード
Reactの公式からコードを引用します。
内容はボタンコンポーネントを作成してそれを親コンポーネントで表示するというシンプルな構成のコードです。
- Reactの場合
function MyButton() {
return (
<button>
I'm a button
</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
);
}
- Vueの場合
<script setup>
</script>
<template>
<button>
I'm a button
</button>
</template>
<script setup>
import MyButton from './MyButton.vue'
</script>
<template>
<h1>Welcome to my app</h1>
<MyButton />
</template>
このような表示になります。
Reactの場合一つのファイル内にコンポーネントを複数作ることができるのが良いな、と感じました。
※VueもSFCを採用しなければ一つのファイル内に複数コンポーネントを作成できます
JSXの記法について
基本的な記述方法
- タグは必ず閉じる
function Component() {
return <input type="text" className="input" />
}
- 複数のJSXをリターンできない
<div>...</div>
の様な要素でラップする場合
function Component() {
return (
<div>
<h1>Hello, World</h1>
<p>Learn React</p>
</div>
)
}
レンダリングの際に<div></div>
が含まれるようになる。
空の<>...</>
でラップする場合。
<Fragment>
と呼ばれる機能を使用する。
function Component() {
return (
<>
<h1>Hello, World</h1>
<p>Learn React</p>
</>
)
}
レンダリングの際に無駄な要素を生まない。
Vueだと<template>
がレンダリングに影響を及ばさないから同等の機能だと思います。
- styleの指定には
className
を使用する。
JSXはJSの拡張記法であり、class
が予約語なので競合してしまう。
それを回避するためにclassName
を使用する。
<img className="avatar" />
Vueは通常のHTMLと同様にclass
が使用できる。
- データの表示
{}
を使用することで、JSX の中から JavaScript に「戻る」ことができ、コード内の変数を埋め込んでユーザに表示することができます。
return (
<h1>
{user.name}
</h1>
);
{}
の中はJSの世界なので文字列の連結なども可能。
const user = {
name: 'Hedy Lamarr',
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
imageSize: 90,
};
export default function Profile() {
return (
<>
<h1>{user.name}</h1>
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
width: user.imageSize,
height: user.imageSize
}}
/>
</>
);
}
上記の例では、style={{}}
は特別な構文ではなく、style={ }
という JSX の波括弧内にある通常の {} オブジェクトです。スタイルが JavaScript 変数に依存する場合は、style 属性を使うことができます
Vueだと二重波括弧{{}}
(マスタッシュ構文)を使用することで、マークアップ部分にJavaScriptを展開できる。
<script setup>
import { ref } from 'vue';
const user = ref({
name: 'Hiro'
age: 0
})
</script>
<template>
<h1>
{{ user.name }}
</h1>
</template>
条件付きレンダー
React には、条件分岐を書くための特別な構文は存在しません。代わりに、通常の JavaScript コードを書くときに使うのと同じ手法を使います。例えば、if ステートメントを使って条件付きで JSX を含めることができます
import AdminPanel from './AdminPanel';
import LoginForm from './LoginForm';
export default function MyComponent({ isLoggedIn }) {
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return (
<div>
{content}
</div>
);
};
// 三項演算子を使用した場合
<div>
{isLoggedIn ? (
<AdminPanel />
) : (
<LoginForm />
)}
</div>
// else側の処理がない場合
<div>
{isLoggedIn && <AdminPanel />}
</div>
Vueの場合はv-if
ディレクティブを使用することで条件付きレンダリングを実現している。
<script setup>
import { ref } from 'vue';
import AdminPanel from './AdminPanel.vue';
import LoginForm from './LoginForm.vue';
const isLoggedIn = ref(false);
</script>
<template>
<div>
<AdminPanel v-if="isLoggedIn" />
<LoginForm v-else />
</div>
</template>
リストのレンダー
データの配列を操作するにはJSの配列メソッドを使用することができる。
const products = [
{ title: 'Cabbage', isFruit: false, id: 1 },
{ title: 'Garlic', isFruit: false, id: 2 },
{ title: 'Apple', isFruit: true, id: 3 },
];
export default function ShoppingList() {
const listItems = products.map(product => {
<li
key={product.id}
style={{
color: product.isFruit ? 'magenta' : 'darkgreen'
}}
>
{product.title}
</li>
});
return (
<ul>{listItems}</ul>
)
}
至ってシンプルで直感的な書き方で好きです。
key属性
について
リストレンダリングをする際は、key属性
という特別な属性を必ず指定する。
<li
key={product.id}
style={{
color: product.isFruit ? 'magenta' : 'darkgreen'
}}
>
これはリスト内の項目を、一意に識別するための属性で、文字列または数値を指定する。
key
は、配列のどの要素がどのコンポーネントに対応するのかを React が判断し、後で正しく更新するために必要。これが重要となるのは、配列の要素が移動(ソートなどによって)した場合、挿入された場合、あるいは削除された場合。適切に key
を選ぶことで、React は何が起こったか推測し、DOM ツリーに正しい更新を反映させることができる。
通常このkey
はデータから来るはずで、DB上のIDなどを渡すといい。
公式にも以下のような記述がある。
key は動的に生成するのではなく、元データに含めるべきです。
Vueの場合にはv-forディレクティブを使う。
<script setup>
import { ref } from 'vue';
const products = ref([
{ title: 'Cabbage', isFruit: false, id: 1 },
{ title: 'Garlic', isFruit: false, id: 2 },
{ title: 'Apple', isFruit: true, id: 3 },
]);
</script>
<template>
<ul>
<li
v-for="product in products"
:key="product.id"
:style="{ color: product.isFruit ? 'magenta' : 'darkgreen' }"
>
{{ product.title }}
</li>
</ul>
</template>
イベントの登録
コンポーネント内でイベントハンドラ関数を宣言し、それを要素のonclick
属性に対して作成した関数を設定することでイベントに応答することができる。
JSXなのでonclick
属性はonClick
のようにキャメルケースで記述する必要があることに注意。
function MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
ボタンをクリックするとalertが表示されます。
※あくまで上記はクリックイベントにフォーカスした例で、もちろんonchange
イベントやonblur
等も使用可能。
以下にイベント一覧が記述されています。
Vueの場合
Vueの場合はv-on
ディレクティブを使用することで、イベントを登録することができる。
v-on
と書くのが面倒な場合は@
に省略することができる。(個人的に省略して@
で書くことが多いと思います。)
上記のReactのコードと同様の処理を書き換えてみます。
<script setup>
function handleClick() {
alert('You clicked me!');
}
</script>
<template>
<button @click="">Click me</button>
</template>
Vueに関してもonchange
に対応する@change
やonblur
に対応する@blur
等もあり、様々なイベントを扱えます。
画面の更新
状態管理するためのhooksであると理解。
状態とはコンポーネントのレンダリング間で保持され、値を変更することで再レンダリングが起きるような値のこと。
Vueではこのような値を**リアクティブな値(リアクティビティー)**と呼んでいる。
理解の手助けのためにボタンを押すとカウントが1ずつ増えるようなコンポーネントを考える。
function Counter() {
let count = 0;
function increment() {
return count++;
}
return <button onClick={increment}>Clicked {count} times</button>;
};
表示されている数値({count}
)がクリックのたびに増えて行きそうなコード。しかしこれでは画面の表示は変わらない。
内部的にcount
の数値は増えて行くが表示は初期値の0のままである。
また、このCounterコンポーネントを複数箇所で定義することを考えると、定義されたすべての場所で同じ変数を共有するような動作になる。このような外部の値を扱うようなコンポーネントはバグや動作不良の原因になってしまう。
そのため、
- 値の変更に応じて再レンダリングが発火するような変数
- 異なるレンダリング間で値を保持するような変数
を満たせば正常なカウンターができる。
useState
上記の条件を満たすために使用されるのがuseState
フック。
import { useState } from 'react';
function MyComponent() {
const [count, setCount] = useState();
const [name, setName] = useState('Taylor');
// ...
// 分割代入を使用しなかったらかなり冗長
const somethingState = useState(0);
const something = somethingState[0];
const setSomething = somethingState[1];
}
使い方はシンプルで、
- コンポーネントのトップレベルで
useState
をインポート - 分割代入を使用して
[something, setSomething]
のように命名
でOK!
useState は、以下の 2 つの値を持つ配列を返します。
- この state 変数の現在の値。最初は、初期 state に設定されます。
- インタラクションに応じて、state を他の値に変更するためのset 関数。
以上を踏まえてボタンをクリックするたびにカウンターが増えるコードに書き換えてみます。
import { useState } from 'react';
export default function MyApp() {
return (
<div>
<h1>Counters that update separately</h1>
<Counter />
<Counter />
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return (
<button onClick={increment}>
Clicked {count} times
</button>
);
}
これでクリックのたびに数値が再レンダリングされるようになりました。
Vueの場合
Vueではリアクティブな値を定義するためにref
(reactive
もあるがここではref
のみを使用)を使用します。
先述のReactのコードを書き換えてみます。
<script setup>
import { ref } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">Clicked {{ count }} times</button>
<button @click="increment">Clicked {{ count }} times</button>
</template>
Vueの方がシンプルに記述できている印象です。
コンポーネント間でデータを共有する
前述の例では、それぞれのCounter
コンポーネントが独立したcount
を持っていて、ボタンがクリックされるたびにクリックされたボタンのcount
だけが変更されていた。
つまりそれぞれ独立したstate
(状態)を持っていた。
この章ではコンポーネント間でデータを共有し、常に一緒に更新して状態を共有する方法を学ぶ。
結論から言うと、親コンポーネントから子コンポーネントに対して情報を渡すことで実現できる。
このように親から子に対して渡す情報をprops
という。この呼び名はVueも同じなので理解しやすい。
では記述していく。
まず、Counter
からMyApp
にstate
を移動する
import { useState } from 'react';
export default function MyApp() {
+ const [count, setCount] = useState();
+
+ function handleClick() {
+ setCount(count + 1);
+ }
return (
<div>
<h1>Counters that update separately</h1>
<Counter />
<Counter />
</div>
);
}
function Counter() {
- {/* ... we're moving code from here ... */}
}
次に、MyApp
からMyButton
にstate
を渡し、クリックハンドラも渡す。
import { useState } from 'react';
export default function MyApp() {
const [count, setCount] = useState();
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update separately</h1>
+ <Counter count={count} onClick={handleClick} />
+ <Counter count={count} onClick={handleClick} />
</div>
);
}
最後に、Counter
でprops
を受け取る。
一般的に分割代入を使用したほうが冗長な記述を減らせる。
{/* ...MyAppコンポーネントの処理 */}
{/* このようにpropsのみを受け取って、それぞれ展開もできるけど... */}
function Counter(props) {
return (
<button onClick={props.onClick}>
{props.count}
</button>
);
}
{/* 分割代入を使った方がスマート */}
function Counter({count, onClick}) {
return (
<button onClick={onClick}>
{count}
</button>
);
}
これでどちらのボタンをクリックしてもcountは同時に増えて行く
Vueの場合
Vueの場合(Composition API
でscript setup
を使用する前提)は子コンポーネントでdefineProps
というscript setup
内でのみ使用可能なコンパイラーマクロを使用する必要がある。
また、子コンポーネントでのイベントを親コンポーネントで検知するためにdefineEmits
を使用する必要もある。
<script setup>
const props = defineProps({
count: {
type: Number,
required: true
}
});
const emit = defineEmits(['childClick']);
function onClick() {
emit('childClick');
}
</script>
<template>
<button @click="onClick">
Clicked {{ count }} times
</button>
</template>
<script setup>
import { ref } from 'vue';
import Count from './Count.vue';
const count = ref(0);
function handleClick() {
count.value++;
}
</script>
<template>
<div>
<h1>Counters that update together</h1>
<Count :count="count" @childClick="handleClick" />
<Count :count="count" @childClick="handleClick" />
</div>
</template>
Vueの場合はイベントを子コンポーネントから発行する必要があり、Reactよりも一手間多いですね。
ただ、慣れてしまえばどちらも使いやすいです。