dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Pixela APIをブラウザ上で試せるPlaygroundをNext.jsで作った

作った。
f:id:dackdive:20210905015031p:plain:w480

最低限の機能しかないしコードも汚いところいっぱいあるけどとりあえず公開することにした。
ここで試せます。

pixela-api-playground.zaki-yama.dev

ソースコードはこちらに。

モチベーション

ここのところ仕事では SDK など GUI を持たないライブラリやCLI を作ることが多く、Next.jsswr といった最近よく聞くフロントエンド技術スタックを素振りできずにいて危機感があった。

これらの素振りをするのにちょうどよい題材はないものかと思っていたところ、 Pixela という API サービスに出会ったので
この API をブラウザ上で実行できる簡単な Web アプリケーションを作ってみた。

Pixela について、詳しくは公式サイト(https://pixe.la/ja) の説明に譲るとして、GitHub の Contribution Graph のような草を生やすことができる API サービス。
習慣化したい活動を何でも記録して草を生やすことができる。最高。

自分の場合は、日々の読書時間を記録して GitHub ぽく見れたらなーと思っていたところこのサービスを見つけた。

Pixela は、 すべての機能が REST 風の API でしか提供されていない ところが特徴的。つまり、最初のユーザー登録から何からすべて curl などを利用して直接 API を叩いて行う必要がある。

これは API を利用することに慣れたプログラマにとって非常にとっつきやすい一方、特に最初どんな API があるのか、どんなリクエストに対してどんなレスポンスが返ってくるのかを色々試したいようなときには GUI でのインターフェースもあると自分以外の人にとっても便利かもなーと思い、ブラウザ上で API を一通り試せるようなサイトを作ることにした。

機能について

今のところは本当に各種 API を揃えただけ、という感じなので、便利機能とかはないです。
グラフ表示 API など一部の APIJSON ではなく SVG でレスポンスが返ってくるが、そういったものはサイト上で直接画像が見られるようになっている。

f:id:dackdive:20210905003457p:plain:w480

今後時間があれば作りたい機能はいくつかあって、

  • ログイン機能
    • 自分が使う上で一番ほしい。username, token を毎回入力したくない
  • レスポンスの JSON をコピーするボタン
  • ユーザーのグラフをまとめて表示するページ
    • どちらかというとダッシュボードでは感はあるが
  • サポーター限定の機能(パラメータ)はそれとわかるようにしたい
  • ダークモード対応
    • 技術的にやってみたいというだけ

などなど。

反応があればやる気も上がると思うので、リポジトリに Star をお願いします...🙏

技術的な話

今回使った主な技術スタックは以下。

それぞれ、初めて使ってみた感想とかよくわからなかったとこを書く。

Next.js

公式ドキュメントが非常に充実しているので、まずは Learn Next.js を一通りやってその後は必要になった機能だけ個別に調べる、というやり方で十分だった。

今回は必要にならなかったけど SSG、SSR、ISR の違いとかが理解できた。

SWR

今回のようにボタン押したらリクエスト送るだけのアプリに対しては完全に too much だったとは思うものの、どういう使い勝手なのか試したくて導入した。

条件付きフェッチ – SWR
の項にもあるように、「ボタンが押されたら useSWR() する」のではなく「レンダリングのたびに常に useSWR() は実行されるので、実際に fetch するかどうか別の state で管理する」というところは
ちょっとこれまでと発想を変える必要があった。けど hooks に共通して言える考え方なんだろうな、そういう意味で hooks 時代の設計に頭が追いついてないんだろうなとも思った。

あと、恥ずかしながらしばらく fetch や axios などのライブラリの代わりとして選択できるものだと思っていて、あくまでデータの「取得」用のライブラリだということに遅れて気がついた。
ref. how to use post method and pass params in swr? · Issue #93 · vercel/swr

🤔 わからなかったこと

クエリパラメータがそこそこあるような API を叩くとき、 useSWR()key パラメータをどう指定するのがお作法的に正しいのかよくわからなかった。

引数 – SWR にあるように第一引数の key には配列を渡せるが、

const { data: user } = useSWR(['/api/user', token], fetcher)

クエリパラメータが複数あるからといってそれをオブジェクトにまとめてしまうのは NG とされている。

// NG
const { data: events } = useSWR(['/api/events', { from, to }], fetcher)

基本的にクエリパラメータが異なると別々の結果としてキャッシュしたいから key には含めておきたいと思うんだけど、
クエリパラメータが増えるごとに配列に追加することになる?

そうすると fetcher 関数の引数も増えて...って思ったけど、第二引数をこう書けばいいのか。

const { data: events } = useSWR(['/api/events', from, to], (url, from, to) => fetcher(url, { from, to })

Chakra UI

よくある UI コンポーネントライブラリの1つとして必要なものだけ使った、という感じなので、どのあたりに強みがあるのかとかはまだ理解が不十分。

Comparison - Chakra UI
ここを一回ちゃんと読んだほうがいい。

あと、Chakra UI を使えば Tailwind CSS の思想とかも理解できるかと思ったけど全然そんなことはなかった。

あわせて読みたい

React Hook Foom

フォーム項目にきめ細かいバリデーションを実装したい場合に力を発揮しそうだなと思いつつ、今回は必須項目ぐらいしかなかったのでこれも too much ですね。

また、Chakra UI のドキュメントに
Chakra UI + React Hook Form - Chakra UI
なんてページもあるぐらいだから両方組み合わせるのも余裕なんだと思ってたけどそんなことなかったです。
ラジオボタン (Radio, RadioGroup) にどう組み込むのかわからず今のところこうなっている。

<FormControl>
  <FormLabel>type</FormLabel>
  <Controller
    control={control}
    name="type"
    render={({ field: { onChange, value } }) => {
      return (
        <RadioGroup onChange={onChange} value={value}>
          <Stack spacing={4} direction="row">
            <Radio value="int">int</Radio>
            <Radio value="float">float</Radio>
          </Stack>
        </RadioGroup>
      );
    }}
  />
</FormControl>

🤔 わからなかったこと

「単純なラベルつき input + 必須バリデーション + エラー時のメッセージ表示」みたいなことをやろうとすると
こういうコードを何回も書くことになる。

<FormControl isInvalid={!!errors.username}>
  <FormLabel htmlFor="username">username</FormLabel>
  <Input
    id="username"
    type="text"
    {...register("username", {
      required: "This is required.",
    })}
  />
  <FormErrorMessage>
    {errors.username && errors.username.message}
  </FormErrorMessage>
</FormControl>

さすがにこれはめんどいってことでコンポーネントに切り出したけど、TypeScript の型が合わず何箇所か挫折してる。

type Props<TFieldValues extends Record<string, any>> = {
  name: keyof TFieldValues;
  required?: boolean;
  register: UseFormRegister<TFieldValues>;
  errors: FieldErrors<TFieldValues>;
};

export default function Input<TFieldValues>({
  name,
  required,
  register,
  errors,
}: Props<TFieldValues>) {
  return (
    <FormControl isInvalid={!!errors[name]}>
      {/* @ts-ignore */}
      <FormLabel htmlFor={name}>{name}</FormLabel>
      <ChakraUIInput
        // @ts-ignore
        id={name}
        type="text"
        // @ts-ignore
        {...register(name, {
          required: required && "This is required.",
        })}
      />
      {/* @ts-ignore */}
      <FormErrorMessage>{errors[name]?.message}</FormErrorMessage>
    </FormControl>
  );
}

これは正解がよくわからない。