こんにちは、KOUKIです。
Reactで画面開発をハンズオン形式で紹介しています。
今回は、Lazy Loading機能の実装を行いましょう。
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
前回
前回は、ソート機能を実装しました。
Lazy Loading機能
Lazy loadingとは画像がビューポート外にある時は読み込みを実行せず、ビューポートに近づいた時に画像の読み込みを開始する、表示速度を最適化する名称のことです。
今回は、Lazy Loading用のボタンが押された時に、残りのコンテンツを出すように実装します。
実は、以前API側で以下の実装を行いました。
URLの末尾に「page」キーワードをつけ、ページを示す番号とともにリクエストを送ると、指定したページのプロダクトが取得できます。この機能を利用するわけです。
1 2 |
// 例 http://localhost:8000/api/ambassador/products/backend?page=1 |
Filtersモデルの修正
Filtersモデルに、Lazy Loading用のパラメータを追加します。
1 2 3 4 5 6 |
// models/filters.ts export interface Filters { q: string sort: string page: number // 追加 } |
この変更に伴い、Filtersを型にした処理の修正を行います。
1 2 3 4 5 6 7 8 9 10 |
// pages/ProductsFrontend.tsx ... const ProductsFrontend = () => { ... const [filters, setFilters] = useState<Filters>({ q: '', sort: '', page: 1, // 追加 }) } |
1 2 3 4 5 6 7 8 9 10 11 |
// pages/ProductsBackend.tsx ... const ProductsBackend = () => { ... const [filters, setFilters] = useState<Filters>({ q: '', sort: '', page: 1, // 追加 }) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// pages/Products.tsx ... const Products = (props: { products: Product[] filters: Filters, setFilters: (filters: Filters) => void }) => { const search = (q: string) => { props.setFilters({ ...props.filters, page: 1, // 追加 q }) } const sort = (sort: string) => { props.setFilters({ ...props.filters, page: 1, // 追加 sort }) } ... } |
Lazy Loadingボタンの設置
Lazy Loadingボタンを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// pages/Products.tsx ... const Products = (props: { ... return ( <> ... <div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> ... </div> <div className="d-flex justify-content-center mt-4"> <button className="btn btn-primary">Load More</button> </div> </> ) } export default Products |

