Panda Noir

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

reactのイベントハンドラーにはasync関数を設定してもよい

意外と知られてないというか、なんかあんまやってる人いない気がしたので

(ちなみに、本当に async 関数が必要なパターンは実は少なくて、ほとんどの場合はアンチパターンなのでやめたほうがよいです。 別の方法で実装できるならそちらにしましょう)

実際にasync関数を設定する例がこちらになります (myConfirm は confirm の promise 版)

const button = () => (
  <button onClick={async () => {
    if (await myConfirm('本当に削除しますか?')) {
      deleteItem();
    }
  }}>
    delete
  </button>
);

こんな感じで、async/await をそのまま設定することもできます。

日報をすぐ書く技術

追記: よりよい方法を思いついたので、ver.2を書きました

以下は追記前の本文です


新卒で入ってからずっと業務の日報を個人的につけてます。日報があると便利なことは多くて、たとえば連休前になにをやっていたのかを思い出したりするときに便利です。

ほぼ毎日日報をつけるには、とにかく書き始めるまでのハードルを下げるのが大事です。僕は memo コマンドを叩くとすぐ日報が書けるようにしています。今回はそのコマンドについて紹介します。

日報を即書ける memo コマンドの仕様

memo コマンドの仕様は以下のとおりです。

  • memo コマンドを叩くと ~/Documents/daily-report/{今年}/{今月}.md が vim で開かれる
  • もし日報ファイルがなければ作成してから vim で開く
  • 日報ファイルを作成する際、 # 2024/06/18(火) のように平日すべての見出しをつくる
  • 開いたとき、今日の見出し位置にカーソルが当たるようにする (さらに、マークもつける)

実際に叩くとこんな感じになります。

memo コマンドの実装

shell command はこんな感じです

memo() {
  local dailyDir="$HOME/Documents/daily-report"
  if [ $# -eq 2 ]; then
    local yyyy=$1
    local mm=$(printf "%02d" $2)
    nvim "$dailyDir/$yyyy/$mm.md"
    return
  fi
  if [ $# -eq 1 ]; then
    local yyyy=$(date +%Y)
    local mm=$(printf "%02d" $1)
    nvim "$dailyDir/$yyyy/$mm.md"
    return
  fi

  local current_daily_file="$dailyDir/$(date +%Y/%m.md)"
  if ! [ -f "$current_daily_file" ]; then
    # 日記ファイルがまだないとき、ファイルを作る
    mkdir -p "$dailyDir/$(date +%Y)"
    node "$dailyDir/create.js" $(date +%-m) >$current_daily_file
  fi

  todays_position="$(cat $current_daily_file | rg "$(date +%Y/%-m/%-d)\(" --line-number | cut -d':' -f1)"
  if [ -z "$todays_position" ]; then
    # なぜか今日の日付が見当たらないときはそのまま開く
    nvim "$current_daily_file"
  else
    # 1. 今日の位置にnマークをつける
    # 1. 昨日の位置にyマークをつける
    # 1. 今日の位置に移動する
    nvim "$current_daily_file" \
      -c "normal ${todays_position}G" \
      -c 'normal mn' \
      -c "/^# $(date +%Y)" \
      -c 'normal my' \
      -c 'let @/=""' \
      -c 'normal `n'
  fi
}

create.js

/**
 * @file 日報を生成するプログラム。
 * node create.js [month] [year]
 * month, year は省略可能。指定しなかった場合、来月の日報が出力される
 */

const today = new Date();

const [
  _command,
  _file,
  month = today.getMonth() + 2,
  year = today.getFullYear(),
] = process.argv;
const date = (d) =>
  new Date(parseInt(year, 10), parseInt(month, 10) - 1, d, 0, 0, 0, 0);

const res = [];
for (let i = 1; i <= 31; i++) {
  const d = date(i);
  if (date(1).getMonth() != d.getMonth()) break;
  if (d.getDay() === 0 || d.getDay() === 6) continue;
  res.push(
    `# ${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}(${
      '日月火水木金土'[d.getDay()]
    })`
  );
}
res.reverse();
console.log(res.join('\n'));

zshのプロンプトに直前のコマンドの終了ステータスを表示する

zshのprompt内で%?を指定すると直前の終了ステータスを表示させることができます。

PROMPT='[%n@%m]%?# '

↑これで[user@host][last_status]#というプロンプトが表示されます。

ただ、これだと正常終了のときにも表示されて煩わしいので、正常終了のときには表示させないようにします。その結果がこちらです。

local user_host='%F{074}[%n@%m]'
local last_status='%F{red}$([[ $? -ne 0 && $? -ne 130 ]] && echo "[%?]")' # 異常終了のとき(Ctrl-cを除く)にステータスを表示する
PROMPT="$user_host$last_status%f# "

ついでに直前がctrl-cのときも表示しないようにしています。結構僕は押しがちなので…

適用するとこんな感じになります。

余談: ctrl-cのときを除外しないなら

ちなみに、ctrl-cのときにも終了ステータスを表示させて良いのであればもう少し簡潔に書くことができます。

local user_host='%F{074}[%n@%m]'
local last_status='%F{red}%(?..[%?])' # 異常終了のときにステータスを表示する
PROMPT="$user_host$last_status%f# "

参考

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>
  );
};