🫠

ReaSemi(Vol.4) 【fetch,promise,useEffect編】

2024/08/20に公開4

今回の内容

今回は「fetch」と「useEffect」の使い方と解説を行います。
「Reactわかんないよ~😭」という方向けですが、理解している方も復習目的で覗いてみてはいかがでしょうか?

ちなみに今回の記事はりあゼミ内で説明できなかった非同期処理とPromise、useEffectの使い方についての解説も載せています。
「この前聞いたよ~😗」という方でも新しい学びがあるかもしれません。

①fetchを理解しよう!

■fetchってなあに?

正確にはFetchAPIと呼ばれるJavaScriptのAPIの1つです。
普段話題に出る「fetch」は「FetchAPIの中のfetch()メソッド」のことと思っていいでしょう。
fetchは日本語で「連れてくる、取ってくる」といった意味で、文字通りサーバーからデータを取得する場合に使用します。
実は取得だけでなくサーバーにデータを送信する場合にも使用するので、作るアプリによっては重要なメソッドです。

■特徴

・非同期処理でサーバーと通信ができる
 「非同期処理」ってなんだ?🤔となるかもしれませんが、
 今は「ロード時間をなくすための仕組み」と思ってください。
・promiseを返す
 これもまた「promise」ってなんだ?😠となりますね…。
 こちらは「非同期処理をきれいに書くための仕組み」と思ってください。

上記2つは後に解説いたしますのでご安心してください!

■とにかく使ってみよう!

ああだこうだと説明されるより、実際に使ってみた方が頭に入ります。
早速基本的な使い方を見ていきましょう!
今回は猫の画像をランダムで返してくれる「The Cat API」を使用します。
https://docs.thecatapi.com/

  const getCatUrl = () => {
    fetch("https://api.thecatapi.com/v1/images/search")
      .then((res) => {
        return res.json();
      })
      .then((json) => console.log(json));
  };

