Open6
URQLで無限スクロール
https://github.com/danbovey/react-infinite-scroller を使ってみる
package.json
"react-infinite-scroller": "^1.2.6",
キャッシュの設定はこんな感じ
const App: FC = () => {
const client = createClient({
// 省略
exchanges: [
dedupExchange,
cacheExchange({
resolvers: {
Query: {
posts: relayPagination(),
},
},
}),
fetchExchange,
],
})
投稿一覧を表示してみる
import { Suspense, useState } from "react"
import type { FC } from "react"
import { useQuery, gql } from "urql"
import InfiniteScroll from "react-infinite-scroller"
import Box from "@mui/material/Box"
import Container from "@mui/material/Container"
import Grid from "@mui/material/Grid"
import { PostCard } from "components/PostCard/PostCard"
import { GetPostsQuery } from "generated/types"
import style from "./index.module.scss"
const TopPage: FC = () => {
const [variables, setVariables] = useState<{
first: number
limit: number
after: string | null | undefined
}>({ first: 12, limit: 12, after: "" })
const [{ data, fetching }] = useQuery<GetPostsQuery>({
query: gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
description
thumbnailUrl(size: S)
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
variables,
})
const loadMore = () => {
if (fetching) {
return
}
setVariables({
...variables,
after: data?.posts.pageInfo.endCursor,
})
}
return (
<Box component="div" className={style.postsBackground} sx={{ pt: 3 }}>
<Container maxWidth="lg">
<Suspense fallback={"loading..."}>
{data && (
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={data.posts.pageInfo.hasNextPage}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
>
<Grid container spacing={2}>
{data.posts?.edges?.map(
(edge) =>
edge?.node && (
<Grid item key={edge.node.id} xs={6} sm={3}>
<PostCard
title={edge.node.title}
src={edge.node.thumbnailUrl}
href={`/posts/${edge.node.id}`}
/>
</Grid>
)
)}
</Grid>
</InfiniteScroll>
)}
</Suspense>
</Container>
</Box>
)
}
export default TopPage
動いたけれど、キャッシュの設定を追加したくない(exchangesにこの設定を書いていると他の個所でドキュメントキャッシュが動作しなかった)
URQLのドキュメントにあるUIパターンみたいに書きたい
ただ、この例はボタンを押したら次の一覧がロードされる例で、一番下までスクロールしたら自動で読み込む処理はない。
ごちゃついたコードだけど、似たことができた
同じ要素が何度も表示される問題に困っていたが、exchangesのキャッシュの設定を消していなかったからだった
ただ、ページ表示時に2回GraphqlApiからデータを取得してしまっている(つまり12*2=24件目まで取得している)
なぜ。。。
import { Suspense, useState, useEffect } from "react"
import type { FC, Dispatch, SetStateAction } from "react"
import { useQuery, gql } from "urql"
import InfiniteScroll from "react-infinite-scroller"
import Box from "@mui/material/Box"
import Container from "@mui/material/Container"
import Grid from "@mui/material/Grid"
import { PostCard } from "components/PostCard/PostCard"
import { GetPostsQuery } from "generated/types"
import style from "./index.module.scss"
interface ResultPostCardsProps {
variables: {
first: number
limit: number
after: string | null | undefined
}
setPageInfo: Dispatch<
SetStateAction<{
endCursor: string
hasNextPage: boolean
}>
>
}
const ResultPostCards: FC<ResultPostCardsProps> = (props) => {
const variables = props.variables
const [{ data, fetching }] = useQuery<GetPostsQuery>({
query: gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
description
thumbnailUrl(size: S)
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
variables,
})
useEffect(() => {
if (data?.posts.pageInfo.endCursor && data?.posts.pageInfo) {
console.log("setPageInfo")
props.setPageInfo({
endCursor: data.posts.pageInfo.endCursor,
hasNextPage: data.posts.pageInfo.hasNextPage,
})
}
}, [data?.posts.pageInfo.endCursor, data?.posts.pageInfo])
return (
<>
{data?.posts?.edges?.map(
(edge) =>
edge?.node && (
<Grid item key={edge.node.id} xs={6} sm={3}>
<PostCard
title={edge.node.title}
src={edge.node.thumbnailUrl}
href={`/posts/${edge.node.id}`}
/>
</Grid>
)
)}
</>
)
}
const TopPage: FC = () => {
const [pageVariablesList, setPageVariablesList] = useState<
{
first: number
limit: number
after: string | null | undefined
}[]
>([{ first: 12, limit: 12, after: "" }])
const [pageInfo, setPageInfo] = useState<{
endCursor: string
hasNextPage: boolean
}>({ endCursor: "", hasNextPage: true })
const loadMore = () => {
console.log("loadMore")
console.log(pageVariablesList)
if (
pageVariablesList[pageVariablesList.length - 1].after !=
pageInfo.endCursor
) {
setPageVariablesList([
...pageVariablesList,
{ first: 12, limit: 12, after: pageInfo.endCursor },
])
}
}
return (
<Box component="div" className={style.postsBackground} sx={{ pt: 3 }}>
<Container maxWidth="lg">
<Suspense fallback={"loading..."}>
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={pageInfo.hasNextPage}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
>
<Grid container spacing={2}>
{pageVariablesList.map((pageVariables, index) => (
<ResultPostCards
key={index}
variables={pageVariables}
setPageInfo={setPageInfo}
/>
))}
</Grid>
</InfiniteScroll>
</Suspense>
</Container>
</Box>
)
}
export default TopPage
IntersectionObserverを使ってInfiniteScrollを自作してみた
loadMoreが実行されたときにloadMore内で使われているState変数の値が初期値になっている。
おそらくコールバック関数がnew IntersectionObserver
で登録されたときに固定されてしまってそう。
import { Suspense, useState, useEffect } from "react"
import type { FC, Dispatch, SetStateAction, ReactNode } from "react"
import { useQuery, gql } from "urql"
// import InfiniteScroll from "react-infinite-scroller"
import Box from "@mui/material/Box"
import Container from "@mui/material/Container"
import Grid from "@mui/material/Grid"
import { PostCard } from "components/PostCard/PostCard"
import { GetPostsQuery } from "generated/types"
import style from "./index.module.scss"
interface ResultPostCardsProps {
variables: {
first: number
limit: number
after: string | null | undefined
}
setPageInfo: Dispatch<
SetStateAction<{
endCursor: string
hasNextPage: boolean
}>
>
}
const ResultPostCards: FC<ResultPostCardsProps> = (props) => {
const variables = props.variables
const [{ data, fetching }] = useQuery<GetPostsQuery>({
query: gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
description
thumbnailUrl(size: S)
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
variables,
})
useEffect(() => {
if (
data?.posts.pageInfo.endCursor &&
data?.posts.pageInfo.endCursor !== "" &&
data?.posts.pageInfo
) {
console.log("setPageInfo")
console.log({
endCursor: data.posts.pageInfo.endCursor,
hasNextPage: data.posts.pageInfo.hasNextPage,
})
props.setPageInfo({
endCursor: data.posts.pageInfo.endCursor,
hasNextPage: data.posts.pageInfo.hasNextPage,
})
}
}, [data?.posts.pageInfo.endCursor, data?.posts.pageInfo])
return (
<>
{data?.posts?.edges?.map(
(edge) =>
edge?.node && (
<Grid item key={edge.node.id} xs={6} sm={3}>
<PostCard
title={edge.node.title}
src={edge.node.thumbnailUrl}
href={`/posts/${edge.node.id}`}
/>
</Grid>
)
)}
</>
)
}
interface InfiniteScrollProps {
children: ReactNode
loadMore: () => void
hasMore: boolean
loader: ReactNode
}
const InfiniteScroll: FC<InfiniteScrollProps> = (props) => {
useEffect(() => {
const el = document.getElementById("InfiniteScrollLoader")
const observer = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
console.log("loadMore")
props.loadMore()
}
}
})
if (el) {
observer.observe(el)
}
}, [])
return (
<>
{props.children}
<div id="InfiniteScrollLoader">{props.loader}</div>
</>
)
}
const TopPage: FC = () => {
const [pageVariablesList, setPageVariablesList] = useState<
{
first: number
limit: number
after: string | null | undefined
}[]
>([{ first: 12, limit: 12, after: "" }])
const [pageInfo, setPageInfo] = useState<{
endCursor: string | undefined
hasNextPage: boolean
}>({ endCursor: undefined, hasNextPage: true })
const loadMore = () => {
console.log("loadMore")
console.log(pageVariablesList)
console.log(pageInfo)
if (
pageVariablesList[pageVariablesList.length - 1].after !=
pageInfo.endCursor
) {
setPageVariablesList([
...pageVariablesList,
{ first: 12, limit: 12, after: pageInfo.endCursor },
])
}
}
console.log("aaa")
console.log(pageInfo)
return (
<Box component="div" className={style.postsBackground} sx={{ pt: 3 }}>
<Container maxWidth="lg">
<Suspense fallback={"loading..."}>
<InfiniteScroll
loadMore={loadMore}
hasMore={pageInfo.hasNextPage}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
>
<Grid container spacing={2}>
{pageVariablesList.map((pageVariables, index) => (
<ResultPostCards
key={index}
variables={pageVariables}
setPageInfo={setPageInfo}
/>
))}
</Grid>
</InfiniteScroll>
</Suspense>
</Container>
</Box>
)
}
export default TopPage
react-intersection-observer使ってみてうまく動いてそう
import { Suspense, useState, useEffect } from "react"
import type { FC, Dispatch, SetStateAction, ReactNode } from "react"
import { useQuery, gql } from "urql"
// import InfiniteScroll from "react-infinite-scroller"
import Box from "@mui/material/Box"
import Container from "@mui/material/Container"
import Grid from "@mui/material/Grid"
import { InView } from "react-intersection-observer"
import { PostCard } from "components/PostCard/PostCard"
import { GetPostsQuery } from "generated/types"
import style from "./index.module.scss"
interface ResultPostCardsProps {
variables: {
first: number
limit: number
after: string | null | undefined
}
setPageInfo: Dispatch<
SetStateAction<{
endCursor: string
hasNextPage: boolean
}>
>
}
const ResultPostCards: FC<ResultPostCardsProps> = (props) => {
const variables = props.variables
const [{ data, fetching }] = useQuery<GetPostsQuery>({
query: gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
description
thumbnailUrl(size: S)
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
variables,
})
useEffect(() => {
if (data?.posts.pageInfo.endCursor && data?.posts.pageInfo) {
console.log("setPageInfo")
console.log({
endCursor: data.posts.pageInfo.endCursor,
hasNextPage: data.posts.pageInfo.hasNextPage,
})
props.setPageInfo({
endCursor: data.posts.pageInfo.endCursor,
hasNextPage: data.posts.pageInfo.hasNextPage,
})
}
}, [data?.posts.pageInfo.endCursor, data?.posts.pageInfo])
return (
<>
{data?.posts?.edges?.map(
(edge) =>
edge?.node && (
<Grid item key={edge.node.id} xs={6} sm={3}>
<PostCard
title={edge.node.title}
src={edge.node.thumbnailUrl}
href={`/posts/${edge.node.id}`}
/>
</Grid>
)
)}
</>
)
}
interface InfiniteScrollProps {
children: ReactNode
loadMore: () => void
hasMore: boolean
loader: ReactNode
}
const InfiniteScroll: FC<InfiniteScrollProps> = (props) => {
return (
<>
{props.children}
<InView
as="div"
onChange={(inView, entry) => {
if (inView && props.hasMore) {
props.loadMore()
}
}}
>
{props.loader}
</InView>
</>
)
}
const TopPage: FC = () => {
const [pageVariablesList, setPageVariablesList] = useState<
{
first: number
limit: number
after: string | null | undefined
}[]
>([{ first: 12, limit: 12, after: "" }])
const [pageInfo, setPageInfo] = useState<{
endCursor: string
hasNextPage: boolean
}>({ endCursor: "", hasNextPage: true })
const loadMore = () => {
console.log("loadMore")
console.log(pageVariablesList)
console.log(pageInfo)
if (
pageVariablesList[pageVariablesList.length - 1].after !=
pageInfo.endCursor
) {
setPageVariablesList([
...pageVariablesList,
{ first: 12, limit: 12, after: pageInfo.endCursor },
])
}
}
console.log("aaa")
console.log(pageInfo)
return (
<Box component="div" className={style.postsBackground} sx={{ pt: 3 }}>
<Container maxWidth="lg">
<Suspense fallback={"loading..."}>
<InfiniteScroll
loadMore={loadMore}
hasMore={pageInfo.hasNextPage}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
>
<Grid container spacing={2}>
{pageVariablesList.map((pageVariables, index) => (
<ResultPostCards
key={index}
variables={pageVariables}
setPageInfo={setPageInfo}
/>
))}
</Grid>
</InfiniteScroll>
</Suspense>
</Container>
</Box>
)
}
export default TopPage
もうちょっと細かいとこ詰めたら実際に使えそう