Next.jsでフォームに入力されたデータを使ってAPIを叩く
1. はじめに
こんにちは。アプレンティス2期生のsakanaです。
ReactとNext.jsを触り始めて1週間ほど、JavaScriptも全然触ってこなかったので、Next.jsに悪戦苦闘しています。
そんな中で、特にフォームのデータを利用してAPIのデータを取得することに苦戦したので、その流れをまとめていこうと思います。
どこかでとんちんかんなことをしているかもしれませんが、ご容赦ください。
TypeScriptを使った方が良いと思うのですが、そこまで手が伸びていないので、今回はTypeScriptは使用していません。
また、アロー関数推奨という記事をよく見かけるのですが、ReactやNext.jsのチュートリアルを見ても、export default function () {};
みたいな書き方をしているので、今回は公式に倣った書き方でまとめていきます。
2. 事前準備
2.1. API TOKENの取得
にアクセスし、APIトークンを取得しておきましょう。これがないと、天気データを取得することができません。
2.2. 環境変数の設定
新しいNext.jsのプロジェクトを作成したら、まずは以下を変更しましょう。
- page.jsをpage.jsxに直す
- 縞模様がうっとうしかったら、layout.jsのcssのimportをコメントアウト
テストなので直接コードの中にトークンを書いてもよいですが、せっかくなのでenvファイルを作成してその中にトークンを置きます。
プロジェクトのルート直下に.envファイルを作成してください。
API_TOKEN = "***************************"
このAPIトークンをちゃんと出力できるか試してみましょう。
export default function Page() {
console.log(process.env.API_TOKEN);
return <></>;
}
Next.jsでは基本的にserverを起動しているターミナルにconsole.log
の結果が表示されるので、それを確認できる環境にしておくことも大事です。
Dockerを使って起動している場合は、あらかじめdocker attach
しておくか、Dcker Desktopのコンテナのログを開いておきましょう。
3. WheatherAPIを叩いてみる
API_TOKENさえあれば、Wheather APIからデータを取得することができるので、実際にデータを取得してみましょう。
APIからデータを取得するのにはfetchメソッドを使うと楽です。
fetchメソッドは戻り値がPromiseを返すことからもわかる通り、非同期で扱う必要があります。
そして、Next.jsではasync/awaitの記法が使えるので、非同期の処理をシンプルに記述できます。.then地獄で見づらくなることはありません。
export default async function Page() {
const weather = await fetchWeather();
return (
<>
<h1>{weather.name}の天気</h1>
<div>最高気温:{weather.main.temp_max}</div>
<div>最低気温:{weather.main.temp_min}</div>
</>
);
}
export async function fetchWeather() {
const place = "Tokyo";
try {
console.log("データ取得中です...");
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${place}&units=metric&lang=ja&appid=${process.env.API_TOKEN}`
);
const data = await response.json();
return data;
} catch (error) {
console.log("データ取得エラー");
}
}
4. ボタンを押したらAPIを叩く処理
ここまではfetchしたデータをHTMLとしてそのまま表示していましたが、次からは返ってくるデータを受け取る処理が面倒なので、一旦console.log
で取得した天気データを確認することを目指します。
console.log
でデータを確認する
4.1. APIを叩いて先ほどは、ページを読み込むと同時にfetchWeatherメソッドを実行すれば良かったのですが、ボタンを押すなどのクライアント側のアクションに紐づいてプログラムを実行したい場合は、Client Componentを使用する必要があります。
特に触れていませんでしたが、Next.jsでは、何も宣言していないファイルはServer Componentと判断されて、プログラムがサーバー上で実行されます。だからこそ、Serverを起動しているターミナルにconsole.log
の結果が表示されていたんですね。
しかし、それではブラウザでのユーザーのアクションなど、クライアント側の変化を検知することが難しいので、ボタンを押すという処理をClient Componentで書く必要があります。
ファイルをClient Componentに変更するためには、ファイルの冒頭で”use client”
と宣言すればokです。
"use client";
export default function Page() {
return (
<>
<form action={fetchWeather}>
<button type="submit">データを取得する</button>
</form>
</>
);
}
export async function fetchWeather() {
const place = "Tokyo";
try {
console.log("データ取得中です...");
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${place}&units=metric&lang=ja&appid=${process.env.API_TOKEN}`
);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.log("データ取得エラー");
}
}
しかし、ボタンを押してもコンソールに天気データは表示されず、このコードが正常に動いていないことがわかります。
“use client”
しているということで、今回のコードのconsole.log
の結果は、ブラウザの開発者ツールから見れるはずです。
そちらを確認すると、401 Unauthorizedのエラーが表示されています。
それもそのはず。fetchする際に利用しているprocess.env.API_TOKEN
をClient Compoentからは読み取れないので、エラーが起きているのでした。
というわけで、ファイルを2つに分割して、Client ComponentとServer Componentに分けることにします。
page.jsxはそのままClient Componentとして使い、新たにdata.jsxファイルを作成して、fetchWeatherメソッドをそちらに移動します。
"use client";
import { fetchWeather } from "./data";
export default function Page() {
return (
<>
<form action={fetchWeather}>
<button type="submit">データを取得する</button>
</form>
</>
);
}
"use server";
export async function fetchWeather() {
const place = "Tokyo";
try {
console.log("データ取得中です...");
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${place}&units=metric&lang=ja&appid=${process.env.API_TOKEN}`
);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.log("データ取得エラー");
}
}
ここで注意しないといけないのは、importされる側のdata.jsxファイルの冒頭で”use server”
という宣言を行わなければならないということです。
page.jsxで”use client”
という宣言を行うと、page.jsxだけでなく、そのページにインポートされているものまでClient Componentとして扱われることになります。それではファイルを分けた意味がありませんし、process.env.API_TOKEN
の値が取得できずにまた401 Unauthorizedのエラーがでてしまいます。
そのため、data.jsxの先頭で明示的に”use server”
と宣言することで、Server Compoentとして扱うことが可能になります。
普段は何も宣言しなくてもServer Componentとして扱われていますが、Client ComponentにimportされているときにServer Componentとして扱いたい場合は、ちゃんと”use server”
を宣言しなければならないということです。
4.2. useStateを用いて、クライアントサイドでデータを使用する
これで無事にデータをfetchすることができました。
しかし、この値をそのままクライアントサイドに渡しても、データを表示するためのトリガーがありません。
そこで、Hookを用いてstateにデータを渡すことで、stateの変化を感知して、再レンダリングをしてくれるようになります。
今回はフォームの値を送信したいので、useFormState
というHookを使用します。
"use client";
import { fetchWeather } from "./data";
import { useFormState } from "react-dom";
export default function Page() {
const [weather, setWeather] = useFormState(fetchWeather, "");
return (
<>
<form action={setWeather}>
<button type="submit">データを取得する</button>
</form>
{weather && (
<>
<h1>{weather.name}の天気</h1>
<div>最高気温:{weather.main.temp_max}</div>
<div>最低気温:{weather.main.temp_min}</div>
</>
)}
</>
);
}
これで、データを受け取って表示することができました。
5. フォームデータを渡してAPIを叩く処理
最後に、フォームのデータに応じて出力を変えることができるように、フォームデータを渡す処理を加えましょう。
まず、page.jsxに都市名を入力するフォームを追加します。
inputタグにnameを指定しておかないと、フォームの内容をformDataに渡すことができないので、注意が必要です。
<form action={setWeather}>
<input type="text" name="place" placeholder="都市名を入力してください" />
<button type="submit">データを取得する</button>
</form>
それから、data.jsxのfetchWeatherメソッドで引数を設定し、受け取れるようにします。
第一引数にstate, 第二引数にformDataが入るので、そのように指定してあげます。
place変数に代入する前に、console.log
でちゃんとformDataの受けとりができているか確認してみましょう。
"use server";
export async function fetchWeather(state, formData) {
console.log(formData)
const place = "Tokyo";
try {
console.log("データ取得中です...");
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${place}&units=metric&lang=ja&appid=${process.env.API_TOKEN}`
);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.log("データ取得エラー");
}
}
ちゃんと受け取りができていたら、以下のようなログが確認できるはずです。
FormData {
[Symbol(state)]: [
{ name: '$ACTION_REF_1', value: '' },
{
name: '$ACTION_1:0',
value: '{"id":"8254925f5092e8a780b9a8fb74f1a953c0dbda59","bound":"$@1"}'
},
{ name: '$ACTION_1:1', value: '[""]' },
{ name: '$ACTION_KEY', value: 'k2064331658' },
{ name: 'place', value: 'Tokyo' }
]
}
ここまで確認できたら、後はdataFormからplaceという名前を持つ属性を取得して、place変数に代入すれば良いです。
ということで、最終的なコードは以下になります。
"use client";
import { fetchWeather } from "./data";
import { useFormState } from "react-dom";
export default function Page() {
const [weather, setWeather] = useFormState(fetchWeather, "");
return (
<>
<form action={setWeather}>
<input
type="text"
name="place"
placeholder="都市名を入力してください"
/>
<button type="submit">データを取得する</button>
</form>
{weather && (
<>
<h1>{weather.name}の天気</h1>
<div>最高気温:{weather.main.temp_max}</div>
<div>最低気温:{weather.main.temp_min}</div>
</>
)}
</>
);
}
"use server";
export async function fetchWeather(state, formData) {
const place = formData.get("place");
try {
console.log("データ取得中です...");
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${place}&units=metric&lang=ja&appid=${process.env.API_TOKEN}`
);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.log("データ取得エラー");
}
}
フォームにTokyoとかYokohamaを入力して、ちゃんと表示が変わることが確認できるはずです。
6. まとめ
- APIからのデータを取得する場合、fetchとasync/awaitを使用すると便利
- ユーザーのアクションなどを検知したい場合は、ファイルの先頭で
”use client”
を宣言することで、Client Componentとして扱う - Client ComponentにimportされたComponentはClient Componentと扱われてしまうので、Server Componentとして扱いたい場合は、先頭で
”use server”
を宣言する必要がある - APIから受け取ったデータを表示したい場合は、状態が変化したら再レンダリングを行ってくれるstateを利用すると良い
7. 最後に
Client Componentでもasync/awaitって使えるんだっけとか、インラインで”use server”
とか”use client”
とか書いてエラーを吐かない条件はなんなのとか、実はまだ色々疑問は残っています。
しかし、なんとか自分の理解した(つもりの)範囲で、記事がまとめられて良かったです。そのあたりの疑問点は、解決次第また記事を書いていこうと思います。
この記事が、少しでもNext.jsで苦戦している方の助けとなれば幸いです。
Discussion