😑

「Reactに有利なベンチマークを作ってみた」にJotai実装を追加してみた

2022/07/13に公開

https://qiita.com/uhyo/items/35cb243557df5e1a87fc

JotaiもReactと変わらないので、ベンチマークで競いたいと言うよりは書き心地を試すのが目的。あと、ベンチマークで大きく差が出ないことを確認するのも別の目的。

コード

react-jotai/src/components/Item/index.tsx
import { atom, useAtomValue, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { itemMap } from "../../data/items";
import classes from "./Item.module.css";

const searchQueryAtom = atom("");

export const useSetSearchQuery = () => {
  const setSearchQuery = useSetAtom(searchQueryAtom);
  return setSearchQuery;
};

export const itemFamily = atomFamily((id: string) => atom((get) => {
  const searchQuery = get(searchQueryAtom);
  const item = itemMap.get(id);
  if (!item) {
    throw new Error(`no item found for ${id}`);
  }

  const name = item.en.toLowerCase();
  if (!searchQuery) {
    return {
      displayName: item.ja,
      nameMarked: <span className={classes.name}>{item.en}</span>,
    };
  }
  const searchIndex = name.indexOf(searchQuery);
  if (searchIndex === -1) {
    return {
      displayName: item.ja,
      nameMarked: (
        <span className={`${classes.name} ${classes.unmatchedName}`}>
          {item.en}
        </span>
      ),
    };
  }
  return {
    displayName: item.ja,
    nameMarked: (
      <span className={classes.name}>
        {item.en.substring(0, searchIndex)}
        <mark>
          {item.en.substring(searchIndex, searchIndex + searchQuery.length)}
        </mark>
        {item.en.substring(searchIndex + searchQuery.length)}
      </span>
    ),
  };
}));

type Props = {
  /**
   * ID of Item
   */
  id: string;
};

export const Item: React.FC<Props> = ({ id }) => {
  const { displayName, nameMarked } = useAtomValue(itemFamily(id));
  return (
    <div className={classes.wrapper}>
      <div className={classes.id}>{id}</div>
      <div>{nameMarked}</div>
      <div>{displayName}</div>
    </div>
  );
};
react-jotai/src/App.tsx
import { useCallback, useState, useTransition } from "react";
import classes from "./App.module.css";
import { Item, useSetSearchQuery } from "./components/Item";
import { SearchBox } from "./components/SearchBox";
import { itemMap } from "./data/items";

function App() {
  const [input, setInput] = useState("");
  const setSearchQuery = useSetSearchQuery();
  const [, startTransition] = useTransition();
  const onChange = useCallback((input: string) => {
    setInput(input);
    startTransition(() => {
      setSearchQuery(input.toLowerCase());
    });
  }, []);

  return (
    <>
      <div className={classes.pokemonList}>
        {Array.from(itemMap.keys()).map((id) => {
          return <Item key={id} id={id} />;
        })}
      </div>
      <footer className={classes.footer}>
        <p>
          Data is obtained from{" "}
          <a href="https://pokeapi.co/" rel="external">
            PokéAPI
          </a>
          .
        </p>
      </footer>
      <div className={classes.searchBox}>
        <SearchBox input={input} onChange={onChange} />
      </div>
    </>
  );
}

export default App;

結果

ベンチマーク結果はreactとreact-jotaiでほぼ変わらず。Jotaiの方が多少のオーバヘッドあるのか、描画が追いついていないことがある。これって多少のオーバヘッドがあっても、startTransitionの効果でレンダリングがスキップしちゃうだけなので、ベンチマークの数値は変わらないのかも。

おわりに

特にオチはないけれど、書いたコードを消すのはもったいない精神で載せてみた。

ちなみに元のコードがuseMemoにJSX Element入れていたので、形をそのままにatom化したけど、この書き方はそれほど一般的ではない。しかし、テクニックとしてはあり 👇

https://twitter.com/dai_shi/status/1468875799327817730

Discussion