私が歌川です

@utgwkk が書いている

25歳になった

7/28で25歳になりました。

2021年には退学をしました。人生の岐路ともいえるのですが、決まってからの一連の手続きは高速に済んでいきました。こうやって突き進んでいくんですね。

退学を決めた頃のプライベートな日記を見返してみると、あまりにスピード感がありすぎるとビビっている様子が伺えます。人生は決断の連続って感じですね。

25歳になるということで、25歳のアイドルのことを思い出しました。25歳のモデルケースは他にもある気がするけど真っ先に思い付いたので仕方ないです。温泉に行きたいし健康的な飲酒をやりたい。ブログには書いてなかったと思うけど、健康診断の結果のみどころは特にありませんでした。

blog.utgw.net


とりあえず欲しい本のウィッシュリストを貼っておきます。ほかにもオススメの本がありましたらウィッシュリスト外から送ってください。

読書カテゴリScrapboxにある本は、読んだことがあるか、もしくは持っています。

日常を取り戻した暁には飲みに行きましょう。


あと、Kindleで買えるオススメの百合漫画があったら教えてください。

TypeScript + JSXでrenderするコンポーネントに型引数を書ける

子ネタだしタイトルが全てだけど、おもしろかったので共有します。

たとえば、react-router (react-router-dom) v5 の Link コンポーネントto propには、文字列・オブジェクト・関数を渡すことができる。

ここで、react-router-domの型定義を見ると、<Link> コンポーネントはジェネリックな関数・インタフェースとして定義されていることが分かる。

これらを踏まえて以下のようなコードを書いてみると、コンパイルが通るし動作することが分かる。renderしたいコンポーネントに型引数を書くことができる。

type State = ...;

export const WrappedLink = () =>
  <Link<State> to={(location) => ...} />

こすると to propの関数の location 引数の型が Location<State> に推論されて便利だけど、見た目はおもしろい。関数呼び出しだと思うと、ここに型引数が書けるのはそういうものかとも思う。

2021/7/26 12:54 追記: コンポーネントのrender時に「型注釈」を渡す、と書いていたけど正しくは「型引数」だったので修正しました。

艦これリキュールを飲んで連休ラストスパートに差しかかりつつある。


catatsuyさんの社内ISUCONで26万点を取った。3年前にも解いたことがあるけどそのときよりも遥かに点数が高い。解いたことがあるとはいえ手数が増えたということだと思う。小手先テクニックも覚えている……。

blog.utgw.net

2021/7/25 追記: 27万点まで伸びた。


www.youtube.com

7月の終わりが近づいている、ということを意識した。誕生日まであと4日。

イベントハンドラを設定した要素内にreact-modalのモーダルを置くとイベントハンドラが反応する

tl;dr

イベントハンドラを設定した要素内にreact-modalのモーダルを置かないようにするのが手っ取り早そう。

イベントハンドラを設定したコンポーネント

以下の Clickable コンポーネントは、divに click イベントのハンドラを設定しており、かつchildrenを取るコンポーネントである。divをクリックするとコンソールにログを出力する。

import { FC, MouseEventHandler, useCallback } from "react";

export const Clickable: FC = ({ children }) => {
  const handleMouseDown: MouseEventHandler = useCallback((e) => {
    console.log(`clicked!!!! ${new Date()}`);
  }, []);

  return (
    <div onMouseDown={handleMouseDown}>
      {children}
    </div>
  );
};

モーダル、外に置くか? 中に置くか?

react-modalのモーダルを <Clickable> の外に置くか、それとも中に置くか、について考える。

外に置く

モーダルを <Clickable> の外に置くとこういう感じになる。モーダルを開くボタンだけが <Clickable> の中にある。とくに気にかけることもない。

<div>
  <h2>along with modal</h2>
  <Clickable>
    This area is clickable!!!!!
    <p>
      <button onClick={() => setIsOpen(true)}>open modal</button>
    </p>
  </Clickable>
  <Modal isOpen={isOpen} onRequestClose={handleRequestClose}>
    <div>
      <h2>Modal</h2>
      <button onClick={handleRequestClose}>close</button>
    </div>
  </Modal>
</div>

中に置く

一方で、モーダルを <Clickable> の中に置くとどうなるか? この場合もモーダルを開くことはできるし、モーダルが前面に出てくるのだが、モーダル内をクリックすると <Clickable> のイベントハンドラが反応してしまう。これは多くの場合は望ましい挙動ではないと思う。

