こんにちは、新卒エンジニアの鈴木です!
せっかくバックエンドでAPIを完成させたのに、どうやってReactで表示すればいいんだ? と手が止まってしまった経験はありませんか?
「どのタイミングでAPIを叩けばいいの?」
「取得したデータをどうやってコンポーネントに渡すの?」
「調べたけど非同期処理ってなんだ?」
非同期処理という言葉を聞くだけで、なんだか難しそうだと感じてしまいますよね。
でも、ご安心ください! Reactの基礎知識だけで、驚くほどシンプルにAPI通信を実装できるんです。
この記事では、APIをReactアプリケーションから呼び出し、取得したデータを画面に表示するまでの一連の流れを、ステップバイステップで丁寧に解説します。
開発の準備
Reactプロジェクトの準備
まずは、Reactのプロジェクトを作成しましょう。今回は、高速な開発環境で注目されているVite(ヴィート)というツールを使います。
Viteを初めて使う方や、詳しく知りたいという方は、さきにVite公式サイトで概要を確認してみてください。
準備ができたら、作業したいフォルダ内で以下のコマンドを実行してください。
C:\Users\鈴木 伊織\Desktop\doc>npm create vite@latest
> npx
> create-vite
│
◇ Project name:
│ wordbook-react-blog
│
◇ Select a framework:
│ React
│
◇ Select a variant:
│ JavaScript
│
◇ Scaffolding project in C:\Users\鈴木 伊織\Desktop\doc\wordbook-react-blog…
│
└ Done. Now run:
cd wordbook-react-blog
npm install
npm run dev
これだけで準備は完了です! 簡単ですね!
呼び出し先のAPIの確認
前回作成したAPIを使います。ぜひ以下記事から確認をお願いします!
Express.js x PostgreSQLではじめての認証付きAPI構築
| エンドポイント | 説明 | Tokenの有無 | |
|---|---|---|---|
| ログイン(POST) | /auth/login | usernameとpasswordを使ってログインする。ログインに成功するとtokenが返される | 無し |
| 単語一覧取得(GET) | /words | DBに登録されている単語を全部取得する | 無し |
| 単語新規追加(POST) | /words | 新しく単語を登録する | 有り |
| 単語1件取得(GET) | /words/:id | DBに登録されている指定された単語を1件取得する | 有り |
| 単語更新(PUT) | /words/:id | 指定された単語を任意のものに更新する | 有り |
| 単語削除(DELETE) | /words/:id | 指定された単語を削除する | 有り |
実装
さっそく書いていきましょう。
以下は簡単なログイン画面のコンポーネントの例です。こちらを使ってログイン処理を実装していきましょう。
//src/App.jsx
import { Login } from "./components/pages/login/login"
function App() {
return (
<>
<Login />
</>
);
}
export default App
//src/components/pages/login/login.jsx
import { useState } from "react";
export function Login() {
// state
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
// handlers
const handleSubmit = (event) => {
event.preventDefault();
// API処理をここで行う
}
return (
<>
<h2>Login Page</h2>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input type="text" id="username" value={username} required
onChange={(event) => setUsername(event.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" id="password" value={password} required
onChange={(event) => setPassword(event.target.value)}
/>
</div>
<button type="submit">Login</button>
</form>
</>
);
}
Ajaxの実装
ログイン画面のUIができたのでAPIの処理を実装していきます。
さまざまな場所でAPIを呼び出すたびにfetchを記述するのは大変ですし、コードも重複してしまいます。そこで、ページをリロードせずにサーバーと通信するAjaxという技術を実現するために、fetchを使った便利な共通関数を作成します。
//src/utils/Ajax.js
export async function Ajax(url = "", method = 'GET', bodyData, token,) {
const BASE_URL = "http://localhost:3001/api/";
const opt = {
method,
headers: {
"Content-Type": "application/json",
}
};
// If a token is provided, add it to the headers
if (token) {
opt.headers.Authorization = `Bearer ${token}`;
}
// If bodyData is provided, add it to the body
if (bodyData) {
opt.body = JSON.stringify(bodyData);
}
const response = await fetch(`${BASE_URL}${url}`, opt);
return response.json();
}
この関数を使えば、必要な情報を引数として渡すだけでAPIリクエストが完了し、毎回同じコードを書く手間が省けてコンポーネントもスッキリします。こちら使って、APIを実装していきましょう。
ログイン機能の実装
さきほどのhandleSubmitに追記します。
//src/components/pages/login/login.jsx > handleSubmit
const handleSubmit = (event) => {
event.preventDefault();
// API処理をここで行う
Ajax('auth/login', 'POST', { username, password }, null)
.then(res => {
if(res.token) {
console.log('Login successful:', res);
}
else {
alert('Login failed: ' + res.message);
}
});
}
作成したAjax関数を使い、ログイン処理を実装しています。
ログインAPIの仕様に合わせて、引数にはエンドポイントのパス('auth/login')、メソッド('POST')、そしてリクエストボディとしてusernameとpasswordのオブジェクトを渡します。
Ajax関数はPromiseを返す設計なので、.then()でレスポンスを受け取ります。レスポンスの中にtokenが含まれていればログイン成功と判断し、そうでなければ失敗としてアラートを表示する、という流れです。
コンテキストの実装
ログインの実装が完了しました。
ログインに成功すると、APIサーバーからtokenが返ってきます。このtokenは、今後単語の追加や削除といった認証が必要なAPIを呼び出す際に必ず必要になります。
しかし、tokenをLoginコンポーネントだけで持っていても、他のコンポーネントから利用できません。そこで、ReactのContextを使います。
Contextを使えば、コンポーネントの親子関係を気にすることなく、アプリケーション全体でのユーザー情報や現在の表示画面といったデータを簡単に共有できるようになります。
//src/components/appContext.jsx
import { createContext, useState } from "react";
import useStorage from "../hooks/useStorage";
export const AppContext = createContext();
function AppContextProvider({ children }) {
// appの状態管理
const [appState, setAppState] = useState('login');
// ユーザ情報の状態管理
const [user, setUser] = useStorage('user', { userId: null, token: null }});
const ctx = {
appState,
setAppState,
user,
setUser,
};
return (
<AppContext.Provider value={ctx}>
{children}
</AppContext.Provider>
);
}
export default AppContextProvider;
さらに今回は、ブラウザをリロードしてもログイン状態が消えないように、useStorageというカスタムフックを使い、取得した情報をブラウザのlocalStorageに保存する処理も加えています。
//src/hooks/useStorage
import { useState } from "react";
function useStorage(key, defaultValue) {
// storage check
const item = window.localStorage.getItem(key);
// if item is not found, use defaultValue
const [value, setValue] = useState(
item ? JSON.parse(item) : defaultValue
);
// setValue is a function to update the state and localStorage
const set = (value) => {
setValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
};
return [value, set];
}
export default useStorage;
main.jsxに記述することもお忘れなく。
//src/main.jsx > createRoot
createRoot(document.getElementById('root')).render(
<StrictMode>
<AppContextProvider>
<App />
</AppContextProvider>
</StrictMode>
)
これでappStateとuser情報を簡単に参照できるようになりました。こちらを使用して、クライアント側の処理を実装していきます。
まずLoginコンポーネントがappStateが'login'のときにしか表示されないようにします。
useContextを使用してappStateを受け取り表示するコンポーネントを切り替えられるようにします。また、userを参照してログイン済みであればsetAppStateを呼び出して自動的に単語一覧画面へ遷移させます。
//src/app.jsx
import { useContext } from "react";
import { AppContext } from "./components/appContext";
import { Login } from "./components/pages/login/login"
function App() {
const {
appState,
setAppState,
user
} = useContext(AppContext);
useEffect(() => {
if(user?.token) {
// ユーザがログインしている場合は一覧画面へ遷移
setAppState('wordList');
}
}, [user]);
return (
<>
{appState === 'login' && <Login />} //appStateがloginのときのみ表示する
</>
);
}
export default App;
いよいよログイン処理が完成します。さきほどのhandleSubmit関数を以下のようにしてください。
// handlers
const handleSubmit = (event) => {
event.preventDefault();
// API処理をここで行う
Ajax('auth/login', 'POST', { username, password }, null)
.then(res => {
if(res.token) {
setUser({userid: res.userId, token: res.token}); //user情報を登録する
setAppState('wordList'); //appStateを変更する
}
else {
alert('Login failed: ' + res.message);
}
});
}
これでログインに成功したときuserの情報とappStateが更新されます。
appStateが更新されたことによって画面に何も表示されなくなりますが、正常に動作しています。
一覧取得の実装
続いて単語一覧取得画面を作成していきます。
//src/components/pages/word/wordList.jsx
import { useContext, useState, useEffect } from "react";
import { AppContext } from "../../appContext";
import { Ajax } from "../../../utils/Ajax";
import { WordCard } from "./components/wordCard";
export function WordList() {
const [wordList, setWordList] = useState([]);
const {
setAppState,
setActiveId
} = useContext(AppContext);
const handleClick = (word) => {
setActiveId(word.id);
setAppState('WordDetail');
}
useEffect(() => {
Ajax('words')
.then(res => {
if(res.length > 0) {
setWordList(res);
}
});
}, []);
return (
<>
<h2>Word List</h2>
<div>
<ul>
{wordList.map((word) => (
<li key={word.id}>
<WordCard word={word} onClick={handleClick} />
</li>
))}
</ul>
</div>
</>
);
}
これだけで一覧取得をすることができます。Ajaxでログイン処理と大きく違うところは、引数にURLしか渡していないところです。
methodにはデフォルト値で'GET'を指定しているため引数に含める必要がなく、bodyDataもtokenも不要なためこの記述ができるようになります。
別のアプリケーションで使用するときは、Ajaxの引数の順番を変えることで、より引数の少ない実装を考えるのが楽しかったりします。
今後1件取得時や単語の編集・削除のAPIを実装します。これらの機能では、どの単語に対して操作を行うかをが正確に把握している必要があります。このどの単語に対して行うかの変数をContextに定義しておきます。
//src/components/appContext.jsx
//省略
//選択された単語IDを管理
const [activeId, setActiveId] = useState(null);
const ctx = {
appState,
setAppState,
user,
setUser,
activeId, //追加
setActiveId //追加
}
//省略
//src/components/pages/word/components/wordCard.jsx
import { useState } from "react";
export function WordCard({ word, onClick }) {
const [isOpen, setIsOpen] = useState(false);
const descOpen = (event) => {
event.stopPropagation();
setIsOpen(!isOpen);
}
return (
<div onClick={() => onClick(word)}>
<h3>{word.word}</h3>
{ isOpen && <p>{word.description}</p> }
<button onClick={(event) => descOpen(event)}>
{isOpen ? 'close' : 'open'}
</button>
</div>
);
}
一覧画面の準備が整ったのでapp.jsxに記述します。
//src/app.jsx > return
return (
<>
{appState === 'login' && <Login />}
{appState === 'wordList' && <WordList />}
</>
);
こうすることで
appStateが'wordList'のときに一覧画面が表示されるようになります。
ログイン処理が成功したら一覧画面が表示されることを確認してください。
また、一度ログイン状態になるとリロードしてもログイン画面が表示されないようになっているはずです!
追加機能の実装
続いて追加機能を実装します。一覧画面に登録画面に遷移するボタンを配置します。
//src/components/pages/word/wordList.jsx
//省略
const handleClick = (word) => {
setActiveId(word.id);
setAppState('wordDetail');
}
const handleClickAdd = () => { //追加
setAppState('wordAdd');
}
useEffect(() => {
Ajax('words')
.then(res => {
if(res.length > 0) {
setWordList(res);
}
})
}, []);
return (
<>
<h2>Word List</h2>
<div>
<button type="button" onClick={handleClickAdd}>add</button> //追加
<ul>
//省略
次に追加画面を作成します。以下のコードを記述してください。
//src/components/pages/word/wordAdd.jsx
import { useState, useContext} from 'react';
import { Ajax } from '../../../utils/Ajax';
import { AppContext } from '../../appContext';
export function WordAdd() {
const [word, setWord] = useState('');
const [description, setDescription] = useState('');
const {
setAppState,
user,
setUser
} = useContext(AppContext);
const handleBack = () => {
setAppState('wordList');
};
const handleSubmit = (event) => {
event.preventDefault();
Ajax('words', 'POST', {word, description}, user.token)
.then(res => {
if(res.message === 'Token expired') {
alert('Token expired. Please log in again.');
setUser({userId: null, token: null});
setAppState('login');
} else {
alert('Word added successfully');
setAppState('wordList');
}
})
.catch(err => {
alert('Error adding word:', err);
});
}
return(
<>
<h2>Word Add</h2>
<button type="button" onClick={handleBack}>back</button>
<form onSubmit={handleSubmit}>
<div>
<label>Word:</label>
<input
type="text"
id="word"
value={word}
onChange={(event) => setWord(event.target.value)}
placeholder="Enter word"
/>
</div>
<div>
<label>Description:</label>
<input
type="text"
id='description'
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="Enter description"
/>
</div>
<button type="submit">add</button>
</form>
</>
);
}
これはformに入力されたデータを基にAPIを実行しています。
単語の追加はログインしているユーザーのみに許可された操作のため、Ajax関数の第4引数にuser.tokenを渡して認証情報をサーバーに送っています。
認証失敗時にはuser情報を削除し、再度ログインを求めるようログイン画面へ誘導しています。正常に登録された場合は一覧画面へ遷移します。
これだけで追加処理は完成です。
詳細画面の実装
続いて詳細画面を作成していきます。
一覧画面で単語をクリックすると詳細画面が表示される処理は書いてあるので、さっそく実装していきます。
//src/components/pages/word/wordDetail.jsx
import { useContext, useEffect, useState } from "react";
import { Ajax } from "../../../utils/Ajax";
import { InputField } from "./components/InputField";
import { AppContext } from "../../appContext";
export function WordDetail() {
const [word, setWord] = useState();
const [description, setDescription] = useState();
const {
setAppState,
user,
setUser,
activeId
} = useContext(AppContext);
const handleBack = () => {
setAppState('wordList');
};
const handleSubmit = (event) => {
event.preventDefault();
// API処理をここで行う(PUT)
};
const handleDelete = () => {
// API処理をここで行う(DELETE)
}
useEffect(() => {
Ajax(`words/${activeId}`)
.then(res => {
setWord(res.word);
setDescription(res.description);
});
}, []);
return (
<>
<h2>WordDetail</h2>
<button type="button" onClick={handleBack}>back</button>
<form onSubmit={handleSubmit}>
<InputField
value={word}
onChange={(event) => setWord(event.target.value)}
tagName="h3"
/>
<InputField
value={description}
onChange={(event) => setDescription(event.target.value)}
tagName="p"
/>
<br/>
<button type="submit">update</button>
</form>
<button type="button" onClick={handleDelete}>delete</button>
</>
);
}
上のコードでは、詳細画面が表示された瞬間にactiveIdに基づいて1件取得するAPIを実行しています。一覧画面からpropsでもらってくる方法もありますが、APIを実行することでより最新のデータを取得することができます。
//src/components/pages/word/components/inputField.jsx
import { useState, useRef } from "react";
export function InputField({ value, onChange, tagName}) {
const inputRef = useRef(null);
//動的にタグを切り替える
const Tag = tagName || 'span';
const [isInput, setIsInput] = useState(false);
const inputStart = () => {
setIsInput(true);
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 0);
};
return (
<>
{isInput ? (
<input
type="text"
value={value}
onChange={onChange}
onBlur={() => setIsInput(false)}
onKeyDown={(event) => event.key === 'Enter' && setIsInput(false)}
ref={inputRef}
/>
) : (
<Tag onClick={() => inputStart()}>{value}</Tag>
)}
</>
);
}
上のコンポーネントでは、テキストをクリックするとその場で編集できるUIを、Stateによる条件分岐で実現しています。
変更機能の実装
さきほど詳細画面でformを実装してしまったためこちらではAPIの処理を書くだけで良いです。
処理自体も単語追加とはmethodが違うだけで要領は変わりません。
wordDetail.jsxに以下のコードを記述してください。
//src/components/pages/word/wordDetail.jsx > handleSubmit
const handleSubmit = (event) => {
event.preventDefault();
Ajax(`words/${activeId}`, 'PUT', {word, description}, user.token)
.then(res => {
if(res.message === 'Token expired') {
alert('Token expired. Please log in again.');
setUser({userId: null, token: null});
setAppState('login');
} else {
alert('Word updated successfully');
setAppState('wordList');
}
})
.catch(err => {
alert('Error updating word:', err);
});
};
削除機能の実装
削除も詳細画面作成時にボタンを配置していたため、APIを実装するだけです。
bodyDataがnullであることに注意してください。
//src/components/pages/word/wordDetail.jsx > handleDelete
const handleDelete = () => {
if (window.confirm('Are you sure you want to delete this word?')) {
Ajax(`words/${activeId}`, 'DELETE', null, user.token)
.then(res => {
if(res.message === 'Token expired') {
alert('Token expired. Please log in again.');
setUser({userId: null, token: null});
setAppState('login');
} else {
alert('Word deleted successfully');
setAppState('wordList');
}
})
.catch(err => {
alert('Error deleting word:', err);
});
}
};
まとめ
今回は、Reactアプリケーションにおける実践的なAPI連携について解説しました。
僕自身、API連携を学び始めた頃は難しく感じましたが、一つひとつの仕組みを理解すると、作れるものの幅がぐっと広がりプログラミングが楽しくなりました!
非同期処理のような複雑なテーマは、まず今回作成した関数のように使い方を理解し、それからなぜそう動くのかを内部のコードで理解していくと、知識が定着しやすいような気がします。今回の内容をベースに、ご自身のプロジェクトで試行錯誤してみてください!
最後までお読みいただき、ありがとうございました!