如何防止组件在切换浏览器选项卡时重新呈现?

How to prevent component re-render on switching browser tabs?

在我的 Next.js 应用程序中,当我更改浏览器选项卡然后返回到应用程序已打开的选项卡时,组件会重新呈现。例如应用程序打开选项卡 1,当我切换到选项卡 2 然后返回到选项卡 1 时。

实际上,我有一个显示记录列表的页面,所以当我使用文本匹配进行本地过滤器时,它工作正常。但是当我更改选项卡并返回到应用程序选项卡时,它会再次重置列表。

当我用文本过滤位置时,它会进行过滤。

但是当我切换标签时它会重置结果。

我正在使用 useSwr 来获取数据和显示列表。下面是组件代码:

import useSWR from 'swr'
import Link from 'next/link'
import Httpservice from '@/services/Httpservice'
import { useState, useEffect, useCallback } from 'react'
import NavBar from '@/components/NavBar'
import Alert from 'react-bootstrap/Alert'
import Router, { useRouter } from 'next/router'
import NoDataFound from '@/components/NoDataFound'

import nextConfig from 'next.config'

import { useTranslation, useLanguageQuery, LanguageSwitcher } from 'next-export-i18n'

export default function Locations({...props}) {
    const router = useRouter()
    const { t } = useTranslation()
    const [queryLanguage] = useLanguageQuery()
    const httpService = new Httpservice
    const pageLimit = nextConfig.PAGE_LIMIT

    const [loading,setLoading] = useState(true)
    const [pageIndex, setPageIndex] = useState(1)
    const [locations, setLocations] = useState([])
    const [searchText, setSearchText] = useState('')
    const [locationId, setLocationId] = useState(null)
    const [isExpanding, setIsExpending] = useState(null)
    const [loadMoreBtn, setLoadMoreBtn] = useState(true)
    const [locationName, setLocationName] = useState(null)
    const [errorMessage, setErrorMessage] = useState(null)
    const [tempLocations, setTempLocations] = useState([])
    const [deleteMessage, setDeleteMessage] = useState(null)
    const [successMessage, setSuccessMessage] = useState(null)
    
    const [displayConfirmationModal, setDisplayConfirmationModal] = useState(false)

    const showDeleteModal = (locationName, locationId) => {
        setLocationName(locationName)
        setLocationId(locationId)
        setSuccessMessage(null)
        setErrorMessage(null)
        setDeleteMessage(`Are you sure you want to delete the '${locationName}'?`)
        
        setDisplayConfirmationModal(true)
    }

    const hideConfirmationModal = () => {
        setDisplayConfirmationModal(false)
    }

    const locationsFetcher = async() => {
        try{
            await httpService.get(`/v1/locations?page=${pageIndex}&limit=${pageLimit}`).then((response) => {

                if(response.status == 200 && response.data) {
                    let data = response.data.results
                    
                    setLocations([...new Set([...locations,...data])])
                    setTempLocations([...new Set([...locations,...data])])
    
                    if(response.data.next == undefined && response.data.results.length == 0) {
                        setLoadMoreBtn(false)
                    }
                    setLoading(false)
                    setIsExpending(null)
                    return data
                } else {
                    setLoading(false)
                    setIsExpending(null)
                    const error = new Error('An error occurred while fetching the data.')
                    error.info = response.json()
                    error.status = response.status
                    throw error
                }
            }).catch((error) => {
                setLoading(false)
                setIsExpending(null)
            })
        } catch (error) {
            setLoading(false)
            setIsExpending(null)
        }
    }
    
    const {data, error} = useSWR(`/v1/locations?page=${pageIndex}&limit=${pageLimit}`, locationsFetcher,{
        onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
            if (error.status === 404) return
            if (retryCount >= 10) return
            
            setTimeout(() => revalidate({ retryCount }), 5000)
        }
    })

    

    const loadMore = () => {
        setPageIndex(pageIndex + 1)
        setIsExpending(true)
    }

    const handleSearch = (e) => {
        let searchKey = e.target.value
        setSearchText(e.target.value)
        
        if(searchKey.length > 0) {
            console.log(tempLocations)
            let foundValue = tempLocations.filter(location => location.name.toLowerCase().includes(searchText.toLowerCase()))
            if(foundValue) {
                setLoadMoreBtn(false)
                setLocations(foundValue)
            } else {
                setLoadMoreBtn(true)
                setLocations(tempLocations)
            }
        } else {
            setLoadMoreBtn(true)
            setLocations(tempLocations)
        }
    }

    return (
        <>
            <NavBar />
            <div className="app-wrapper">
                <div className="app-content pt-3 p-md-3 p-lg-4">
                    <div className="container-xl">
                        <div className="row gy-4  mb-2">
                            <div className="col-12 col-lg-8">
                                <h1 className="page-head-title"> {t('locations')} </h1>
                            </div>
                        </div>
                        <div className="summary_col">
                            <div className="row gy-4">
                                <div className="col-12 col-lg-12">

                                    <div className="dotted float-end">
                                        <a href="javascript:void(0)">
                                            <img src="/images/icons/dotted.png" width="16" height="4" alt=""  />
                                        </a>
                                    </div>
                                </div>
                            </div>
                            <div className="row gy-4 mt-2">
                                <div className="col-6 col-lg-3 col-md-4">
                                    <div className="input-group search_col">
                                        <div className="form-outline ">
                                            <input type="search" className="form-control" placeholder={t('search')} value={searchText} onChange={handleSearch} />
                                        </div>
                                        <button type="button" className="btn">
                                            <img src="/images/icons/search.png" width="19" height="19" alt=""  />
                                        </button>
                                    </div>
                                </div>

                                <div className="col-6 col-lg-9 col-md-8 ">
                                    <Link href={{ pathname: '/settings/locations/add', query: (router.query.lang) ? 'lang='+router.query.lang : null }}>
                                        <a className="btn btn-primary float-end">{t('location_page.add_location')}</a>
                                    </Link>
                                </div>
                            </div>
                            <div className="row gy-4 mt-2">
                                <div className="col-12 col-lg-12">
                                    <div className="vehicles_col table-responsive">
                                        
                                        <table className="table" width="100%" cellPadding="0" cellSpacing="0">
                                            <thead>
                                            <tr>
                                                <th>{t('location_page.name')}</th>
                                                <th>{t('location_page.company')}</th>
                                                <th>{t('location_page.contact')}</th>
                                                <th>{t('location_page.email')}</th>
                                                <th>{t('location_page.phone')}</th>
                                                <th>{t('location_page.address')}</th>
                                                <th>{t('detail')}</th>
                                            </tr>
                                            </thead>
                                            <tbody>
                                            {error && <tr><td><p>{t('error_in_loading')}</p></td></tr>}
                                            {(loading) ? <tr><td colSpan="6"><p>{t('loading')}</p></td></tr> : 
                                            (locations && locations.length > 0) ? (locations.map((location, index) => (
                                                <tr index={index} key={index}>
                                                    <td>{location.name}</td>
                                                    <td>
                                                        <a href="javascript:void(0)">
                                                            {(location.links && location.links.Company) ? location.links.Company : '-'}
                                                        </a>
                                                    </td>
                                                    <td>{location.contact}</td>
                                                    <td>{location.email}</td>
                                                    <td>{location.phone}</td>
                                                    <td>
                                                        {(location.address1) ? location.address1 : ''} 
                                                        {(location.address2) ? ','+location.address2 : ''} 
                                                        {(location.address3) ? ','+location.address3 : ''} 
                                                        <br />
                                                        {(location.city) ? location.city : ''} 
                                                        {(location.state) ? ','+location.state : ''} 
                                                        {(location.country) ? ','+location.country : ''} 
                                                        {(location.zip) ? ','+location.zip : ''} 
                                                    </td>
                                                    <td>
                                                    
                                                        <Link href={{ pathname: '/settings/locations/edit/'+ location.UUID, query: (router.query.lang) ? 'lang='+router.query.lang : null }}>
                                                            {t('view')}
                                                        </Link>
                                                        
                                                    </td>
                                                </tr>
                                            ))) : (<tr><td><NoDataFound  /></td></tr>)}
                                            </tbody>
                                        </table>
                                        <div className="click_btn">
                                            {(loadMoreBtn) ? (isExpanding) ? t('loading') : <a href="javascript:void(0)" onClick={() => loadMore()}>
                                                <span>{t('expand_table')}</span>
                                            </a> : t('no_more_data_avail')}
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            
        </>
    )
}

