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.
gaiaapp(pages)(secured)vetsvacinaspage.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.
gaiaapp(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.
In time, idk if this is relevant or not, but here is my Axios component which fetched the data.
gaiaapp_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.
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.
gaiaapplayout.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>
)
}
gaiaapp_componentsauthuser-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
Answers
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.As per the issue :
Cause :
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 :
User.js Component
locsrcappcompUser.js
:page.js
locsrcappuserpage.js
: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
& underName
click onuser
, on righthand side click onPreview
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 :
Rendering :
https://nextjs.org/docs/app/building-your-application/rendering
Server Components :
https://nextjs.org/docs/app/building-your-application/rendering/server-components
Client Components : https://nextjs.org/docs/app/building-your-application/rendering/client-components
Server and Client Composition Patterns : https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns
What is the React Server Component Payload (RSC)? https://nextjs.org/docs/app/building-your-application/rendering/server-components#how-are-server-components-rendered