かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

リストの中から特定の index の前後を n 件の範囲のデータを取得したい。

Pagination みたいにリストの中から常に n 件の値を取得する方法を考えてみた。(車輪の再発明)

要件

  • リストが n 件以上ある時は 常に n 件 表示する
  • current を中心として前後に n / 2 件づつの範囲を取る
  • 偶数件の範囲を取る場合は後ろの方を 1 件多くする
  • current が前後に n / 2 件取れない場合は先頭から又は最後から n 件取得する

current を中心に n 件 の範囲をとる

偶数件の範囲を取る場合は後ろを 1 件多くする

current を中心にできない場合は先頭から n 件の範囲に

current を中心にできない場合は最後から n 件の範囲に

実装イメージ

完成イメージ

Sample

See the Pen Untitled by KIKIKI (@kikiki_kiki) on CodePen.


current の index から開始と終了の index を求める

リストから抽出するので Array.slice() を使う想定で開始 start と 終了 end (範囲の index + 1) を取得できれば良い

type GetSliceRangeProps = {
  itemLength: number;
  currentIndex: number;
  displayNum: number;
};

const getSliceRange = ({
  itemLength,
  currentIndex,
  displayNum
}: GetSliceRangeProps): {
  start: unumber;
  end?: number;
} => {
  // 表示数がリストより多い場合は全件表示する
  if (displayNum > itemLength) {
    return {
      start: 0,
    };
  }
  // current の前後に表示する要素数
  // 表示数が偶数の場合、前が 1つ少なくなるので Math.floor で少ない数に合わせる
  const half = Math.floor((displayNum - 1) / 2);

  // current が前に必要な数に満たない場合は、先頭から表示件数分を表示する
  if (currentIndex < half) {
    return {
      start: 0,
      end: displayNum,
    };
  }

  // currentIndex + 後ろに必要な数 がリスト長以上になってしまう場合はリストの後ろから表示件数分を表示する
  // index は 0 からで、リストの要素数 length は 1 からのカウントなのでリストの最後の要素の index と比較する
  const lastItemIndex = itemLength - 1;
  if (currentIndex + half >= lastItemIndex) {
    return {
      start: itemLength - displayNum
    };
  }
  
  const start = currentIndex - half;
  const end = start + displayNum;
  
  return {
    start,
    end,
  };
};

// 使い方
const {start, end} = getSliceRange({
  itemLength: MyArray.length,
  currentIndex,
  displayNum
});

MyArray.slice(start, end);

index と length 混ざってると混乱しがち。
おわり


[参考]

React Tailwind CSS v3 クラス名を動的に作ったらクラスが適応されない?にハマる

React で Tailwind CSS v3.x を使っていて変数で動的に Tailwind CSS のクラス名を作成したらスタイルが適応されなかったのでメモ

環境

  • react@18.1.0
  • typescript@4.6.4
  • tailwindcss@3.1.2

動的にサイズを指定できる Spinner を作りかった。

<svg role="status" class="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M100 … 50.5908Z" fill="currentColor"/>
  <path d="M93.9676 … 39.0409Z" fill="currentFill"/>
</svg>

このコンポーネントのサイズは <svg> タグの w-{n} h-{n} クラスを変更すれば良いっぽい

🙅 動作しない例

import { FC } from 'react';

type SizeType = 'xs' | 'sm' | 'md' | 'lg';
type SpinnerProps = {
  size?: SizeType;
};

const getSize = (size: SizeType): number => {
  switch (size) {
    case 'xs': { return 4; }
    case 'sm': { return 6; }
    case 'md': { return 8; }
    case 'lg': { return 10; }
    default: { return 8; }
  }
};

const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
  const n = getSize(size);
   return (
    <svg
      role='status'
      className={`inline w-${n} h-${n} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
      viewBox='0 0 100 101'
      fill='none'
      xmlns='http://www.w3.org/2000/svg'
    >
      <path d='M100 ... 50.5908Z' fill='currentColor' />
      <path d='M93.9676 … 39.0409Z' fill='currentFill' />
    </svg>
  );
};

export const App: FC = () => {
  return (
    <Spinner size="md" />
  );
};

<Spinner /> コンポーネントに付けられる w-6, h-6 のスタイルがそもそも存在しない状態でした…

🙆 問題ない例

type SizeType = 'xs' | 'sm' | 'md' | 'lg';

type SpinnerProps = {
  size?: SizeType;
};

const getSizeClass = (size: SizeType): string => {
  switch (size) {
    case 'xs': { return 'w-4 h-4'; }
    case 'sm': { return 'w-6 h-6'; }
    case 'md': { return 'w-8 h-8'; }
    case 'lg': { return 'w-10 h-10'; }
    default: { return 'w-8 h-8'; }
  }
};

const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
  const sizeClass = getSizeClass(size);
   return (
    <svg
      role='status'
      className={`inline ${sizeClass} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
      viewBox='0 0 100 101'
      fill='none'
      xmlns='http://www.w3.org/2000/svg'
    >
      <path d='M100 ... 50.5908Z' fill='currentColor' />
      <path d='M93.9676 … 39.0409Z' fill='currentFill' />
    </svg>
  );
};

Tailwind CSS で定義されているクラス名そのものを動的に作るのではなく、存在しているクラス名の文字列が変数で渡される場合は問題なくスタイルが適応された

Tailwind CSS の賢い Migrating to the JIT engine との相性の問題

The new Just-in-Time engine we announced in March has replaced the classic engine in Tailwind CSS v3.0.
The new engine generates the styles you need for your project on-demand, and might necessitate some small changes to your project depending on how you have Tailwind configured.
cf. Upgrade Guide - Tailwind CSS

