……いや、React というより Fetch API のせいなのかもしれない。
ここのところで React で web アプリを作ってる。例によって自分で使うためだけのもの。
JSON を返す API を2つバックエンドに置いて、両方から取ってきたデータを合わせて表示する Collections コンポーネントを書いてたんだけど、いい書き方がわからずにハマった。
Collections コンポーネントは次のように動作する(のを期待している)。
- 一方のAPIからコレクションのリストを取得する
- リスト中のコレクションそれぞれについて、もう一方のAPIから詳細を取得する
- 2つのAPIから取得したデータを合わせて、コレクションのリストとしてテーブル表示する
コレクションというのは、プログラミングにおけるデータ構造のことじゃなくて買い集めたコレクション(CDとかDVDみたいな)のこと。renderCollections 関数でレンダリングしているテーブルの行のうち、collection.id と collection.buy_date が一方のAPIから取得したデータ、collection.title と collection.brand がもう一方のAPIから取得したデータだ。
import React, { useState, useEffect } from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
const Collections = (props) => {
const perPage = 10;
const [collections, setCollections] = useState([]);
const [currentPage, setPage] = useState(0);
const page = props.query.page ? parseInt(props.query.page) : 1;
useEffect(
() => {
setPage(page);
const offset = perPage * (page - 1);
let colls = [];
const fetchProduct = async (coll) => {
const result = await fetch(`${props.api2Endpoint}/products/${coll.product_id}`)
.then(response => response.json())
.then(data => data["products"][0]);
colls = [...colls, {...coll, title: result.title, brand: result.brand.name}];
setCollections(colls);
};
const fetchData = async () => {
const result = await fetch(`${props.api1Endpoint}/collections?limit=${perPage}&offset=${offset}`, { mode: "cors" })
.then(response => response.json())
.then(data => data["collections"]);
for (const c of result) {
fetchProduct(c);
}
};
fetchData();
},
[]
);
return (
<div>
<h2>Collections</h2>
<TableContainer component={ Paper }>
<Table aria-label="collection table">
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Title</TableCell>
<TableCell>Brand</TableCell>
<TableCell>Buy date</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ renderCollections(collections) }
</TableBody>
</Table>
</TableContainer>
</div>
);
}
const renderCollections = (collections) => {
return (
collections.map(collection => (
<TableRow key={ collection.id }>
<TableCell>{ collection.id }</TableCell>
<TableCell>{ collection.title }</TableCell>
<TableCell>{ collection.brand }</TableCell>
<TableCell>{ collection.buy_date }</TableCell>
</TableRow>
))
);
}
export default Collections;
いろいろググりながらなんとか書いた上のコードで一応動くようにはなった。けど、表示されるコレクションの順番が不定になってしまう。APIからリストを取得した段階では ID 順に並んでいるのは確認したので、たぶん、個々の詳細データを取得する fetchProduct 関数内でリスト(変数 collections)を更新するタイミングのせいだ。つまり、ループの中で使ってる fetch 関数が非同期なせいだ(たぶん)。
あぁ、JavaScript ってこういうところがなんかやりづらいんだよなぁ。
なんかいい書き方はないものか。あとからソートすればいいのはわかるんだけど。