Load関数の実装
次に、Load関数を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// pages/Products.tsx ... const Products = (props: { ... const load = () => { props.setFilters({ ...props.filters, page: props.filters.page + 1 }) } return ( <> ... <div className="d-flex justify-content-center mt-4"> <button className="btn btn-primary" onClick={load}>Load More</button> </div> </> ) } export default Products |
Load MoreボタンにonClickハンドラーを設置したことで、Load関数を実行できるようになりました。
Load関数の追加により、ProductsFrontend/Backendの修正が必要になります。
Frontend
Frontendは、APIを利用しないため、全ての処理をブラウザ上で行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// pages/ProductsFrontend.tsx ... const ProductsFrontend = () => { ... const perPage = 9 // 追加 useEffect(() => { ... }, []) useEffect(() => { let products = allProducts.filter( p => p.title.toLocaleLowerCase().indexOf( filters.q.toLowerCase()) >= 0 || p.description.toLowerCase().indexOf( filters.q.toLowerCase()) >= 0) if (filters.sort === 'asc') { ... } else if (filters.sort === 'desc') { ... } setFilterProducts(products.slice(0, filters.page * perPage)) // 修正 }, [filters]) return (...) } export default ProductsFrontend |
Backend
Backendは、API側へリクエストを送り、指定したページの商品を受け取ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// pages/ProductsBackend.tsx ... const ProductsBackend = () => { ,,, useEffect(() => { ( async () => { const arr = [] if (filters.q) { arr.push(`q=${filters.q}`) } if (filters.sort) { arr.push(`sort=${filters.sort}`) } if (filters.page) { // 追加 arr.push(`page=${filters.page}`) } const {data} = await axios.get( backendUrl + '?' + arr.join('&')) if (data.data) {// 修正 setProducts(filters.page === 1 ? data.data : [...products, ...data.data]) } } )() }, [filters]) ... } export default ProductsBackend |
検証1 ~ ページ読み込み確認 ~
Load Moreボタンの表示切り替え
Load Moreボタンは、新たに読み込めるProductがある場合のみ表示させたいです。
そのための、処理を実装しましょう。
まずは、ProductFrontend/BackendからProductsへ最終ページを渡せるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// pages/ProductsFrontend.tsx ... const ProductsFrontend = () => { ... const [lastPage, setLastPage] = useState(0) // 追加 const perPage = 9 // 追加 useEffect(() => { ( async () => { const {data} = await axios.get(frontendUrl) if (data) { setAllProducts(data) setFilterProducts(data) setLastPage(Math.ceil(data.length / perPage)) // 追加 } } )() }, []) useEffect(() => { ... if (filters.sort === 'asc') { ... } else if (filters.sort === 'desc') { ... } setLastPage(Math.ceil(products.length / perPage)) // 追加 setFilterProducts(products.slice(0, filters.page * perPage)) }, [filters]) return ( <Layout> <Products products={filterProducts} filters={filters} setFilters={setFilters} lastPage={lastPage} // 追加 /> </Layout> ) } export default ProductsFrontend |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// pages/ProductsBackend.tsx ... const ProductsBackend = () => { ... const [lastPage, setLastPage] = useState(0) // 追加 useEffect(() => { ( async () => { ... if (data.data) { console.log(data) setProducts(filters.page === 1 ? data.data : [...products, ...data.data]) setLastPage(data.last_page); // 追加 } } )() }, [filters]) return ( <Layout> <Products products={products} filters={filters} setFilters={setFilters} lastPage={lastPage} // 追加 /> </Layout> ) } export default ProductsBackend |
次に、Products.txsの処理を変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// pages/Products.tsx ... const Products = (props: { products: Product[] filters: Filters, setFilters: (filters: Filters) => void, lastPage: number }) => { ... let button if (props.filters.page != props.lastPage) { button = ( <div className="d-flex justify-content-center mt-4"> <button className="btn btn-primary" onClick={load}>Load More</button> </div> ) } return ( <> ... <div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> ... </div> {button} // 変更 </> ) } export default Products |
これで、最終ページになった時、Load Moreボタンは表示されなくなりました。
検証2 ~ Load Moreボタンの表示切り替え ~
次回
次回は、プロダクトの選択とLink生成機能の実装をしましょう。
Reactまとめ
参考書籍
ソースコード
Products.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
// pages/Products.tsx import { Product } from '../models/product' import { Filters } from '../models/filters' const Products = (props: { products: Product[] filters: Filters, setFilters: (filters: Filters) => void, lastPage: number }) => { const search = (q: string) => { props.setFilters({ ...props.filters, page: 1, q }) } const sort = (sort: string) => { props.setFilters({ ...props.filters, page: 1, sort }) } const load = () => { props.setFilters({ ...props.filters, page: props.filters.page + 1 }) } let button if (props.filters.page != props.lastPage) { button = ( <div className="d-flex justify-content-center mt-4"> <button className="btn btn-primary" onClick={load}>Load More</button> </div> ) } return ( <> <div className="col-md-12 mb-4 input-group"> <input type="text" className="form-control" placeholder="Search" onChange={e => search(e.target.value)} /> <div className="input-group-append"> <select className="form-select" onChange={e => sort(e.target.value)}> <option>Select</option> <option value="asc">Price Ascending</option> <option value="desc">Price Descending</option> </select> </div> </div> <div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> {props.products.map(product => { return ( <div className="col" key={product.id}> <div className="card shadow-sm"> <img src={product.image} height={200} /> <div className="card-body"> <p className="card-text"> {product.title} </p> <div className="d-flex justify-content-between align-items-center"> <small className="text-muted">${product.price}</small> </div> </div> </div> </div> ) })} </div> {button} </> ) } export default Products |
ProductsFrontend.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// pages/ProductsFrontend.tsx import { useEffect, useState } from 'react' import { Product } from '../models/product' import { Filters } from '../models/filters' import Layout from '../components/Layout' import Products from '../pages/Products' import axios from 'axios' const ProductsFrontend = () => { const frontendUrl = 'products/frontend' const [filterProducts, setFilterProducts] = useState<Product[]>([]) const [allProducts, setAllProducts] = useState<Product[]>([]) const [filters, setFilters] = useState<Filters>({ q: '', sort: '', page: 1, }) const [lastPage, setLastPage] = useState(0) const perPage = 9 useEffect(() => { ( async () => { const {data} = await axios.get(frontendUrl) if (data) { setAllProducts(data) setFilterProducts(data) setLastPage(Math.ceil(data.length / perPage)) } } )() }, []) useEffect(() => { let products = allProducts.filter( p => p.title.toLocaleLowerCase().indexOf( filters.q.toLowerCase()) >= 0 || p.description.toLowerCase().indexOf( filters.q.toLowerCase()) >= 0) if (filters.sort === 'asc') { products.sort((a: Product, b: Product) => { if (a.price > b.price) { return 1 } if (a.price < b.price) { return -1 } return 0 }) } else if (filters.sort === 'desc') { products.sort((a: Product, b: Product) => { if (a.price > b.price) { return -1 } if (a.price < b.price) { return 1 } return 0 }) } setLastPage(Math.ceil(products.length / perPage)) setFilterProducts(products.slice(0, filters.page * perPage)) }, [filters]) return ( <Layout> <Products products={filterProducts} filters={filters} setFilters={setFilters} lastPage={lastPage} /> </Layout> ) } export default ProductsFrontend |
ProductsBackend.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// pages/ProductsBackend.tsx import { useEffect, useState } from 'react' import { Product } from '../models/product' import { Filters } from '../models/filters' import Layout from '../components/Layout' import Products from '../pages/Products' import axios from 'axios' const ProductsBackend = () => { const backendUrl = 'products/backend' const [products, setProducts] = useState<Product[]>([]) const [filters, setFilters] = useState<Filters>({ q: '', sort: '', page: 1, }) const [lastPage, setLastPage] = useState(0) useEffect(() => { ( async () => { const arr = [] if (filters.q) { arr.push(`q=${filters.q}`) } if (filters.sort) { arr.push(`sort=${filters.sort}`) } if (filters.page) { arr.push(`page=${filters.page}`) } const {data} = await axios.get( backendUrl + '?' + arr.join('&')) if (data.data) { console.log(data) setProducts(filters.page === 1 ? data.data : [...products, ...data.data]) setLastPage(data.last_page); } } )() }, [filters]) return ( <Layout> <Products products={products} filters={filters} setFilters={setFilters} lastPage={lastPage} /> </Layout> ) } export default ProductsBackend |
filters.ts
1 2 3 4 5 6 |
// models/filters.ts export interface Filters { q: string sort: string page: number } |
コメントを残す
コメントを投稿するにはログインしてください。