Search code examples
reactjsnext.jsreact-bootstrapinfinite-scrollswr

useSWR conditional fetch and react-boostrap Accordion


trying to load youtube comments into a infinite load component (using a npm for it)

the mess happens due to the fact the infinite load component is child of parent Accordion component (from react-bootstrap), and what I'm trying to achieve is fetching with useSWR only if Accordion gets clicked (opened).

What I tried is to use useSWR conditional, so that only fetches when state "show" is true, which is being set inside function:

const showComments = () => {
    setShow(true)
    if (comments) {
      setCommChunks(_.chunk(comments.comm, 10))
      setCommList(commChunks[counter])
    }
  }

called on Accordion.Toggle onClick event.

But what happens is I can only show the comments after I click the Accordion twice, why is that?

My code is:

import { useState, useEffect } from 'react'
import { Row, Col, Button, Accordion } from 'react-bootstrap'
import * as _ from 'lodash'
import useSWR from 'swr'
import { MdUnfoldMore } from 'react-icons/md'
import InfiniteScroll from "react-infinite-scroll-component"
import Comments from './Comments'

const siteurl = process.env.NEXT_PUBLIC_SITE_URL

export default function VideoComments({ video }){
    
  const [show, setShow] = useState(false)
  const [counter, setCounter] = useState(0)
  const [commList, setCommList] = useState(null)
  const [commChunks, setCommChunks] = useState([])

  const showComments = () => {
    setShow(true)
    if (comments) {
      setCommChunks(_.chunk(comments.comm, 10))
      setCommList(commChunks[counter])
    }
  }

  const fetcher = (...args) => fetch(...args).then(res => res.json())
  const { data: comments, error } = useSWR(show ? `${siteurl}/api/c/${video.id}` : null, fetcher)
  
  // useEffect(() => {
  //   if (comments) {
  //     commChunks = _.chunk(comments.comm, 10)
  //     setCommList(commChunks[counter])
  //   }
  // },[comments])

  const fetchMoreData = () => {
    const newCounter = counter + 1;

    // loaded all, return
    if (commChunks[newCounter] === undefined || commChunks[newCounter] == null) {
        return;
    }

    const newCommList = [
        ...commList,
        ...commChunks[newCounter]
    ]
    setCommList(newCommList)
    setCounter(newCounter)
  }

  return (
    <div>
      <Accordion>
        <Row>
          <Col xs={12}>
            <Accordion.Toggle as={Button} onClick={() => {showComments()}} variant="link" eventKey="0"><div><span>Comments</span></div></Accordion.Toggle>
          </Col>
        </Row>
        <Accordion.Collapse eventKey="0">
          <div id="commentsBox" style={{maxHeight: '300px', overflowY: 'auto'}}>
            <Col xs={12}>
              {commList &&
                <InfiniteScroll
                    dataLength={commList.length}
                    next={fetchMoreData}
                    hasMore={true}
                    scrollableTarget="commentsBox"
                >
                  <Comments data={commList} />
                </InfiniteScroll>
              }
            </Col>
          </div>
        </Accordion.Collapse>
      </Accordion>
    </div>
  );
}

EDIT: as suggested below I reactivated useEffect, but it still needs two clicks of the Accordion

const showComments = () => {
    setShow(true)
    if (comments) {
      setCommChunks(_.chunk(comments.comm, 10))
      setCommList(commChunks[counter])
    }
  }

  const { data: comments } = useSWR(show ? `${siteurl}/api/c/${video.id}` : null, fetcher)

  useEffect(() => {
    if (comments) {
      setCommChunks(_.chunk(comments.comm, 10))
      setCommList(commChunks[counter])
    }
  },[comments])

Solution

  • The issue is in your useEffect, calling setCommList(commChunks[counter]) right after modifying commChunks state won't have the updated value. Setting state in React is an asynchronous operation (see React setState not updating immediately).

    You should save the comments in a block-scoped variable and use that to update both states consecutively.

    useEffect(() => {
        if (comments) {
            const commentsChunks = _.chunk(comments.comm, 10)
            setCommChunks(commentsChunks)
            setCommList(commentsChunks[counter])
        }
    }, [comments])