I experienced a problem when rendering data in next.js where the data would be duplicated when SWR finished fetching data to the server. The scenario that happens is like this:
- When the secure data is first accessed, none of them are the same as each other.
- When you want to search for data and after completion the SWR gets the results from server 1, the data will still be rendered. even when I look for data that has nothing to do with it (in terms of keywords).
I think this is a problem with the SWR that I use. but I couldn't find where the error was, I thought that with debounce everything would be resolved, but it had no effect at all.
in a case I tried recently. even when I look for data and the response from the server is not there (data not found) there is a line that still appears as in the following image.
It can be seen that in fact there is no response from the server that matches the search criteria, but there is 1 line that is still displayed, and after that there is a message that the data is not there.
here is the code I have.
index.tsx (main components)
import React, { ReactElement, useEffect, useState } from "react"
import useSWR from "swr"
import dynamic from "next/dynamic"
import fetcherGet from "@/utils/fetcherGet"
import { useRouter } from "next/router"
import { useDebounce } from 'use-debounce';
import { IconPlus } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { NextPageWithLayout } from "@/pages/_app"
import { CardDescription, CardTitle } from "@/components/ui/card"
const AppLayout = dynamic(() => import('@/components/layouts/app'), { ssr: false })
const Loading1 = dynamic(() => import('@/components/custom/icon-loading'), { ssr: false })
const TableSuratMasuk = dynamic(() => import('@/components/custom/tables/surat-masuk'), { ssr: false })
const DialogMenuSuratMasuk = dynamic(() => import('@/components/custom/modals/dialog-menu-surat-masuk'), { ssr: false })
const DialogPreviewSuratMasuk = dynamic(() => import('@/components/custom/modals/dialog-preview-surat-masuk'), { ssr: false })
const SuratMasukPage: NextPageWithLayout = () => {
const router = useRouter()
const [fltrData, setFltrData] = useState({})
const [filterQuery, setFilterQuery] = useState('')
const [debouncedFilterQuery] = useDebounce(filterQuery, 1000)
const [isOpenMenu, setIsOpenMenu] = useState(false)
const [isOpenPreview, setIsOpenPreview] = useState(false)
const [selectedItem, setSelectedItem] = useState<any>({})
const fetcher = (url: string) => fetcherGet({ url, filterQuery })
const { data, error, mutate, isLoading, isValidating } = useSWR(`${process.env.NEXT_PUBLIC_API_URL}/surat/masuk`, fetcher, {
refreshWhenHidden: true,
revalidateOnFocus: false,
refreshWhenOffline: false,
})
useEffect(() => {
const count = Object.keys(fltrData).length
if (count > 0) {
let fq = '';
for (const [key, value] of Object.entries(fltrData)) {
if (value) {
fq += fq === '' ? `?${key}=${value}` : `&${key}=${value}`;
}
}
setFilterQuery(fq);
}
}, [fltrData]);
useEffect(() => {
mutate();
}, [debouncedFilterQuery, mutate]);
if (isLoading) return <Loading1 height={50} width={50} />
if (error) return <div>{error.message}</div>
if (!data) return <div>No data</div>
return (
<>
<div className="space-y-2">
<div className="flex justify-between items-center mb-4">
<div className="space-y-1">
<CardTitle>Surat Masuk</CardTitle>
<CardDescription>Data Surat Masuk | <strong>RSIA Aisyiyah Pekajangan</strong></CardDescription>
</div>
<Button size={'icon'} className="w-7 h-7" onClick={() => router.push('/surat/masuk/create')}>
<IconPlus className="w-5 h-5" />
</Button>
</div>
{data && (
<TableSuratMasuk
data={data}
filterData={fltrData}
setFilterData={setFltrData}
isValidating={isValidating}
setIsOpenPreview={setIsOpenPreview}
setSelectedItem={setSelectedItem}
lastColumnAction={true}
onRowClick={(row: any) => {
setSelectedItem(row)
setIsOpenMenu(true)
}}
/>
)}
</div>
{/* Preview */}
<DialogPreviewSuratMasuk
isOpenPreview={isOpenPreview}
setIsOpenPreview={setIsOpenPreview}
selectedItem={selectedItem}
/>
{/* Menu Surat */}
<DialogMenuSuratMasuk
mutate={mutate}
isOpenMenu={isOpenMenu}
setIsOpenMenu={setIsOpenMenu}
selectedItem={selectedItem}
/>
</>
)
}
SuratMasukPage.getLayout = function getLayout(page: ReactElement) {
return (
<AppLayout>{page}</AppLayout>
)
}
export default SuratMasukPage
surat-masuk.tsx (return filter components and table from another files)
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Combobox } from "../inputs/combo-box";
import { Button } from "@/components/ui/button";
import { IconBrandWhatsapp, IconFile, IconFileSearch, IconFileText, IconMail, IconPrinter } from "@tabler/icons-react";
import dynamic from "next/dynamic";
const LaravelPagingx = dynamic(() => import('@/components/custom-ui/laravel-paging'), { ssr: false })
interface tableSuratMasukProps {
data: any
filterData: any
setFilterData: any
isValidating?: boolean | undefined
setIsOpenPreview?: (value: boolean) => void
setSelectedItem?: (value: any) => void
onRowClick?: (row: any) => void;
lastColumnAction?: boolean | undefined
}
function getIconFromKetSurat(ket_surat: string) {
switch (ket_surat) {
case 'wa':
case 'whatsapp':
return <IconBrandWhatsapp className="w-5 h-5 dark:stroke-green-500 stroke-green-500" />
case 'fisik':
return <IconFileText className="w-5 h-5 dark:stroke-yellow-500 stroke-yellow-500" />
case 'email':
return <IconMail className="w-5 h-5 dark:stroke-red-500 stroke-red-500" />
case 'fax':
return <IconPrinter className="w-5 h-5 dark:stroke-blue-500 stroke-blue-500" />
case 'surat':
return <IconFileText className="w-5 h-5 dark:stroke-yellow-500 stroke-yellow-500" />
default:
return <IconFile className="w-5 h-5 dark:stroke-gray-500 stroke-gray-500" />
}
}
const TableSuratMasuk = (props: tableSuratMasukProps) => {
const {
data,
filterData,
setFilterData,
isValidating,
setIsOpenPreview = () => { },
setSelectedItem = () => { },
onRowClick = () => { },
lastColumnAction
} = props
const columns = [
{
name: 'No SIMRS',
selector: 'no_simrs',
data: (row: any) => <Badge variant={'outline'}>{new Date(row.no_simrs).toLocaleDateString('id-ID', { year: 'numeric', month: '2-digit', day: '2-digit' })}</Badge>,
},
{
name: 'Perihal',
selector: 'perihal',
enableHiding: false,
data: (row: any) => (
// Gunakan props di sini jika diperlukan
<div className="flex flex-row items-center gap-4">
<div>{row.ket ? getIconFromKetSurat(row.ket) : null}</div>
<div className="flex flex-col">
<span className="font-semibold">{row.perihal}</span>
<div>
{row.no_surat ? <Badge variant={'secondary'} className="mt-1">{row.no_surat}</Badge> : null}
</div>
</div>
</div>
),
},
{
name: 'Pengirim',
selector: 'pengirim',
data: (row: any) => <p className="text-sm">{row.pengirim}</p>,
},
{
name: 'Tanggal Surat',
selector: 'tgl_surat',
data: (row: any) => row.tgl_surat && row.tgl_surat != '0000-00-00' ? <p className="text-sm whitespace-nowrap">{new Date(row.tgl_surat).toLocaleDateString('id-ID', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
})}</p> : '-',
},
{
name: "#",
selector: 'preview',
data: (row: any) => (
// Gunakan props di sini jika diperlukan
<div className="w-full flex justify-end">
<Button
size="icon"
className="h-6 w-6"
disabled={!row.berkas || row.berkas == '-' || row.berkas == '' || row.berkas == ' '}
onClick={(e) => {
setSelectedItem(row);
setIsOpenPreview(true);
}}
>
<IconFileSearch className="w-4 h-4" />
</Button>
</div>
),
},
];
return (
<>
<div className="mt-4 mb-4 w-full flex flex-col md:flex-row items-center justify-end gap-4 p-4 rounded-xl bg-gray-100/50 dark:bg-gray-900/50 border border-border">
<div className="w-full space-y-1">
<Label>Dikirim Via</Label>
<Combobox
items={[
{ value: '', label: 'Semua' },
{ value: 'wa', label: 'WhatsApp' },
{ value: 'fisik', label: 'Fisik' },
{ value: 'email', label: 'Email' },
{ value: 'fax', label: 'FAX' },
]}
setSelectedItem={(item: any) => {
setFilterData({ ...filterData, via: item })
}}
selectedItem={filterData?.jenis}
placeholder="Dikirim Via"
/>
</div>
<div className="w-full space-y-1">
<Label>No. SIMRS</Label>
<Input
type="date"
className="w-full"
name='no_simrs'
onChange={(e) => {
setFilterData({ ...filterData, no_simrs: e.target.value })
}}
/>
</div>
<div className="w-full space-y-1">
<Label>Tanggal Surat</Label>
<Input
type="date"
className="w-full"
name='tgl_surat'
onChange={(e) => {
setFilterData({ ...filterData, tgl_surat: e.target.value })
}}
/>
</div>
<div className="w-full space-y-1">
<Label>Search</Label>
<Input
type="search"
placeholder="Search..."
className="w-full"
defaultValue={filterData?.keyword}
onChange={(e) => {
setFilterData({ ...filterData, keyword: e.target.value })
}}
/>
</div>
</div>
{data && (
<LaravelPagingx
data={data.data}
columnsData={columns}
filterData={filterData}
setFilterData={setFilterData}
isValidating={isValidating}
onRowClick={(item: any) => {
setSelectedItem(item)
onRowClick(item)
}}
lastColumnAction={lastColumnAction}
/>
)}
</>
);
}
export default TableSuratMasuk;
fether files
import { getSession } from "next-auth/react"
interface FetcherGetProps {
url: string
filterQuery?: string
}
const fetcherGet = async ({ url, filterQuery }: FetcherGetProps) => {
const session = await getSession()
const response = await fetch(url + filterQuery, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session?.rsiap?.access_token}`,
},
})
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText)
}
const jsonData = await response.json()
return jsonData
}
export default fetcherGet
actually there are several more components such as the Modal Dialog, the Table. but because I don't think it has any effect, I won't attach it to this question.
