Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

ApolloClient でカスタムフェッチを使う

いっつも書き方を調べてるのでここにメモを残しておく。

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  HttpLink,
  from,
} from '@apollo/client';

const uri = 'https://flyby-router-demo.herokuapp.com/';
const client = new ApolloClient({
  uri,
  cache: new InMemoryCache(),
  link: from([
    new HttpLink({
      // ↓ここでカスタムフェッチを実装する
      fetch: async (uri, options) => {
        await new Promise((r) => setTimeout(r, 1000));
        return fetch(uri, options);
      },
    }),
  ]),
});

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </StrictMode>,
);

結論: link の部分を追加すればよい。

link のとこをコメントアウトすれば普通の状態に戻せる。

ちなみに urql の場合は fetch オプションがあるのでもっと簡単。

const client = new Client({
  url,
  exchanges: [cacheExchange, fetchExchange],
  fetch: async (...args) => {
    await new Promise((r) => setTimeout(r, 1000));
    return fetch(...args);
  },
});

シンプルにToastを実装する in react

使い方

使いたい箇所より上のコンポーネントに <ToastProvider> を追加して(↓こんな感じ)、コンポーネント内で const toast = useToast(); で使う感じです。

createRoot(document.getElementById('root')!).render(
  <ToastProvider>
    <App />
  </ToastProvider>
);
const App = () => {
  const toast = useToast();
  return <button onClick={() => toast('clicked!')}>toast</button>
};

実装

import clsx from 'clsx';
import { PropsWithChildren, createContext, useContext, useState } from 'react';

const ToastContext = createContext<(message: string) => void>(() => {});

export const useToast = () => useContext(ToastContext);

export const ToastProvider = ({ children }: PropsWithChildren) => {
  const [showsToast, setShowsToast] = useState(false);
  const [fadesOutToast, setFadesOutToast] = useState(false);
  const [toastMessage, setToastMessage] = useState('');
  let toastHideTimerId: number | undefined = undefined;

  const toast = (message: string) => {
    clearTimeout(toastHideTimerId);
    setToastMessage(message);
    setShowsToast(true);
    toastHideTimerId = window.setTimeout(() => {
      setFadesOutToast(true);
    }, 3000);
  };

  return (
    <ToastContext.Provider value={toast}>
      {children}
      {showsToast && (
        <div
          className={clsx('toast', {
            ['opacity-0 transition-all duration-300']: fadesOutToast,
          })}
          onTransitionEnd={() => {
            if (!fadesOutToast) {
              return;
            }
            setShowsToast(false);
            setFadesOutToast(false);
          }}
        >
          <div className="alert">
            <span>{toastMessage}</span>
          </div>
        </div>
      )}
    </ToastContext.Provider>
  );
};

access(obj, 'foo.bar.baz') みたいにパスを指定してアクセスしたい

発端: ApolloClient の useQuery の data と error をいい感じに扱うために getOrThrow(data, error, 'path.to.field') みたいなユーティリティ関数が欲しくなった

欲しい関数

const {loading, error, data} = useQuery(query);
const fooBar = getOrThrow(data, error, 'foo.bar');
const fooBarBaz = getOrThrow(data, error, 'foo.bar.baz');

こんな感じで、foo.bar でエラーが起きてたらエラーが throw され、エラーがなければ data.foo.bar が返ってくるという関数です。

実装:

さっそく実装を載せます(throw までつけると長くなるので、get に特化した実装を載せてます)。

ちなみに type-challenges の Object Key Paths の解答コードを借りました (こちらの解答)。

// cf. https://github.com/type-challenges/type-challenges/issues/7939
type ObjectKeyPaths<
  T extends object,
  P extends string = '', // prefix
  K extends keyof T = keyof T,
> =
  K extends string ?
    | `${P}${K}`
    | (T[K] extends object ? `${P}${K}${ObjectKeyPaths<T[K], '.'>}` : never)
  : never;

type Access<T extends object, U> =
  U extends `${infer x extends keyof T & string}.${infer xs}` ?
    T[x] extends object ?
      Access<T[x], xs>
    : T[x]
  : U extends keyof T ? T[U]
  : never;

const access = <T extends object, U extends ObjectKeyPaths<T>>(
  object: T,
  path: U,
): Access<T, U> =>
  path.split('.').reduce((acc, name) => (acc as any)[name], object) as any;

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript

型を無理くりつけてるため実装に any を使ってます。これはもう仕方ないので、テストで保証すれば OK って方針にしました。

コマンドラインの内容を vim で編集できるようにする

zsh で control-o を押すと vim が立ち上がってコマンドラインの内容を編集できる関数を(ChatGPTが)作ったので紹介します。

(要件を伝えたら ChatGPT が目当てのものを一発で生成したので、かなりビックリしました。すごい。)

デモ動画

youtu.be

デモ動画では、コマンドラインの「vim ~/.config/zsh/.zprofile.local」の .zprofile.local を .zshrc.local に変えています。僕は vimmer なので、emacs キーバインドを使うより格段に早く修正できています。

実際のスクリプト

実際のスクリプトがこちら↓

# コマンドラインをエディタで編集する関数
edit-command-line() {
  # 現在のコマンドラインを一時ファイルに書き出す
  local tmpfile=$(mktemp)
  print -rl -- $BUFFER > $tmpfile

  $EDITOR $tmpfile < /dev/tty

  # エディタが正常に終了した場合、一時ファイルの内容でコマンドラインを置き換え
  if [[ $? -eq 0 ]]; then
    BUFFER=$(<$tmpfile)
    zle reset-prompt
  fi

  rm -f $tmpfile
}

zle -N edit-command-line
bindkey "^O" edit-command-line
  1. control-O を押す
  2. エディタが立ち上がる
  3. 編集して保存する
  4. エディタを終了する
  5. 編集内容がコマンドラインに反映される

なお、エディタを正常に終了しないと変更内容は破棄されます。たとえば vim なら :cq で終了するとコマンドラインは変更されません。

これ、zsh の vim モードで良いのでは?とも思うんですが、zsh の vim モードは絶妙に使いづらいんですよね… デフォルトがインサートモードである、プラグインが使えないあたりが個人的に致命的でした。

zellij のなかで neovim を開くと色がおかしくなる

結構試行錯誤したのでメモ

↑こんな感じで、結構色が違って表示されたので調べて修正しました。

結論

なぜか neovim の background オプションが "light" になってたのが原因でした。 set background=dark を明示的に書いたら解決しました。

調べたこと

  1. zellij を起動してない状態と起動した状態で比較してみる → zellij のなかでのみ問題が起きる 1.別のターミナルで zellij を起動してチェックしてみる → ターミナルを変えても同じ
  2. TrueColor は動くか? → zellij のなかでも問題なし
  3. system color は同じか? → 同じ
  4. bat など他コマンドの出力はどうか? → 問題ない

と、このように順番に原因を絞りました (zellijの起動に原因がありそう → 特定のターミナルと zellij の相性が原因ではなさそう → 色がおかしいわけではない → zellij が neovim になにか影響をしていてそれが原因っぽい)

ここからさらに neovim をプラグインなしで開いてもダメなことがわかったのでneovim 自体になにかあるんだと判明 → 最終的に background オプションに原因があることがわかりました。