こんな感じです!1行ずつ見てみましょう!
(「const getCatUrl = () => {」の説明は割愛します🙏)

fetch("https://api.thecatapi.com/v1/images/search")

早速出てきましたね!これがfetchです!
ここでは第一引数の「https://api.thecatapi.com/v1/images/search
からデータを持ってくる役割を担っています。

.then((res) => {return res.json();})

見慣れないものが出てきました。「.then()」ってなんでしょう?
こちらは「fetch(https://api.thecatapi.com/v1/images/search)」が成功した場合に()の中の処理をやって!と言っているのですね。(正確には少し違いますが…)
「then」は日本語で「次に」、「それから」といった意味を持ちます。

()の中は「引数resの中身をjson形式に変換して!」と言っています。
何のことかさっぱりですね。
後でしっかり解説しますので一旦「ここはこういうものだ」と理解をお願いします🙇

.then((json) => console.log(json));

また「.then()」ですね、今度は「.then((res) => {return res.json();})」が成功した場合に今回の処理をやって!と言っているみたいです。
()の中身は「引数jsonをコンソールログ出力して!」です。
引数jsonはともかく処理自体はシンプルですね。

ではコンソールにはどのように出力されているでしょうか!
早速見てみましょう!

引数jsonの中身
{
    "id": "3IWhPRL3a",
    "url": "https://cdn2.thecatapi.com/images/3IWhPRL3a.jpg",
    "width": 1176,
    "height": 1184
}

オブジェクトが返されましたね!
中を見る限り何かのidと画像URLと縦横のサイズが返されています。
つまり「https://api.thecatapi.com/v1/images/search」から画像を取得することに成功しました!やったね!🎉🥳🎉
以上がfetchの基本的な使い方です!
こんなん基本じゃねーよ!と思われても石を投げるのはお止めください。

■解説

順を追って説明していきましょう!

・fetchの正体

fetch("https://api.thecatapi.com/v1/images/search")

今までの処理で「fetchが成功か失敗か判定をしている」と思っている方も多いのではないでしょうか。(自分がそう説明をしてしまいましたが…)
fetch(https://api.thecatapi.com/v1/images/search)」は「https://api.thecatapi.com/v1/images/search」からデータを持ってきて!と指示(リクエスト)を出しています。
指示(リクエスト)を出した後、fetchが返すのは「Promiseオブジェクト」です。
このPromiseオブジェクトが成功か失敗かの結果を返すのです。

・thenの正体

.then((res) => {
  return res.json();
})
.then((json) => console.log(json));

実は先ほどのthenの説明「fetchが成功した場合に実行されるもの」は正しくないのです。
改めてthenとは「Promiseが成功した後に実行されるもの」です。
fetchから返されたPromiseオブジェクトのスタータスがFulfilled(成功)になったときに初めて
.then((res) => { return res.json(); })」を呼び出して、引数に「responseオブジェクト」を渡します
responseオブジェクトにはサーバーから返された情報が含まれています。
今回の例でいえば「https://api.thecatapi.com/v1/images/search」の情報ですね。

ここの処理はずばり「その色々な情報をjson形式に変換している」のです。
変換しないとデータが扱いにくいのでここで変換しているんですね。

更にthenもpromiseオブジェクトを返します
.then((json) => console.log(json));」は1つ前のthenが返したpromiseオブジェクトに反応して実行されてます。
引数jsonに渡されるものはjson形式に変換されたresです。
つまり変換されたresをコンソールに表示しているんですね!

■おまけの解説

・取得に失敗したときは?
今回はfetchと成功した場合に使えるthenを紹介しましたが、失敗した場合に使うものもあります。
fetchがうまくいかなかった場合の処理を書きたいときには「catch」を使いましょう。
記述はthenと同じです。先ほどのコードに書くならば以下のようになります。

catchの使用例
  const getCatUrl = () => {
    fetch("https://api.thecatapi.com/v1/images/search")
      .then((res) => {
        return res.json();
      })
      .then((json) => console.log(json))
     .catch((error) => console.log("Error"));
  };

■呼び出すだけじゃない!色々な使い方

今まではデータの取得の説明をしてきました。
最初も言いましたがfetchは取得だけでなく送信、更新、削除。いわゆるCRUDが行えるのです。

今までfetch()の第2引数に何も入れませんでした、その場合は
GET(Read:読み取り)が動くようになっています。
ではこの第2引数をどのようにしたら他の操作ができるようになるのか見てみましょう。

・POST(Create:生成)

  const post = () => {
    fetch(apiUrl, {
      method: "POST",
      body: JSON.stringify(postData),
      headers: {
            'Content-Type': 'application/json'
      }
    })
  };

fetchの中身が「apiUrl, { method: "POST", body: JSON.stringify(postData),
headers: { 'Content-Type': 'application/json' } }」となっていますね。
POSTの場合は第2引数に「method: "POST"」、「body: (渡したいデータ)」を記載する必要があります。
ちなみに「headers: { 'Content-Type': 'application/json' }」はこのデータはjsonだよと説明しているみたいです。
書いても書かなくてもいいそうですが書いておくと余計なエラーが出さずに済むかもしれません。

・PUT(Update:更新)

  const post = () => {
    fetch(apiUrl, {
      method: "PUT",
      body: JSON.stringify(postData),
      headers: {
            'Content-Type': 'application/json'
        }
    })

POSTと比べるとmethodがPUTに変わっただけですね。
もちろん実際のデータでは全く一緒だと意味がないですが、形だけならば同じのようです。

・DELETE(Delete:削除)

  const post = () => {
    fetch(apiUrl, {
      method: "DELETE",
    })

とてもシンプルになりました。methodがDELETEになり、あとは何も書かれていません。
今の例では削除対象のURLがそのままになっていますが、通常は削除対象のデータのIDなどを含めたURLを記載した方がいいと思います。
このままだと全部消えるんじゃないでしょうか🤔

②非同期処理を理解しよう!

■結局、非同期処理ってなあに?

プログラムを実行するとどんな順番で処理が行われるでしょうか?
上から順番に実行されていきますよね!
これを同期処理と言い、1つの処理が終わるまで次の処理を始めることができません
順番と処理が一致している。ということですね!
それに対して非同期処理は、1つの処理が終わるのを待たずに次の処理を始めることができます
順番と処理が一致していない。ということですね!

料理に例えてみましょう🍳
同期処理は煮込んでいる間完成するまでずっと鍋を見ている状態です。
非同期処理は煮込んでいる間に他の材料の準備をしている状態です。

■どうして非同期処理が必要なの?

非同期処理を使わずにデータの通信などを行うと、
通信が完了するまで処理と画面が止まってしまいます
今回のように猫の画像を取りに行くだけなら大した待ち時間ではないでしょうが、
もっと大きなデータを扱ったり通信の回数が多い場合、まともに操作ができない欠陥アプリが出来上がってしまうのです😱
初めに説明した「ロード時間をなくすための仕組み」はこういうことだったんですね。

■Promiseについて

最初の説明では「非同期処理を綺麗に書くための仕組み」と言いました。
この説明も正しいです。
Promiseを使わずに非同期処理を扱うこともできますが、コードが読みづらくなってしまいます。
しかしもっと詳しく説明すると「JavaScriptで非同期処理の結果を扱うためのオブジェクト」なのです!

fetchの正体とthenの正体で少し触れましたが、Promiseは成功か失敗かの結果を返します。
成功の場合はFulfilledでしたね!
失敗の場合はrejected、処理が終わっていないの場合はpendingとなります!

実際にコードを見ながら解説していきましょう!

promise使用例
  const getCat = new Promise((resolve, reject) => {
    const result = true;
    if (result) {
      resolve("猫吸います"); 
    } else {
      reject("逃げられました"); 
    }
  }).then((message) => {
    console.log(message)
  }).catch((err) => {
    console.log(err)
  });
const getCat = new Promise((resolve, reject) => {

Promiseを使用する場合は「new Promise()」という型で使用します。
今回は定数「getCat」にPromiseの処理を入れていますね。((resolve, reject) => {は一旦気にしないでください。)

new Promise((resolve, reject) => {...})

Promise()には引数を渡せます。そしてその引数は必ず関数でなくていけません
ここでは「(resolve, reject) => {...}」が第1引数の関数ですね。
第1引数の関数には成功した時に呼ばれる関数失敗した時に呼ばれる関数が引数として渡されます。
この成功した時に呼ばれる関数がresolve、失敗した時に呼ばれる関数がreject です!

{...}の中は次で説明します。

const result = true;
if (result) {
  resolve("猫吸います"); 
} else {
  reject("逃げられました"); 
}

{...}の中身です。
ここではresolve()とreject()が呼び出されてなにをするかを書いています。
if文でresultがtrueの場合に「resolve("猫吸います")」を、
resultがfalseの場合に「reject("逃げられました")」を呼び出すようにしていますね。
今回resultにはtrueが入っているのでresolve()が呼び出されます。

resolve()が呼び出されるとどうなるか?Promiseの状態がFulfilledになります!
そしてresolve()に渡した"猫吸います"がthenの引数messageに渡されます

.then((message) => {
  console.log(message);
})
.catch((err) => {
  console.log(err);
});

Fulfilledになったあとはthenの正体でやりましたね!
.then((message) => {console.log(message);})」を呼び出して実行します!
fetchの例との違いは引数に渡すものが「resolve("猫吸います")」の「猫吸いします」が渡されています。

■おさらい

Fulfilledだresolveだthenだと混乱してきましたね😵‍💫
もう一度おさらいをしましょう。
①Promiseは非同期処理を行ったあと、成功した場合にresolveを、失敗した場合にrejectを呼び出します。
②resolveが呼び出されると状態がFulfilledに、rejectが呼び出されると状態がrejectedに変化しその結果を返します。
③Fulfilled状態になったらresolveに渡した値をthenの引数に渡してthenが、
rejected状態になったらrejectに渡した値をcatchの引数に渡してcatchが呼び出されます。

こう見るとfetchは①と②が内蔵されているように見えますね!便利です!

③useEffectを理解しよう!

■useEffectってなあに?

useEffectは「コンポーネント内で副作用を扱うことができるフック」です。
副作用と言っても「薬の副作用」の副作用ではありません。
簡単に言ってしまえば「コンポーネントの表示や更新とは別に行う処理のこと」でしょうか。
例を挙げると、APIからデータを取得する、手動でDOMを操作する、タイマーの設定などが副作用にあたります。

また、公式では「コンポーネントを外部システムと同期させるためのReactフックです。」と説明されています。
更に「外部システムと同期する必要がない場合、エフェクトはおそらく不要です。」とも言われています。
外部システムというと大げさに聞こえてしまいますが、Reactが制御していないコードのことですね。

上記例に挙げた3つはすべてReactが制御していないコードなのでuseEffectが使えます。

■早速使ってみよう!

どういう風に使うのか実際に見ていきましょう!

  const [catUrl, setCatUrl] = useState("");
  const getCatImg = () => {
    fetch("https://api.thecatapi.com/v1/images/search")
      .then((res) => {
        return res.json();
      })
      .then((json) => setCatUrl(json[0].url));
  };

  useEffect(() => {
    getCatImg();
  }, []);

fetchで作ったgetCatImg(少し処理を変えています)をuseEffectに入れてますね!
こうすることにより、取得した猫の画像を表示させることができます!
詳しく見てみましょう!

  useEffect(() => {
    getCatImg();
  }, []);

型自体はシンプルですね!
useEffect()の第一引数に関数を入れて、第二引数を設定するだけのようです。
第二引数の「[]」はなんでしょう?

第二引数には配列を入れる必要があります。
これを依存配列といい、useEffect内の処理がどのタイミングで実行されるか制御できます!

・制御のパターン

依存配列を使用する場合、使い方は3パターンあります。

①何も指定しない場合

  useEffect(() => {
    getCatImg();
  });

先ほどと違って第二引数に[]が無くなりましたね!
この場合はコンポーネントの初回表示時と、再表示のたびに処理が実行されます


②空の配列([])を渡す場合

  useEffect(() => {
    getCatImg();
  }, []);

早速使ってみよう!の項で出てきたのと同じです
この場合はコンポーネントの初回表示時に一度だけ実行されます


③特定の値を配列に渡す場合

  useEffect(() => {
    getCatImg();
  }, [value]);

[]の中にvalueが入っていますね。
この場合はコンポーネントの初回表示時と、渡した値が変わったときだけ実行されます
ここではvalueの値が変わったらgetCatImgが動くようになっていますね。

■使い方に気を付けよう!

useEffectは便利で大体の処理に使えます。
ただしなんにでも使っていいものではありません。あくまで「コンポーネントを外部システムと同期させるため」に使用するのが望ましいです。
useEffectを使わなくてもイベントハンドラでなんとかなったりするので使う前には一度考えてみましょう!
上で説明しましたが公式で非推奨とされている使い方もあります。
どんなときに使うか使わないかを更に詳しく知りたい方は公式ドキュメントをお読みください。
https://ja.react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts
https://ja.react.dev/learn/you-might-not-need-an-effect

・お掃除が必要かも?

useEffectにはクリーンアップ関数が必要になることがあります。
必要である場合とない場合の見極めが難しいと思いますが覚えておいてほしいです!

例としてちょっとしたタイマーアプリを作ってみましょう。

クリーンアップ関数を使わないタイマー:子
function Timer() {
  const [dateTime, setDateTime] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => {
      setDateTime(new Date());
      console.log(new Date());
    }, 1000);
  }, []);

  return <h1>{dateTime.toLocaleString()}</h1>;
};
クリーンアップ関数を使わないタイマー:親
const App = () => {
  return <Timer />;
};

子コンポーネントではuseEffect内に直接setIntervalを使って、コンソールに現在時刻を1秒ごとに表示させる処理を書きました。
また、returnにはh1タグに現在時刻を1秒ごとに表示させるようにしました。
親コンポーネントはそれを受け取り、実際にコンソールと画面に現在時刻を表示させます。

こんな感じ。

表示中に親コンポーネントの<Timer />をコメントアウトしてみましょう。

21時28分25秒に親コンポーネントの<Timer />をコメントアウトしました。
子コンポーネントが消えたにも関わらずコンソール上のタイマーは動いていますね…🤔
このままでは不要になった処理が動き続けて、メモリリークの原因となってしまいます。

これを防ぐのがクリーンアップ関数です!
その名の通り使い終わった処理をお掃除する関数ですね!
先ほどのタイマーに書いてみましょう!

クリーンアップ関数を使うタイマー:子
function Timer() {
  const [dateTime, setDateTime] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => {
      setDateTime(new Date());
      console.log(new Date());
    }, 1000);

    return () => {
      clearInterval(id);
    };
  }, []);

  return <h1>{dateTime.toLocaleString()}</h1>;
};

useEffectの中に「return () => {clearInterval(id);};}, []);」が増えています。

    return () => {
      clearInterval(id);
    };

