Multiple fetch requests when using Array.map & filter

227 views Asked by At

I'm working on developing an app using Next.js 13 with App Routes. I've hit a bit of a snag and could use some help.

On one of my pages, I have a grid set up to display information, and there's a search bar at the top that allows users to search through that data. The issue is that whenever someone starts typing in the search bar, the component seems to reload the app unnecessarily, causing the fetch to happen multiple times.

gaia\app\(pages)\(secured)\vets\vacinas\page.tsx

'use client'

--imports

export default function Page() {

    const [search, setSearch] = useState('');
    const { authenticatedUser, authFlow } = useAuthenticatedUser();
    const [filtroEscolhido, setFiltroEscolhido] = useState('2');
    const router = useRouter();

    const filtros = [
        { nome: "Todos", codigo: '1' },
        { nome: "Agendados", codigo: '2' },
        { nome: "Esse mês", codigo: '3' },
        { nome: "Mais antigos", codigo: '4' }
    ];

    if (!authenticatedUser && authFlow.code !== 3) {
        router.push('/');
    } else {
        return (
            <Container>
                <Stack gap={5}>
                    <Row className="text-center">
                        <Stack direction="horizontal" gap={5} className="justify-content-end">
                            <Col xs={5}>
                                <FloatingLabel controlId="inputBuscar" label="Buscar" >
                                    <Form.Control type="text" placeholder="Buscar..." onChange={(e) => setSearch(e.target.value.toLowerCase())}></Form.Control>
                                </FloatingLabel>
                            </Col>
                            <AplicacaoModal />
                        </Stack>
                    </Row>
                    <Row>
                        <p className="display-6">Resultados da pesquisa</p>
                        <ButtonGroup>
                            {filtros.map((filtro, cod) => (
                                <ToggleButton
                                    key={cod}
                                    id={`filtro-${cod}`}
                                    type="radio"
                                    variant="info"
                                    name="filtro"
                                    value={filtro.codigo}
                                    checked={filtro.codigo === filtroEscolhido}
                                    onChange={(e) => setFiltroEscolhido(e.currentTarget.value)}
                                >
                                    {filtro.nome}
                                </ToggleButton>
                            ))}
                        </ButtonGroup>
                    </Row>
                    <Row>
                        <Suspense fallback={<Loading />}>
                            <Aplicacoes filtro={filtroEscolhido} buscar={search}></Aplicacoes>
                        </Suspense>
                    </Row>
                </Stack>
            </Container>
        );
    }
}

To give you a bit more context, I've created the "Aplicacoes" component which fetches data from my Node backend API and uses Array.map combined with .filter to apply the filters and list the information.

gaia\app\(pages)\(secured)\vets\(components)\aplicacao-grid.tsx

import { get_aplicacoes } from "@/app/_api/(secured)/aplicacoes-api";
import Table from "react-bootstrap/Table";

async function Aplicacoes({ filtro, buscar }: { filtro: string, buscar: string }) {
    try {
        console.log('acionando get_aplicacoes()');
        const aplicacoes = await get_aplicacoes();
        return (
            <Table striped bordered hover responsive >
                <thead>
                    <tr>
                        <th>Nome PET</th>
                        <th>Vacina aplicada</th>
                        <th>Dose</th>
                        <th>Data da aplicação</th>
                        <th>Valor cobrado</th>
                    </tr>
                </thead>
                <tbody>
                    {aplicacoes
                        .filter((vacina) => {
                            if (buscar === '') {
                                return vacina;
                            }

                            let filtrd_pet = vacina.nomePet.toLowerCase().includes(buscar);
                            return filtrd_pet || vacina.nomeVacina.toLowerCase().includes(buscar);
                        })
                        .filter((vacina) => {
                            if (filtro === '1') {
                                return vacina;
                            }

                            let hoje = new Date();
                            hoje.setHours(0, 0, 0, 0);
                            let data_partes = vacina.dataAplicacao.toString().split("/");
                            let data_vacina = new Date(+data_partes[2], +data_partes[1] - 1, +data_partes[0]);

                            if (filtro === '2' && data_vacina > hoje) {
                                return vacina;
                            }

                            if (filtro === '3' && (+data_partes[1] - 1 === hoje.getMonth() && +data_partes[2] === hoje.getFullYear())) {
                                return vacina;
                            }

                            if (filtro === '4' && (data_vacina < hoje)) {
                                return vacina;
                            }
                        })
                        .map((aplicacao) => (
                            <tr key={aplicacao._id}>
                                <td>{aplicacao.nomePet}</td>
                                <td>{aplicacao.nomeVacina}</td>
                                <td>{aplicacao.dose}</td>
                                <td>{aplicacao.dataAplicacao.toString()}</td>
                                <td>{aplicacao.valorCobrado}</td>
                            </tr>
                        ))}
                </tbody>
            </Table>
        );
    } catch (error) {
        console.log(`Erro no componente Aplicacoes ${error}`);
        return null;
    }
}

