APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

ReactユーザーがSvelteを使ってみてよかったと思うところ

この記事は、エーピーコミュニケーションズ Advent Calendar 2019 の9日目のエントリです。

先進サービス開発事業部の高橋です。フロントエンドエンジニアを担当しています。 普段の業務ではReactJSを使っていますが、業務外でちょっとした画面作りにSvelteという最近海外で話題になっているフレームワークを使ってみたのでReactとの違いというか良いと感じた点を書いてこうと思います。

f:id:ysktkjnnjktksy:20191208184832p:plain

公式ドキュメントがとても親切

Svelteのドキュメントはかなり親切だと感じます。 チュートリアルを一通りやってみれば、それなりに実装できるようになると思います。 Exampleが実践的かつREPLで提供されているので実にお手軽にコードの挙動を確かめることができます。

Reactのドキュメントも日本語対応(ちょっと怪しい)しているし、不親切なわけではないですが、飲み込むまで結構時間がかかった記憶があります。

svelte.dev

コーディング量がとにかく少ない

ReactもFunctional Componentでできることが増えたことによって、比較的シンプルに書けるようになりましたが、Svelteはさらに簡素に記述できてしまいます。

例えば、クリックしたらカウントアップするというようなチュートリアルみたいなコンポーネントを例に比較してみましょう。

Reactでの記述

まずReactから。v1.16.8からHookが使えるようになり、記述がシンプルな関数コンポーネントでもステートやライフサイクルメソッド(のようなもの)が扱えるようになりました。

import React, { useState } from 'react';

const Counter = props => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Click!
      </button>
      <div>{count}</div>
    </div>
  );
}

export default Counter;

Svelteでの記述

そして、Svelteです。Reactでも十分すぎるほどシンプルなコードがさらに省エネなコードになっています。

<script>
  let count = 0;
</script>
<div>
  <button on:click={() => count++}>
    Click!
  </button>
  <div>{count}</div>
</div>

随分と簡単に作れてしまうので、プログラマとしては少し物足りない感じはします。

データバインディングに融通がきく

Reactは親コンポーネントから子コンポーネントへの単一方向のデータバインディングしか許されていませんでした。 それはそれで統制の取れた素晴らしい設計思想だと思うのですが、正直めんどくさいと感じる時があります。子要素から親要素にデータの変更を通知するには、reduxを使ってPropsとして親要素に渡すといった遠回りな方法が必要になります。 一方、Svelteの場合、原則的にはReactと同じく親から子への単一方向のデータバインディングとなっていますが、例外的に子要素から親要素にデータを渡すことができます。

まずは親コンポーネントを見ていきましょう。

<script>
  let isCounting = false;
  import Child from "./Child.svelte";  // 子コンポーネント
  // 子コンポーネントからカスタムイベントを通じてデータを受け取る
  export let count = 0;
  const updateStatus = event => {
    count = event.detail;
  };
</script>
<button
  disabled={(isCounting)}
  on:click={() => {
    isCounting = true;
}}>
  count start
</button>
<button
  disabled={(!isCounting)}
  on:click={() => {
    isCounting = false;
}}>
  count stop
</button>
<p>{count}</p>
<Child 
  on:countUpStatus={updateStatus} //  カスタムイベント
  isCount={isCounting}/>

数値を1秒ごとにカウントアップするタイマーの再生・停止ボタンを用意します。 Parentコンポーネントではカウントの再生・停止の判断用のisCountというフラグをChildコンポーネントに渡します。 また、後述のChildコンポーネントで設定したカスタムイベントを呼び出して、イベント発火時に 呼ばれるコールバック関数updateStatus で値を受け取っています。

<script>
  export let isCount;
  import { createEventDispatcher, afterUpdate } from "svelte";
  let count = 0;
  let timerId = null;
  const dispatch = createEventDispatcher();
  const countUp = () => {
    timerId = setInterval(() => {
      count++;
      dispatch("countUpStatus", count);
    }, 1000);
  };

  afterUpdate(() => {
    if (isCount) {
      countUp();
    } else {
      clearInterval(timerId);
    }
  });
</script>