Tailwind CSS v3.0 からデフォルトで Just-in-Time engine が搭載されており、プロジェクトに必要なスタイルだけを動的に生成するようになっている

In tailwind you can't use dynamic class naming like bg-${color}, though we can mock it to be that, but it is not preffered. Because Tailwind compiles its CSS, it looks up over all of your code and checks if a class name matches.
cf. reactjs - Tailwind not working when using variables (React.js) - Stack Overflow

つまり、Tailwind CSS v3.0 から不要なクラスをバインドしないように、プロジェクトで使われているクラスに該当するスタイルだけを生成するようになった。スタイルの生成方法が恐らくコードを静的解析し Tailwind CSS のクラス名とマッチするものがあれば、そのスタイルを出力する仕組みになっている。

なので、Tailwind CSS のクラス名そのものを変数を使って動的に作成すると解析時にマッチせずスタイルが出力されない。ということっぽい!

  • 🙅 NGだった例 w-{n} は Tailwind CSS のクラス名とマッチしないので w-4, w-6 といったスタイルが一切生成されない
  • 🙆 OK だった例 はコード中に w-4, w-6, w-8 という文字列があるので、実際にコンポーネントで使っているのが w-6 だけだったとしても、使ってない w-4, w-8 のスタイルも出力される

という挙動になっていたのが理由だったようです。

utility ファーストな設計なので、使ってないクラスも全て読み込むと巨大な CSS を読み込むことになるのでいい感じにしてくれる仕組みのお作法を理解してなかったのが原因でした。
ドキュメントがうまく探せずなんでや〜ってなってました (ᐡ o̴̶̷̤ ﻌ o̴̶̷̤ ᐡ)

おわり


[参考]

しっぽと風…と言えば、みんな大好き「狼と香辛料」ですよね!!!

Vite React + MSW モックサーバーへのリクエストが `net::ERR_FAILED` になる件

Vite で作成した React (TypeScript) のプロジェクトで MSW (Mock Service Worker) を使って開発環境で API へのリクエストにモックを返すようにしたいと思っていたのですが React からのリクエストがことごとく net::ERR_FAILED [MSW] Failed to mock a "POST" request になってしましました。

問題を切り分ける

I think it's the error message that's confusing.

 return event.respondWith(
   handleRequest(event, requestId).catch((error) => {
     console.error(
       '[MSW] Failed to mock a "%s" request to "%s": %s',
       request.method,
       request.url,
       error,
     )
   }),
 )
While this wording makes sense in the context when there's an error during the mocked response, it's misleading when the error occurs during the original (bypassed) response.
cf. Unhandled request throw error: [MSW] Failed to mock a "POST" request to · Issue #841 · mswjs/msw · GitHub

今回 GraphQL で MWS を使っていたのですが、MSW は 該当するハンドラーがなかった場合も net::ERR_FAILED を返すようだったので、確実にハンドラがあることが分かる REST でエンドポイントを作成して GraphQL の呼び出しに問題があったのか、呼び出し以外に問題があるのかを切り分けます。

サンプルにあるようなシンプルな構成に変更する

問題が複雑なときは極力シンプルにせよ。古のインターネッツ(フォーラム)で教わった教えです。

src/mock/worker.ts

import { rest, setupWorker } from 'msw'

export const worker = setupWorker([
  rest.get('/test', (req, res, ctx) => {
    return res(
      ctx.json({ success: true }),
    );
  });
]);

呼び出す側も極力シンプルにします
src/App.tsx

import { useQuery } from 'react-query';

const fetcher = async () => {
  return await fetch(`${API_URL}/test`, {
    method: 'GET',
     headers: {
      'content-type': 'application/json;charset=UTF-8',
    },
  })
    .then((res) => {
      console.log('success fetch', res);
      return res.json();
    });
}

export const App: FC = () => {
  const { data, error } = useQuery('test', fetcher);
};

${API_URL}/test に GET リクエストを送ればどちらに問題があったかが判断できます。

  1. {'success': true} が返ってきたら GraphQL の呼び出し方に間違いがあった
  2. リクエストが net::ERR_FAILED なら MWS へのリクエストそのものに何かしらの問題がある

結果としては ${API_URL}/test への GET メソッドも net::ERR_FAILED になっていたので、リクエストの際に問題が発生している事が原因でした。

Vite の設定で CORS の問題を修正する

net::ERR_FAILED のエラーは CORS の問題の際にも表示されます。
今回は Vite で起動している dev server の localhost:3000API_URL のクロスオリジンの問題でブラウザが CORS のエラー返していることが原因でした。

cf. React SPA docker 上の API にアクセスで CORS が出た!! にハマる - かもメモ

CORS を許可する設定を追加する

server.cors
Type: boolean | CorsOptions
Configure CORS for the dev server. This is enabled by default and allows any origin. Pass an options object to fine tune the behavior or false to disable.
cf. Configuring Vite | Vite

Production の CORS の設定はサーバーサイドで行われている想定なので開発用 dev server では明示的にこのオプションを true にしておけば localhost での CORS の問題が解消されそうです。

vite.config.ts に次のオプションを追加

export default defineConfig({
  server: {
    open: true,
+   cors: false,
  },
  plugins: [react()],
});

これで再度サーバーを起動して ${API_URL}/test に GET メソッドを投げたら無事レスポンスを受けることができました!
これで開発環境で MSW を扱えるようになりました!!

今回は慣れてない GraphQL の呼び出しが原因なのかどうかの部分でハマってしまいました。
はやり問題をシンプルにして切り分けるのが大事。急がば回れ!を体感したのでした。

₍ ᐢ. ̫ .ᐢ ₎ おわり


[参考]

ハマったら問題を切り分けような…