export {Aplicacoes};

And, as you can see, this is the multiple requests to my Node backend API.

Developer tools print screen of t he requests

In time, idk if this is relevant or not, but here is my Axios component which fetched the data.

gaia\app_api\(secured)\aplicacoes-api.tsx

'use client'

import instance_authenticated from "./axios-instance";
import { Aplicacao } from "@/app/_types/vets/IAplicacoes";

async function get_aplicacoes(): Promise<Aplicacao[]> {
    const axiosResponse = await instance_authenticated.get('/diarioDeVacinas/get');
    return axiosResponse.data;
}

export {get_aplicacoes};

Thanks for your help in advance!

I'm still fairly new to Next.js, so I'm not quite sure what I'm missing here. I've read through the Next.js 13 pages lifecycle documentation several times but can't seem to figure it out.

EDIT 1 I'm using App Router. This is my folder structure.

enter image description here

EDIT 2

I use next-auth to enable social logins like Google and Facebook, but I have my own authentication provider and MongoDB database managed by my Node.js backend API.

gaia\app\layout.tsx

'use client'

---another imports

import type { Metadata } from 'next'
import { MenuAccess, MenuNavigation } from './_components/nav';
import { UserProvider } from './_components/auth';
import { SessionProvider } from "next-auth/react";



export default function Layout(props: { children: React.ReactNode}) {

  return (
    <html>
      <head>
        <script src="https://accounts.google.com/gsi/client" async defer></script>
      </head>
      <body>
        <SessionProvider>
          <UserProvider>
            <Stack gap={3}>
              <Navbar expand="lg" className="bg-body-tertiary">
                <Container>
                  <Navbar.Brand href="/">GAIA</Navbar.Brand>
                  <MenuNavigation />
                  <MenuAccess />
                </Container>
              </Navbar>
              <Container>
                <Stack gap={3}>
                  <Row>
                    {props.children}
                  </Row>
                  <Row>
                    <footer>
                      <p>© Gaia 2023</p>
                    </footer>
                  </Row>
                </Stack>
              </Container>
            </Stack>
          </UserProvider>
        </SessionProvider>
      </body>
    </html>
  )
}

gaia\app_components\auth\user-components.tsx

import { login, oauth_use } from "@/app/_api/(public)/auth-api";
import { redirect, useRouter } from "next/navigation";
import { useContext, createContext, useState, useEffect } from "react";
import { useSession, signOut, signIn } from "next-auth/react";
import { jwtDecode } from "jwt-decode";

//Exported components
export { useAuthenticatedUser, UserProvider }

interface AuthedUser {
    id: string;
    email: string;
    profile: string;
    accessToken: string;
    expiresIn: number;
    displayName: string;
    pictureUrl: string;
}

interface AuthFlow {
    code: number,
    status: string,
    message: string
}
let useFakeLoggin = false;

let fakeAuthedUser: AuthedUser = {
    id: "63c3811392a585127099d34a",
    email: "[email protected]",
    profile: "admin",
    accessToken: "xpto",
    expiresIn: 86400,
    displayName: "MASTER ADMIN",
    pictureUrl: "https://xpto.png"
}

let loggedOffAuthFlow = { code: 1, status: 'LOGGED_OFF', message: '' };
let authenticatingAuthFlow = { code: 2, status: 'AUTHENTICATING', message: '' };
let authenticatedAuthFlow = { code: 3, status: 'AUTHENTICATED', message: '' };
let authErrorAuthFlow = { code: 4, status: 'AUTH_ERROR', message: 'Erro na autenticação. Verifique os dados e tente novamente.' };
let socialAuthErrorAuthFlow = { code: 5, status: 'SOCIAL_AUTH_ERROR', message: 'Erro na autenticação. Verifique o meio utilizado e tente novamente.' };


const useAuthenticatedUser = () => useContext(AuthenticatedUserContext);

