2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React 19@beta】PokeAPIを使って新機能のuse APIを動かしてみる

Last updated at Posted at 2024-05-19

概要

来ましたねReact 19 Beta!

個人的に一番気になっていたReact Compilerの方はまだ発表がなかったので残念でしたが、前々からRFCにて議論されていたuse APIについて、本記事では実装を交えて紹介していきます。

(執筆中にReact Compilerについての発表があったので早くも時代遅れ情報になりましたが悔しいのでそのままにします。)

そもそもuse APIってなぁに?

PromiseやContextを引数に取り、中身をサスペンドしながら持ってきてくれます。
Contextの方はuseContextの代わりにこれからuseを使いましょうくらいのやつ(公式参考)なので肝であるPromiseの方の話をします。
このサスペンドしながらというのがミソで、親コンポーネントがSuspenseで囲まれていたら、Promiseの処理中は自動でfallbackに渡したReactNodeを描画してくれます。
文字文字してもあれなのでコード例を示すと、

const fetchHoge = async () => {
  const res = await fetch("hoge");
  return res.json();
};

const Sample = () => {
  const hoge = use(fetchHoge());
  return (
    <>
      <p>{hoge}</p>
    </>
  );
};

function App() {
  return (
    <>
      <Suspense fallback={<div>loading...</div>}>
        <Sample />
      </Suspense>
    </>
  );
}

こんな感じで、Sampleコンポーネントでごちゃごちゃせずにスッキリ書くことができます。
これからもずっとCSR前提で書きますが、今までだと、レスポンス用のstateを適当に初期化しつつ用意して、useEffectの中でfetchしてstateに詰め込んで、その間はstate見てローディング中を考慮したコンポーネントを出しわけて、fetchが完了したらそのデータを表示して、、、みたいなことをしていたと思います。
コードに起こすと以下のような感じです。

const Sample = () => {
  const [data, setData] = useState(null);
  useEffect(() => {
    const req = async () => {
      const res = await fetch("hoge");
      const resObj = await res.json();
      setData(resObj);
    };
    req();
  }, []);

  if (!data) {
    return <div>loading...</div>;
  }
  
  return (
    <>
      <p>{data}</p>
    </>
  );
};

多分みんなこう書きたくないんで既にTanStack QuerySWRを使ってるんだと思いますがその人たちはuseを使わないでも幸せそうなので良いですね。

他にもuseの特徴として、トップレベルで呼ばないといけないであったり、フックの呼び出しの順序が不変でないといけない(条件分岐の中で呼ぶとかダメだよって話)というhooksのルール(公式参考)がuse APIには一部適用されないという、中々に面白い特徴を持ち合わせているのでぜひ調べてみてください。
これで例えば何が嬉しいのかというと、どっちが良いのか・スマートなのかは置いといて、TanStack QueryのuseQueryにenabled=falseを渡して初回レンダリング時にはfetchさせないみたいなのを条件分岐することによってそんなのしなくても良くなります。

参考リンク①: React19で導入されるuse APIについての紹介

参考リンク②: use APIリファレンス

参考リンク③: use API解説記事

実際に手を動かしてみる

環境作りつつ、実際に動くコードを書いてみます。

create app

肉まん×Viteで構築します。
bunがまだPCにないよって方は以下のコマンドにてインストールできるのでやってみてください。

$curl https://bun.sh/install | bash
(zshの人はこれも)$exec /bin/zsh

それではアプリ作成コマンドを入力していきます。

$bun create vite sample-app
? Select a framework: › - Use arrow-keys. Return to submit.
    Vanilla
    Vue
❯   React
    Preact
    Lit
    Svelte
    Solid
    Qwik
    Others

? Select a variant: › - Use arrow-keys. Return to submit.
    TypeScript
❯   TypeScript + SWC
    JavaScript
    JavaScript + SWC
    Remix ↗

こんな感じでReact + TypeScript + SWCを選択します。

作成したフォルダに移動して、bun i(bun install)コマンドを入力してパッケージをインストールしたら準備完了です。
早速bun devを実行して起動したら、ターミナルに表示されているローカルホストのリンクにアクセスしてみます。

QuickTime movie.gif

準備完了です。

React19@betaをインストール

公式のアップグレードガイドを参考にインストールします。

$bun install react@beta react-dom@beta

これでベータ版であるReact19が入ります。余談ですが、bun installはbun addと全く同じらしいです。

そして、TypeScriptを使用しているため、以下の設定もpackage.jsonに追記してあげます。

{
  "dependencies": {
    "@types/react": "npm:types-react@beta",
    "@types/react-dom": "npm:types-react-dom@beta"
  },
  "overrides": {
    "@types/react": "npm:types-react@beta",
    "@types/react-dom": "npm:types-react-dom@beta"
  }
}

一個前の手順でReact19@betaをインストールした時点で、dependenciesにreactとreact-domは入っているはずですので、oberridesの部分だけ追記してあげれば大丈夫です。
これでuse apiがtsに怒られずにインポートできるようになります。

コード書く

まず作成したものを示し、そこから軽く解説していきます。
手元で動かしてみたい方はApp.tsxにマルっとコピペしてください。

App.tsx
import { Suspense, use, useState } from "react";
import "./App.css";

const getPokemonData = async (pokemon: string) => {
  const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`);
  return response.json();
};

const Pokemon = () => {
  const [promise, setPromise] = useState(() =>
    getPokemonData("pikachu")
  );

  const fetchPokemon = () => {
    const randomPokemonId = Math.floor(Math.random() * 151) + 1;
    setPromise(getPokemonData(`${randomPokemonId}`));
  };

  const pokemonData = use(promise);

  return (
    <>
      <img
        src={pokemonData.sprites.other["official-artwork"].front_default}
        alt={pokemonData.species.name}
      />
      <p>{pokemonData.species.name}</p>
      <button onClick={fetchPokemon}>fetch</button>
    </>
  );
};

function App() {
  return (
    <>
      <Suspense fallback={<div>loading...</div>}>
        <Pokemon />
      </Suspense>
    </>
  );
}

export default App;

順番前後しますが一番下のAppコンポーネントからです。
これは中身をごそっと削除し、今回作成したPokemonコンポーネントをSuspenseで囲っただけとなります。use APIの恩恵を確かめるためにSuspenseで囲む必要があります。

次に、Pokemonコンポーネントで使用されるgetPokemonData関数です。
これはPokeAPIのv2にfetchリクエストを送るPromiseを返します。純粋な書き方であればここらへんでasync/awaitしてPromiseオブジェクトの中身を取り出しながらreturnするといった実装になると思いますが、use APIが使える今、それはしません。エラーハンドリングは忘れてください。

最後にPokemonコンポーネントです。PokeAPIから返却されるポケモンの画像データと名前をランダムに表示させます。
Promiseオブジェクトを保持するStateを持ち、そのPromiseオブジェクトで初期化されているStateをuse APIに渡しています。
そうすることで、pokemonDataを参照しているコンポーネントの場合自動的にサスペンドされるという訳です。
buttonエレメントにはランダムに生成された1から151までの数字をPokeAPIへのリクエストURLに乗せることでポケモンを適当に取得する関数をonClickに渡しています。

こうすることで、初回レンダリング時にはピカチューが現れて、あとはカントー地方のポケモンが現れるようになります。
QuickTime movie.gif

svgの削除とか全くしてない無秩序ですが、今回のコードがあるリポジトリです。一応...

結論

正直まだfetch系の処理はそのライブラリで良い感が否めないけどパウワウは可愛かった。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?