それ、もうWeb標準APIでできるんです


みなさんも日々身にしみて感じていらっしゃるとは思いますが、Webの技術の変革の速度は目を見張るものがあります。数ヶ月前の常識が時代遅れになってしまうというネガティブな面もあれば、以前は困難だったものが今ではものすごく簡単に出来てしまうといったポジティブな側面もあります。

本記事で扱うのは後者で、以前はライブラリに頼って実装していたものが、最近ではWebのスタンダードなAPIで出来るようになったという例をいくつかご紹介したいと思います。

ユニークなIDの発行

まずはUUIDの発行です。従来は uuidnanoid をインストールして生成していましたが、 Web Crypto API がどの環境でも使えるようになり、簡単にユニークなIDを生成できるようになりました。

const uuid = crypto.randomUUID(); // => ex) '71b9eb20-39c4-48f1-9680-6f804c79bc6c'

グローバルに crypto という Crypto インターフェースのインスタンスが生えており、それを参照して randomUUID() メソッドを呼び出すことでUUIDが取得できます。引数は受け取らないため uuidnanoid のようにバージョンや桁数をカスタマイズすることはできませんが、シンプルにUUIDを使いたい場合には十分有用です。ただし、この機能は https のような「安全なコンテキスト」内でしか使えませんので注意しましょう。非セキュアな環境では randomUUIDundefined となります。

ユニークなIDの身近な使い道の例としては React の map 処理における key の生成が挙がります。 map で回されるアイテムの要素に id が振られていないなど、 key として使えそうなものがない場合に活用できます。

export function ListComponent ({ items }: { items: string[] }): JSX.Element {
  const itemsWithKeys = useMemo(() => (
    items.map((value) => ({ key: crypto.randomUUID(), value }))
  ), [items]);

  return (
    <ul>
      {itemsWithKeys.map(({ key, value }) => (
        <li key={key}>{value}</li>
      ))}
    </ul>
  );
}

URLのバリデーション

次は、文字列が正しいURLかどうかを判別するバリデーションです。今までは正規表現を使ったり validator のようなパッケージを活用するなどして実装していたと思います。下のコードは validator パッケージによる例です。

import { isURL } from 'validator';

isURL('https://www.example.com'); // => true
isURL('foobarbaz'); // => false

現在ではパッケージに依存せずに標準の URLコンストラクタ でこれを実装することができます。URLコンストラクタは引数にURL文字列を受け取りますが、文字列が正しいURLの書式でなかった場合に TypeError: Invalid URL をスローします。

new URL('foobarbaz'); // Uncaught TypeError: Failed to construct 'URL': Invalid URL

つまり例外がスローされた場合は正しいURLではないということなので、その例外を catch して false を返せばよいですね。

ちなみに正しいURLだった場合は URLインターフェースのオブジェクトが生成されます。URLインターフェースではプロトコル( protocol )が参照出来るので、正としたいプロトコルの場合だけ true を返すようにしましょう。これで ftp:// のようなURL文字列を弾く事ができます。

function isURL (value: string): boolean {
  try {
    const url = new URL(value);
    return ['http:', 'https:'].includes(url.protocol);
  } catch (error) {
    return false;
  }
}

Emailのバリデーション

では、Email文字列のバリデーションはどうでしょうか。残念ながらURLの様に便利なインターフェースは用意されてはいない様なので、別の方法を考えてみました。 HTMLInputElementValidityState を活用してみます。

先に申し上げておきますが、お勧めできる手法ではありません。

HTMLInputElement すなわち <input /> 要素は type 属性で挙動を選択する事ができますが、その中に email があります。これが指定された場合、 input 要素は入力ごとに値が正しいメールアドレス文字列かどうかを検証するようになります。バリデーションの結果は validity プロパティで参照することができます。

const $input = document.createElement('input');
$input.type = 'email';

$input.value = 'webmaster@example.com';
console.log($input.validity.valid); // => true

$input.value = 'foobarbaz';
console.log($input.validity.valid); // => false