const AuthenticatedUserContext = createContext({
    authenticatedUser: useFakeLoggin ? fakeAuthedUser : undefined,
    doLogout: () => { },
    doLogin: (email: string, password: string) => { },
    authFlow: loggedOffAuthFlow
});

function UserProvider(props: { children: React.ReactNode }) {
    const [authenticatedUser, setAuthenticatedUser] = useState<AuthedUser | undefined>();
    const [authFlow, setAuthFlow] = useState<AuthFlow>(loggedOffAuthFlow);
    const router = useRouter();
    const { data: token_data, status } = useSession();

    useEffect(() => {
        console.log('tokenData: ', token_data?.user);
        console.log('status: ', status);

        let usuarioLogado = localStorage.getItem('@Gaia:user');
        let usuarioAccessToken = localStorage.getItem('@Gaia:userAccessToken');

        console.log('usuarioLogado: ', usuarioLogado);
        console.log('usuarioAccessToken: ', usuarioAccessToken);

        if (!usuarioLogado && token_data && status === 'authenticated') {
            // console.log('calling oAuthLogin');
            oauth_login(token_data.user?.email, token_data.user?.name, token_data.user?.last_name, token_data.user?.picture, token_data.user?.provider_name, token_data.user?.id_token);
        }

        if (usuarioAccessToken) {
            let currentDate = new Date();
            let decodedAccessToken = jwtDecode(usuarioAccessToken);
            if (decodedAccessToken.exp && (decodedAccessToken.exp * 1000) < currentDate.getTime()) {
                setAuthFlow(loggedOffAuthFlow);
                setAuthenticatedUser(undefined);
                localStorage.removeItem('@Gaia:user');
                localStorage.removeItem('@Gaia:userAccessToken');
                usuarioLogado = null;
                usuarioAccessToken = null;
            }
        }

        if (usuarioLogado && usuarioAccessToken) {
            setAuthFlow(authenticatedAuthFlow);
            setAuthenticatedUser(JSON.parse(usuarioLogado));
        }
    }, [token_data]);

    async function getAuthedUser() {
        return authenticatedUser ?? undefined;
    }

    function doLogout() {
        setAuthFlow(authenticatingAuthFlow);
        localStorage.removeItem('@Gaia:user');
        localStorage.removeItem('@Gaia:userAccessToken');
        setAuthenticatedUser(undefined);
        signOut(); //next-auth signOut
        setAuthFlow(loggedOffAuthFlow);
        router.push('/account/login');
        //Clear token data.
    }

    function handleCallbackLogin(cbStatus: number, data: any) {
        // console.log('callback recebido');
        if (cbStatus == 9999 || cbStatus == 404) {
            setAuthFlow(authErrorAuthFlow);
        };

        if (cbStatus == 404) {
            authErrorAuthFlow.message.concat(data);
            setAuthFlow(authErrorAuthFlow);
        }

        if (cbStatus != 200) {
            //handleError
            // console.log(`Callback login: Error Status ${cbStatus} | Message: ${data}`);
            setAuthFlow(authErrorAuthFlow);
        } else {
            // console.log(`cbStatus != 200. data: ${JSON.stringify(data, null, 4)}`);
            //Handle login flow.
            if (data) {
                let usuarioLogado: AuthedUser = {
                    id: data.id,
                    email: data.email,
                    profile: data.profile,
                    accessToken: data.accessToken,
                    expiresIn: data.expiresIn,
                    displayName: data.displayName,
                    pictureUrl: data.pictureUrl
                };

                setAuthenticatedUser(usuarioLogado);
                localStorage.setItem('@Gaia:user', JSON.stringify(usuarioLogado));
                localStorage.setItem('@Gaia:userAccessToken', usuarioLogado.accessToken);

                setAuthFlow(authenticatedAuthFlow);
                router.push('/vets/vacinas');
            } else {
                authErrorAuthFlow.message.concat(data);
                setAuthFlow(authErrorAuthFlow);
            }
            //Set token data
        }
    }

    function doLogin(email: string, password: string) {
        //Fetch from apis
        setAuthFlow(authenticatingAuthFlow);
        login(email, password, handleCallbackLogin);
    }

    function oauth_login(email: string | null | undefined, nome: string | null | undefined, sobrenome: string | null | undefined, picture_url: string | null | undefined, handler: string, id: string) {
        //Fetch from apis
        setAuthFlow(authenticatingAuthFlow);
        oauth_use(email, nome, sobrenome, picture_url, handler, id, handleCallbackLogin);
    }

    return (
        <AuthenticatedUserContext.Provider value={{ authenticatedUser, doLogout, doLogin, authFlow }}>
            {props.children}
        </AuthenticatedUserContext.Provider>
    );
}
2

