Closed8

VueユーザーがReactに入門してみる

hirohiro

業務、個人開発含めてVueばかりを使ってきましたがNext.jsを使用する機会が生まれたのでReactに入門します。

VueのCompositionAPIと書き方を比べながらまとめていきたいと思います。

hirohiro

コンポーネントの作成

React アプリはコンポーネントで構成されています。コンポーネントとは、独自のロジックと外見を持つ UI(ユーザインターフェース)の部品のことです。コンポーネントは、ボタンのような小さなものである場合も、ページ全体を表す大きなものである場合もあります。
React におけるコンポーネントとは、マークアップを返す JavaScript 関数です。

Reactは基本的にJSXで記述していく。
Vueの場合はSFCが主流(?)(少なくとも自分はそれ以外を使ったことがない)

勘違いしてはいけないのは、JSXはあくまでもJavaScriptの拡張記法であり、React専用の機能ではないということ。(VueもJSX使えます!使ったことないけど)

サンプルコード

Reactの公式からコードを引用します。
内容はボタンコンポーネントを作成してそれを親コンポーネントで表示するというシンプルな構成のコードです。

  • Reactの場合
App.jsx
function MyButton() {
  return (
    <button>
      I'm a button
    </button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton />
    </div>
  );
}
  • Vueの場合
MyButton.vue
<script setup>
</script>
<template>
 <button>
    I'm a button
  </button>
</template>
App.vue
<script setup>
import MyButton from './MyButton.vue'
</script>
<template>
  <h1>Welcome to my app</h1>
  <MyButton />
</template>

このような表示になります。

Reactの場合一つのファイル内にコンポーネントを複数作ることができるのが良いな、と感じました。
※VueもSFCを採用しなければ一つのファイル内に複数コンポーネントを作成できます

hirohiro

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>がレンダリングに影響を及ばさないから同等の機能だと思います。
https://ja.react.dev/reference/react/Fragment


  • 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>
hirohiro

条件付きレンダー

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>
hirohiro

リストのレンダー

データの配列を操作するには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>
hirohiro

イベントの登録

コンポーネント内でイベントハンドラ関数を宣言し、それを要素のonclick属性に対して作成した関数を設定することでイベントに応答することができる。
JSXなのでonclick属性はonClickのようにキャメルケースで記述する必要があることに注意。

function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

ボタンをクリックするとalertが表示されます。

※あくまで上記はクリックイベントにフォーカスした例で、もちろんonchangeイベントやonblur等も使用可能。
以下にイベント一覧が記述されています。
https://ja.react.dev/reference/react-dom/components/common

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に対応する@changeonblurに対応する@blur等もあり、様々なイベントを扱えます。

hirohiro

画面の更新

状態管理するための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 つの値を持つ配列を返します。

  1. この state 変数の現在の値。最初は、初期 state に設定されます。
  2. インタラクションに応じて、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ではリアクティブな値を定義するためにrefreactiveもあるがここでは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の方がシンプルに記述できている印象です。

hirohiro

コンポーネント間でデータを共有する

前述の例では、それぞれのCounterコンポーネントが独立したcountを持っていて、ボタンがクリックされるたびにクリックされたボタンのcountだけが変更されていた。
つまりそれぞれ独立したstate(状態)を持っていた。

この章ではコンポーネント間でデータを共有し、常に一緒に更新して状態を共有する方法を学ぶ。

結論から言うと、親コンポーネントから子コンポーネントに対して情報を渡すことで実現できる。
このように親から子に対して渡す情報をpropsという。この呼び名はVueも同じなので理解しやすい。

では記述していく。
まず、CounterからMyAppstateを移動する

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からMyButtonstateを渡し、クリックハンドラも渡す。

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>
  );
}

最後に、Counterpropsを受け取る。
一般的に分割代入を使用したほうが冗長な記述を減らせる。

{/* ...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 APIscript setupを使用する前提)は子コンポーネントでdefinePropsというscript setup 内でのみ使用可能なコンパイラーマクロを使用する必要がある。
また、子コンポーネントでのイベントを親コンポーネントで検知するためにdefineEmitsを使用する必要もある。

Count.vue
<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>
MyApp.vue
<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よりも一手間多いですね。
ただ、慣れてしまえばどちらも使いやすいです。

このスクラップは4ヶ月前にクローズされました