これを関数にまとめれば isEmail() の出来上がりという寸法ですが、それだけではバリデーションのたびに input 要素を生成することになり、計算コストが気になります。せっかくなのでReact での使用を前提として、生成した input 要素を使い回せるように hooks 化してみたいと思います。

import { useEffect, useState } from 'react';

export const useIsEmail = (): [((value: string) => boolean) | undefined] => {
  const [isEmail, setIsEmail] = useState<(value: string) => boolean>();
  useEffect(() => {
    const $input = document.createElement('input');
    $input.type = 'email';
    setIsEmail(() =>
      (value: string): boolean => {
        $input.value = value;
        return $input.validity.valid;
      }
    );
  }, []);
  return [isEmail];
};

Reactはレンダリングされてからでないと document は参照できないので、 useEffect を使って回避しており想定外にコードが複雑化してしまいました。

このような使い方を想定しています。

const [isEmail] = useIsEmail();
console.log(isEmail?.('info@example.com'));

なぜお勧めできないか

このやり方がお勧めできない理由はいくつかあります。

  1. 仕様で想定されていない使い方で強引に実装している
  2. document を参照するため node.js で使えない(フロントエンドとバックエンドで異なるロジックで検証をすることになる)
  3. レンダリングを待つ必要があるため CLS などに悪影響を及ぼす可能性がある

一番重要だと考えるのは 1. で、正規の方法ではないバッドノウハウで実装した場合は予想されない不具合が生じる可能性もあるでしょう。とはいえこういう泥臭い手法もあるのだと知っておくことは無駄にはならないと思います。どこかで役立つ場面があるかもしれません。

Arrayの操作

近年のJavaScriptの進化で最も分かりやすいのは、おそらく Array のメソッド群でしょう。以前は lodash の関数等を活用していたようなものも、今ではその多くが標準のインスタンスメソッドで簡単に再現出来るようになりました。

例えば配列の末尾を取得するための _.lastArray.prototype.at で引数に -1 を渡すことで再現できます。

const array = ['foo', 'bar', 'baz'];

_.last(array); // => 'baz'
array.at(-1); // => 'baz'

また例えば、多次元配列を一次元にする _.flatten _.flattenDeep は、 Array.prototype.flat で代替できます。 .flat() は引数に深さレベルを指定できるので、実質上位互換と言えます。

const array = [1, 2, 3, [4, 5, [6, 7, 8]], 9];

_.flattenDeep(array); // => [1, 2, 3, 4, 5, 6, 7, 8, 9]
array.flat(Infinity); // => [1, 2, 3, 4, 5, 6, 7, 8, 9]

従来は回りくどい手順を踏まなければならなかった処理が1メソッドで済むようになった例もあります。 Array.prototype.with はインデックスを指定して値を変更し、結果を新しい配列として返します。

// 従来
const array = ['foo', 'bar', 'baz'];
array[1] = 'qux';
console.log(array); // => ['foo', 'qux', 'baz']

// 新しい方法
const array = ['foo', 'bar', 'baz'];
console.log(array.with(1, 'qux')); // => ['foo', 'qux', 'baz']

この例では一行減っただけですが、メソッドチェーンの中で使うことが出来るのは大きなメリットではないでしょうか。

この他にも有用なメソッドは数多くあります。たまにメソッド一覧を眺めたりすると、新しい発見があるかもしれません。

最後に

繰り返しになりますが、Webの環境はどんどん変化していきます。ライブラリはリッチになり、生のJavaScriptで出来ることも増えていきます。わたしたちはより良いプロダクトをつくるため、それらを常にキャッチアップしていかなければなりません。

数年後は一体どんな世界になっているのか、楽しみですね。

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Oikawa Hisashi

フロントエンドエンジニア。モダンなJavaScript開発に関心があります。 デザインからバックエンドまで網羅的にこなすマルチデザイナーとして長く活動してきた経験を活かして、これから関わる様々なものをデザインしていきたいです。チームもコミュニケーションもデザインするもの。ライフワークはピアノと水泳。