この部分がこのタイマーにおけるクリーンアップ関数ですね!
コンポーネントが削除されたり依存配列の値が変わるときに実行されます。
では先ほどのように途中で親コンポーネントの<Timer />をコメントアウトしてみましょう。

21時59分06秒に親コンポーネントの<Timer />をコメントアウトしました。
コンソールのタイマーも止まっていますね!
これでこのタイマーは親コンポーネントから削除されても処理を続けることはなくなりました!
未然にバグを防ぎましたね!🎊🥳🎊

おわりに

今回はfetch、非同期処理とPromise、useEffectについての解説を行いました。
なるべく難しい言葉を使わずに書いたつもりですがどうでしょうか?
Reactを使う上で避けては通れない箇所だと思っているのでこの記事が助けになれば幸いです。

記事のここが間違えてる!分かりづらい!などご意見があれば優しく教えて下さい。なるべく早く対応します。🙇

感想

喋れなかったことがたくさんあったので長い記事になってしまった気がします。
自分の学習が甘かったということですね…教える係をやっていなかったら多分甘いままだったのでありがたいです。🫠

更にありがたいことに初めての方がいらっしゃったり、
「分かりやすかった!」「楽しかった!」といったご感想をいただいています。励みになります!
もし今後も続けることがあれば、慢心せず頑張りますのでよろしくお願いします!

りあゼミ!

Discussion

NanaNana

説明を実際に聞いていたときも、なるほど〜!って頭にスッと入ってきましたが、議事録になると、更にわかりやすくて、復習になりました!また、クリーンアップ関数もタイマーアプリで分かりやすくて勉強になりました!ありがとうございます!

CNoCNo

喋ってない箇所が分かりやすく伝わるか不安でしたがなんとか伝わったようでよかったです!
いつも温かい言葉を送ってくださりありがとうございます!

RURIRURI

議事録すごすぎて…🥹
引き継ぎます…🥹笑

CNoCNo

ありがとうございます!今仕事ないから時間割けるだけです…😅
余裕出来たらまた書いてくださいね!