……いや、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 ってこういうところがなんかやりづらいんだよなぁ。
なんかいい書き方はないものか。あとからソートすればいいのはわかるんだけど。