<div>
  <h2>contains modal</h2>
  <Clickable>
    This area is clickable!!!!!
    <p>
      <button onClick={() => setIsOpen(true)}>open modal</button>
    </p>
    <Modal isOpen={isOpen} onRequestClose={handleRequestClose}>
      <div>
        <h2>Modal</h2>
        <button onClick={handleRequestClose}>close</button>
      </div>
    </Modal>
  </Clickable>
</div>

どうするか

モーダル内で、モーダルを設置した要素の親のイベントハンドラが呼ばれてほしくないなら、モーダルを外に出したほうがよい。

感想

普通はイベントハンドラを設定した要素の子にモーダルを設置することはないかもしれないけど、モーダルを動的に生成する場合に油断すると引っかかると思う。実際にこの現象に遭遇して、DOM treeの上ではreact-modalのモーダルは完全にroot要素の外にあるけど、Reactが管理するtreeの上では必ずしもそうなっていない*1のがデバッグを難しくしていた。CSSの pointer-events プロパティで解決できるかもしれない、と思っていろいろ試してみたけど解決できなかった。

サンプルコード

github.com

*1:React Developer Toolsで確認できる

RelayでGraphQL multipart request specificationに沿ったファイルアップロードを行う

はじめに

2021/7/17現在、GraphQL APIを通じてファイルをアップロードする方法は規定されていない。そのため、GraphQL APIでファイルアップロードを行いたい場合は何らかの工夫を行う必要がある。

GraphQL APIリクエストの変数中にファイルをエンコードして送信することで、ファイルをアップロードするという目的は達成できそうに見える。が、この方法では巨大なJSONをリクエストボディとして送信する必要があり、効率がよくない。また、各種APIサーバーがファイルアップロードのためにメモリを効率的に利用する実装*1を行っていても、その恩恵を受けづらい。

GraphQL multipart request specificationという仕様に則ると、リクエストの変数中にファイルをエンコードして送信する方法と比べて、効率的にファイルをアップロードすることができる。仕様の解説やAPIサーバー側の実装方法については以下の記事が詳しいので、この記事では割愛する。

blog.agile.esm.co.jp

この記事では、RelayでGraphQL multipart request specificationに沿ってファイルをアップロードする方法を解説する。クライアントの実装言語としてTypeScriptを用いる。

目次

スキーマ

この記事中でのスキーマは以下。Upload scalarがアップロードするファイルを表している。

type Mutation {
  uploadArtwork(input: UploadArtworkInput!): UploadArtworkPayload
}

scalar Upload

input UploadArtworkInput {
  ...

  files: [Upload!]!
}

type UploadArtworkPayload {
  artwork: Artwork
}

ファイルをアップロードするmutationを実行する

以下は2つのファイルをアップロードするmutationのコード例である。通常のmutationと同様に commitMutation 関数を使ってファイルをアップロードできるが、気をつけるべきポイントがいくつかある。

import { commitMutation } from "react-relay";

commitMutation(environment, {
  mutation: graphql`
    mutation UploadArtworkMutation($input: UploadArtworkInput!) {
      uploadArtwork(input: $input) {
        artwork {
          id
        }
      }
    }
  `,
  variables: {
    input: {
      files: [null, null],
      ...
    },
  },
  uploadables: {
    "variables.input.files.0": ...,
    "variables.input.files.1": ...,
  },
});

Upload scalarには null を渡す

mutationの入力のうち Upload 型のフィールドには、アップロードするファイルを表すオブジェクト (FileBlob など) ではなく null を渡す必要がある。ではどうやってファイルをアップロードするのかというと、後述する uploadables を使って渡す。

relay-compilerでファイルを生成する際には --customScalars.Upload null オプションを指定しておくと間違えなくて済む。

relay-compiler --schema ./schema.graphql --src src --language typescript --customScalars.Upload null

uploadables を渡す

mutationの入力中のキーに対応するアップロードするファイルのマッピングを commitMutation 関数の第2引数の uploadables フィールドに渡す必要がある。UploadableMap 型の定義は以下。

export type Uploadable = File | Blob;
export interface UploadableMap {
    [key: string]: Uploadable;
}

マッピングのキーはobject-path形式である。今回の例なら variables.input.files.0variables.input.files.1 が該当する。

複数ファイルのアップロードに対応するなら、FileList から UploadableMap に変換するヘルパー関数を作っておくと取り回しがよいと思う。

const makeUploadablesFromFileList = (
  objectPath: string,
  files: FileList
): UploadableMap =>
  Object.fromEntries<File>(
    Array.from(files, (file, i) => [`${objectPath}.${i}`, file])
  );

const uploadables = makeUploadablesFromFileList('variables.input.files', files);
// {
//   "variables.input.files.0": ...,
//   "variables.input.files.1": ...,
// }