There are 2 answers

2
Beast80K On BEST ANSWER

As per the issue :

The issue is that whenever someone starts typing in the search bar, the component seems to reload the app unnecessarily, causing the fetch to happen multiple times.

Cause :

There is a API Call in aplicacao-grid.tsx & also it has props that change, on user interaction { filtro, buscar },
This state changes causes aplicacao-grid.tsx to re-render which makes a API Call.

Solution :

You should render the page from server-side pass API data to client side component for handling searching operation.

Here is a example code I made : You may make necessary changes wherever required.

Folder Structure :

projectName
├── src
│   └── app
│       ├── comp
│       │   └── User.js
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.js
│       ├── page.js
│       └── user
│           └── page.js
└── tailwind.config.js

User.js Component loc\src\app\comp\User.js:

'use client'
import React, { useState } from 'react'

const User = ({ Data }) => {

    // console.log("Data.users", Data.users);
    // VISBILE IN BROWSER

    const [UserData, SetUserData] = useState(Data.users)

    const [SearchTerm, SetSearchTerm] = useState("")


    return (
        <div>
            <h1>Client Component Gets data from Server as props.</h1>

            <input type="text" onChange={(e) => { SetSearchTerm(e.target.value) }} />
            {
                SearchTerm ?
                    // IF SearchTerm IS EMPTY THEN SHOW ALL DATA, ELSE SHOW SEARCHED 

                    // IM MAPPING USER'S FIRSTNAME, 
                    // HENCE IM SEARCHING USER BY FIRSTNAME, 


                    UserData.filter(s => s.firstName.toLowerCase().includes(SearchTerm.toLowerCase())).map((u, i) => (
                        <p key={i}>{u.firstName}</p>
                    ))

                    :

                    UserData.map((u, i) => (
                        <p key={i}>{u.firstName}</p>
                    ))
            }
        </div>
    )
}

export default React.memo(User);

page.js loc\src\app\user\page.js:

import User from "../comp/User";

async function GetUserData() {
    let data = await fetch('https://dummyjson.com/users')
    let UserData = await data.json()
    return UserData
}

// ABOVE API SIMULATES SERVER-SIDE DATA FETCHING
// YOU CAN MAKE NECESSARY CHANGES

export default async function Page() {


    let UserData = await GetUserData();
    // STORING DATA FROM API 

    // console.log("Data KEYS from API on Serverside : ", Object.keys(UserData));
    // THIS LOG WILL BE IN TERMINAL AS THIS PAGE IS SERVER-SIDE RENDERED
    // SHOWS USERS

    return (
        <div>
            <h1>Users Page</h1>

            <User Data={UserData} />
            {/* USER COMPONENT IS CIENT SIDE HENCE, SERVER DOESN'T RENDER IT, CIENT BROWSER RENDERS IT*/}

            <h3>Rest of the page is  SERVER-SIDE RENDERED </h3>

        </div>
    )
}

Explanation :

  • My server gets data from API, passes it to the User component as prop. By doing this I have given server the responsibility of fetching data.

  • User component is client-side, because of 'use client'.

  • So now whenever state changes page won't reload. Just states will change & No API calls will be triggered.

  • Client component has no API Calling functionality as it gets data from server.

  • I have implemented search functionality by using filter method.

  • Go in Network Tab & under Name click on user, on righthand side click on Preview you will see pre-rendered page.

  • And also you can see user?_rsc=something, this is the React Server Component Payload (RSC Payload is used by React on client-side to update the DOM.)

Read :

If you still have any doubts, or else if missed anything, Please leave a comment I will update the answer.

0
Cooper Runyan On

Whenever the state for search is changed, React will re-render the component that it's in. The problem here is that it's in the root component and not everything needs to be re-rendered, only a specific part of the DOM. It would make more sense for the search logic and content to be extracted into its own component like <Content/>, so that whenever it needs to change the content shown, it doesn't re-render the whole page, only the stuff that actually need to be re-rendered.

TLDR; When setState is called, the component that owns the state re-renders. What we really want is to separate search state from authentication/user state.