{#if isCount}
<div>
カウント中
</div>
{/if}

ChildコンポーネントではcreateEventDispatcher(EventName, Value)というメソッドを使い、カスタムイベントを設定します。 第一引数ではイベント名を、第二引数で値を渡すことができます。 第二引数で渡した値は、当該イベント発火時に、event.detailという形で受け取ることができます。

このようにして、子から親のデータバインドを実現することができます。ただし多用するとデータの流れが複雑化しすぎて帰って面倒になりそうなので、あくまで例外的な処理として活用するのがおすすめです。

Viewにロジックに組み込みやすい

ReactのJSXでは、ロジックを組み込もうとすると少々面倒な記述の仕方になります。その点、Svelteは比較的楽に条件分岐やループの処理をViewに組み込みやすくなっています。 もちろん、Reactがそのような形にしているのは、Viewとロジックを切り分けるという設計思想があり、その思想に賛同しないわけではありません。ですがごくたまーに、もうちょい柔軟にコンポーネントを作れると楽なのになーと思うこともあるわけです。 以下違いを記述します。

Reactでの記述法

JSXの中でループが使えないので、JSXの配列を返す関数を使ってループを実現しています。 また、非同期処理を伴うロジックでは判定用のフラグを用いてレンダリングするDOMを制御しています。

import React, { useState, useEffect } from 'react';

const Items = props => {
  // レンダリング準備完了チェック用ステート
  const [isReady, setStatus] = useState(false);

  // 描画時に走る処理
  useEffect(() => {
    if (!isReady) {
      getReady();
    }
  });

  // Loop処理
  // itemListをレンダリングするための関数
  const renderItems = () => {
    return props.itemList.map(e => {
        return (
          <ul>
            <li>{e.title}</li>
            <li>{e.description}</li>
          </ul>
        );
    });
  };

  // 1秒間待って準備完了ステータスをtrue に変更
  const getReady = async () => {
    return new Promise(resolve => {
      setTimeout(() => {
        setStatus(true);
        resolve(true);
      }, 1000);
    });
  };

  // レンダリング(準備中)
  if (!isReady) {
    return (
      <div>Pleade waiting....</div>
    );
  }

  // レンダリング(準備完了)
  return (
    <div>
      {renderItems()}
    </div>
  );
}

export default Items;

Svelteでの記述法

SvelteのViewはコンポーネント志向というよりはテンプレートエンジンに近いかもしれません。 分岐やループができるのはもちろんのこと、await, then, catchの状態をView上で制御できるのは斬新ですね。

<script>
  export let itemList;
  // 1秒間待って準備完了ステータスをtrue に変更
  const getReady = async () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(true);
      }, 1000);
    });
  };
  let promise = getReady();
</script>

{#await promise}
  <div>Pleade waiting....</div>
{:then}
  <div>
  {#each itemList as item}
    <ul>
      <li>{item.title}</li>
      <li>{item.description}</li>
    </ul>
  {/each}
  </div>
{/await}

ビルドが爆速

一番いい!と思ったのがこれかもしれない。似たようなサイズ感のアプリケーションできちんと計測したわけではないので、正確なことは言えないですが、react-scriptsのビルドよりも2から3倍以上早いと感じます。

デメリット

これまで、良いことばかり言ってきましたが、一応デメリットも。

Reactのようなメジャーなフレームワークに比べると、IDEや開発ツールがまだまだ充実していない ことや、TypeScriptへの対応もまだまだのようで、実践投入は結構ツライかも。 また、業務で採用していろんな人間とプロジェクトを進めていく場合、ある程度不自由でも統制が取れている方が問題は起こりにくく、その点、Reactはバランスが取れたフレームワークだと思います。

所感

そのほかにも、パフォーマンスに優れているとか、優れた機能(click|onceとか)などたくさんありますが、一旦はここまでにします。

基本的に、シンプルで小規模なサイト制作で活躍しそうなフレームワークだと思います。比較しておいてなんですが、ReactよりもVue.jsの方が近い存在かもしれないですね。

既存のモダンフレームワークにはない、斬新さを感じる良いフレームワークだと思います。気になったらぜひ使ってみてください!