ネットワークレイヤを実装する

Relayでは、GraphQL APIへのリクエストを行う層が抽象化されている。実装がXMLHttpRequestでもfetch APIでもよいし、なんなら単なるJavaScriptのオブジェクト操作でもよい。

今回は簡単のため、公式ドキュメントに則ってfetch APIを使うことにする。

relay.dev

ファイルアップロードを行う場合は以下のようにネットワークレイヤを実装する必要がある。

multipart form形式でHTTPリクエストを行う

通常のGraphQL APIリクエストはリクエストボディとしてJSONを送信すればよい。GraphQL multipart request specificationではmultipart form形式でリクエストを送信する必要がある。

これは単に FormData を送ればよい。

const formData = new FormData();

operations パラメータでGraphQLクエリや変数を送信する

通常のGraphQL APIリクエストでは {"query": "GraphQLクエリ", "variables": {変数}} という形式のJSONを送信すればよかった。GraphQL multipart request specificationでは operations というリクエストパラメータで、通常のGraphQL APIリクエストと同様のJSONを送信する必要がある。

formData.append(
  "operations",
  JSON.stringify({ query: params.text!, variables })
);

map パラメータで変数中のキーを送信する

map リクエストパラメータで変数中のファイルアップロードに対応するパスを送信する必要がある。

uploadable マッピングから作ると以下のようなコードになる。"variables.input.files.0": ["variables.input.files.0"] のようなフィールドを持つJSONを送信することになる。このマッピングを使って、先述した Upload 型のフィールドの null をファイルで埋めていくイメージらしい。

const map: Record<string, string[]> = {};
Object.keys(uploadables).forEach((key) => {
  if (Object.prototype.hasOwnProperty.call(uploadables, key)) {
    formData.append(key, uploadables[key]);
  }
});
formData.append("map", JSON.stringify(map));

ファイルをアップロードする

あとは FormData オブジェクトでファイルをアップロードする方法に従ってアップロードしていけばよい。 map パラメータを送信するより前にファイルを送信してはいけない*2ので、2回 Object.keys(...).forEach(...) のループを回している。

Object.keys(uploadables).forEach((key) => {
  if (Object.prototype.hasOwnProperty.call(uploadables, key)) {
    formData.append(key, uploadables[key]);
  }
});

body = formData;

完成したコード

完成したネットワークレイヤのコードは以下。通常のGraphQLクエリを発行する場合とファイルアップロードを行う場合とで分岐している。

const fetchRelay: FetchFunction = async (
  params,
  variables,
  cacheConfig,
  uploadables?
) => {
  const headers = new Headers({ Accept: "application/json" });

  let body: string | FormData;
  if (uploadables) {
    // ファイルアップロードの場合
    const formData = new FormData();
    formData.append(
      "operations",
      JSON.stringify({ query: params.text, variables })
    );

    const map: Record<string, string[]> = {};
    Object.keys(uploadables).forEach((key) => {
      if (Object.prototype.hasOwnProperty.call(uploadables, key)) {
        map[key] = [key];
      }
    });
    formData.append("map", JSON.stringify(map));

    Object.keys(uploadables).forEach((key) => {
      if (Object.prototype.hasOwnProperty.call(uploadables, key)) {
        formData.append(key, uploadables[key]);
      }
    });

    body = formData;
  } else {
    // 通常のクエリの場合
    headers.append("Content-Type", "application/json");

    body = JSON.stringify({
      query: params.text,
      variables,
    });
  }

  const response = await fetch("http://localhost:3000/api/graphql", {
    method: "POST",
    headers,
    body,
  });

  return await response.json();
};

サンプルコード

実際にRelayでGraphQL multipart request specに従ってファイルアップロードを行うサンプルアプリケーションを作って公開した。簡単なファイルアップローダーで、最低限の機能を備えている。記事中のスキーマとは異なるけど実装の簡略化のためにそうなっている。アップロードされたファイルの情報をオンメモリに記録しているので、アプリケーションを再起動すると吹っ飛ぶ。

github.com

おわりに

この記事では、RelayでGraphQL multipart request specificationに沿ったファイルアップロードを行う際の実装方法を解説した。また、GraphQL API経由でファイルアップロードを行うサンプルアプリケーションを公開した。

この記事を読んでも分からなかったところについては、実際にコードを動かしたり弄ったりして確かめられるとよさそう。

*1:アップロードされた巨大なファイルの内容を直ちにメモリに確保するのではなく、一時ファイルに退避するなど

*2:サーバーの実装によっては順序が違ってもよい場合があるけど、specでは順序が決まっているので従うべき