有两个调试点我建议首先尝试,但我相信问题不是由这个组件引起的。

export default function Locations({...props}) {
  console.log('Render')

    const locationsFetcher = async() => {
      console.log('Fetch')

以上是切换标签时的确认,

  • 如果 Locations 组件重绘
  • 如果 locationsFetcher 重新发射

以上问题将帮助您进一步挖掘。我的直觉是你的代码中有另一段检测选项卡切换,例如。收听页面是否活跃。因为默认情况下此 Locations 组件不应自行重绘。

默认情况下,当您重新聚焦页面或在选项卡之间切换时,useSWRautomatically revalidate 数据。这就是导致重新渲染的原因。

您可以通过 useSWR 调用中的 options 对象禁用此行为,方法是将 revalidateOnFocus 字段设置为 false

useSWR(`/v1/locations?page=${pageIndex}&limit=${pageLimit}`, locationsFetcher, {
    onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
        if (error.status === 404) return
        if (retryCount >= 10) return
            
        setTimeout(() => revalidate({ retryCount }), 5000)
    },
    revalidateOnFocus: false
})

或者,您可以使用 useSWRImmutable(而不是 useSWR)禁用 SWR 完成的各种自动重新验证。

import useSWRImmutable from 'swr/immutable'

// ...
useSWRImmutable(key, fetcher, options)

这与调用基本相同:

useSWR(key, fetcher, {
    // other options here
    revalidateIfStale: false,
    revalidateOnFocus: false,
    revalidateOnReconnect: false
})