Next.js - Footer / Header loading via API only once, server side - reactjs

I'm building a Next.js headless application, where I'm getting the data via API calls to an Umbraco backend. Im using getServerSideProps to load the data for each of my pages, which then is passed as "data" into the functional component and into the page.
The issue I have is that I have separate endpoints for the header / footer portion of the website, and is shared across all pages. Thus, it is a shame, and bad practice to do 3 calls per page (header, data, footer).
What could be done, in order to get header / footer once, then keep it across multiple pages, while maintaining SSR? (important). I've tried using cookies, but they cannot hold so much data. Below is some code:
Page Fetching data:
export async function getServerSideProps({ locale }) {
const footer = await Fetcher(footerEndPoint, locale);
return {
props: {
locale: locale,
footer: footer
}
}
}
Layout
const Layout = (props) => {
const { children, footer } = props;
return (
<>
<Header />
<main>
{children}
</main>
<Footer footer={footer} />
</>
);
};
export default Layout;

I see three options to achieve SSR-only data fetching once for things that won't ever change between page transitions:
1. getInitialProps() in _app.ts
You can just use getInitialProps() in _app.tsx. This runs on the server first and you can just cache the response value in a variable. Next time getInitialProps() is executed, it will just serve the cached value instead of firing another request. To make this work client-side, you have to rehydrate the cache variable in an useEffect:
// pages/_app.tsx
let navigationPropsCache
function MyApp({ Component, pageProps, navigationProps }) {
useEffect(
()=>{
navigationPropsCache = navigationProps
},
[]
)
return <>
<Navigation items={navigationProps}/>
<Component {...pageProps} />
</>
}
MyApp.getInitialProps = async () => {
if(navigationPropsCache) {
return {navigationProps: navigationPropsCache}
}
const res = await fetch("http://localhost:3000/api/navigation")
const navigationProps = await res.json()
navigationPropsCache = navigationProps
return {navigationProps}
}
Note that getInitialProps() is a deprecated feature since next 9.3. Not sure how long this will be supported in the the future. See: https://nextjs.org/docs/api-reference/data-fetching/getInitialProps
See https://github.com/breytex/firat500/tree/trivial-getInitialProps for full code example.
2. Use a custom next server implementation
This solution is based on two ideas:
Use a custom server.ts to intercept the nextjs SSR feature. Fetch all the data you need, render the navbar and footer serverside, inject the component HTML into the SSR result.
Rehydrate the DOM based on stringified versions of the fetched data you also attached to the DOM as a <script>.
// server/index.ts
server.all("*", async (req, res) => {
const html = await app.renderToHTML(req, res, req.path, req.query);
const navigationProps = await getNavigationProps()
const navigationHtml = renderToString(React.createElement(Navigation, {items: navigationProps}))
const finalHtml = html
.replace("</body>", `<script>window.navigationProps = ${JSON.stringify(navigationProps)};</script></body>`)
.replace("{{navigation-placeholder}}", navigationHtml)
return res.send(finalHtml);
});
// components/Navigation.tsx
export const Navigation: React.FC<Props> = ({items})=>{
const [finalItems, setFinalItems] = useState(items ?? [])
useEffect(
()=>{
setFinalItems((window as any).navigationProps)
},
[]
)
if(!Array.isArray(finalItems) || finalItems.length === 0) return <div>{"{{navigation-placeholder}}"}</div>
return (
<div style={{display:"flex", maxWidth: "500px", justifyContent: "space-between", marginTop: "100px"}}>
{finalItems.map(item => <NavigationItem {...item}/>)}
</div>
)
}
I'd consider this a pretty dirty example for now, but you could build something powerful based on this.
See full code here: https://github.com/breytex/firat500/tree/next-link-navigation
3. Use react-ssr-prepass to exec all data fetching server side
This uses a custom made fetch wrapper which has some kind of cache
The React component tree is traversed server side, and all data fetching functions are executed. This populates the cache.
The state of the cache is sent to the client and rehydrates the client side cache
On DOM rehydration all data is served from that cache, so no request is sent a second time
This example is a little bit longer and based on the outstanding work of the urql project: https://github.com/FormidableLabs/next-urql/blob/master/src/with-urql-client.tsx
See full example here: https://github.com/breytex/firat500/tree/prepass
Conclusion:
I'd personally would go with option #1 as long as its feasible.
#3 looks like an approach with a good developer experience, suitable for bigger teams. #2 needs some love to actually be useful :D

Related

How do I make Next.js 13 server-side components in the app directory that depend on useEffect for props?

I'm trying to write a Next.js 13 newsletter page in the app directory that uses server-side components that depend on useEffect for props. The useEffect fetches data from a REST API to get newsletters which will render the content of the page. The code I'm using is below. I'm having trouble figuring out how to configure the server-side components to work when I need to "use client" for interactivity. How can I make sure that the server-side components are rendered before it is sent to the client?
Code:
import Navbar from '#/components/navbar'
import Footer from '#/components/footer'
import Pagination from './pagination'
import IssueCards from './issueCards';
import { useState, useEffect } from 'react';
import axios from 'axios';
const Newsletters = () => {
const [issues, setIssues] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [issuesPerPage, setIssuesPerPage] = useState(5);
useEffect(() => {
const fetchIssue = async () => {
const res = await axios.get(`${process.env.NEXT_PUBLIC_BACKEND_API}/newsletters`)
setIssues(res.data)
}
fetchIssue()
}, [])
// Change page
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const indexOfLastIssue = currentPage * issuesPerPage;
const indexOfFirstIssue = indexOfLastIssue - issuesPerPage;
const currentIssues = issues.slice(indexOfFirstIssue, indexOfLastIssue)
return (
<>
<Navbar />
<div className="newsletter-container" id='newsletter-container'>
<h1>Newsletters</h1>
<hr></hr>
<div className="newsletter-wrapper">
<IssueCards issues={currentIssues} />
<Pagination
issuesPerPage={issuesPerPage}
totalIssues={issues.length}
paginate={paginate}
/>
</div>
</div>
<Footer />
</>
);
}
export default Newsletters;
How do I configure Next.js 13 server-side components that depend on useEffect for props and ensure that the content is rendered before it is sent to the client?
I tried following the Nextjs docs on Server and Client components but I am unsure of how I can pass down the props information onto the server.
Unfortunately, server components don't allow for hooks such as useEffect, see documentation here.
You have two main options:
New way of fetching data
Server components allow for a new way of fetching data in a component, described here.
This approach would look something this:
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
Revert to client components
Your other option is to use the use client directive at the top of your file and leaving Newsletter as a client component. Of course, this way, you wouldn't get the benefits of server components, but this would prevent you from having to change your code substantially. Also, keep in mind that server components are still in beta.

React Query does not work with Astro Framework

I'm trying to use React-Query with Astro to fetch data from my Django Rest Framework backend. Astro has been a great way to organize my react-based frontend but I am worried it might not be compatible with React-Query.
Whenever I try to make a query to my backend I get an 'isLoading' value of true (and an isError of false). I never manage to recover the data from my endpoints however.
I have been following a variety of tutorials with the same results. Here is the code where I'm stuck:
import { QueryClient, useQueryClient, QueryClientProvider, useQuery } from '#tanstack/react-query';
import { gettestApi } from "../../api/testApi";
function MyComponent(props) {
const queryClient = useQueryClient();
const {
isLoading,
isError,
error,
data: test
} = useQuery('test', gettestApi)
let content
if (isLoading) {
content = <p>Loading...</p>
} else if (isError){
content = <p>{error.message}</p>
} else {
content = JSON.stringify(test)
}
As you can see, I import an axios function from /api/testAPI.js which looks like this:
import axios from "axios"
const testApi = axios.create({
baseURL: "http://127.0.0.1:8000"
})
export const gettestApi = async () => {
return await testApi.get("/api/endpoint/").then(response => response.data)
}
That's how most tutorials I have seen and the official documentation wrap up their examples, however my backend server which should be triggered by this endpoint records absolutely no hits from react-query, which is curious to me. I understand that nothing 'calls' my react-query or my gettestApi() function, but it seems to be unnecessary for other people to retrieve their data.
Maybe it would be useful to point out that contrary to other framework with React, Astro does not have an App.js root to surround with
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>
Instead, I have added these QueryClientProvider brackets to the highest React component I could.
I feel like I'm missing some intuition about Tanstack Query/ React-Query. Could anybody point me in the right direction? Thanks a lot for the help.
From what I've seen in the astro docs:
The most important thing to know about Astro components is that they render to HTML during your build. Even if you run JavaScript code inside of your components, it will all run ahead of time, stripped from the final page that you send to your users. The result is a faster site, with zero JavaScript footprint added by default.
So it seems all react code only runs on the server, where data fetching via useEffect or useSyncExternalStore subscriptions just doesn't run. But this is exactly what react-query is doing, so yeah I think they don't work well together. I'm also not sure what the purpose of react-query in a setting would be where there is no client side javascript.
You can set client:only on your Astro component so the React component doesn't run on the server. There are shared-state limitations but still React Query feels better than just fetch + useEffect + own-code even if its not in a complete React app. In this example I'm also using an init function that reads cookies from the client's browser which is another case for when to use client:only.
Astro:
---
import Layout from "../../layouts/Layout.astro";
import ClientPanel from "../../components/client/ClientPanel";
---
<Layout title={ 'Client' }>
<ClientPanel client:only></ClientPanel>
</Layout>
React:
// imports
const queryClient = new QueryClient()
/** client:only component */
const ClientPanel = () => (
<QueryClientProvider client={queryClient}>
<ClientData />
</QueryClientProvider>
)
const ClientData = () => {
const { getUser, getSession } = useSession(); // read cookies functions
const [ user, setUser ] = useState(getUser);
const { isLoading, error, data } = useQuery({
queryKey: ['patientData'],
queryFn: () => getSession() // validate or refresh token
.then(session => fetchPatientData(session.tokens.token))
.catch(error => error === 'INVALID_SESSION' ? null : undefined)
})
if (!user || data === null) window.location.replace('/login')
// return statement, etc.

Preloading getServerSideProps data with Next.js?

I've got a simple React component:
const Page = ({ data }) => {
return (
<header>
{data.length !== 0 ?
<>
{data((d) =>
// render data
)}
</>
:
<>Loading...</>
}
</header>
)
}
I'm getting the data using Next.js recommended getServerSideProps:
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`someurl`)
const data = await res.json()
// Pass data to the page via props
return { props: { data } }
}
Now, for the love of God, I can't figure out why <>Loading...</> is never rendering. The page is blank and then everything pops up. Why does it happen and how do I fix that? of course data.length IS 0 before it's fetched...
Note I'm using dynamic routing and do not want to go with getStaticProps.
getServerSideProps always runs on server side also for client side navigation.
When you return data from getServerSideProps (if the fetch method is executed without errors) it will have always return a value.
In your example <Loading /> will be visible only if data returned from fetch has 0 length and will never be visible during fetch.
Here the docs https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
It's obvious that user should not wait a few seconds in which nothing happens (because getServerSideProps is not finished loading) when he clicks a link. He should see some action is happening, for example:
Loading spinner
Data template (boxes for images, text and so on), youtube example.
But for now it's not possible with getServerSideProps, because page is rendered only after getServerSideProps request is complete.
There is exist future request on next.js about this, so i hope it will be implemented.
you need to use isFallback method provided by there next/router. have a look on this code try to look for isfallback https://github.com/vercel/next-site/blob/master/pages/docs/%5B%5B...slug%5D%5D.js.
Edit:
`export async function getServerSideProps() {
Fetch data from external API
const res = await fetch(someurl)
const data = await res.json()
Pass data to the page via props
return {
props: res ? {
data,
id,
url,
} : {}
};
}
`
and in your component
const router = useRouter();
const { isFallback } = router
if (isFallback) {
return <Loading />
}
else {
return (
// render data
)
}

where to use getInitialProps when auto-direct from one page to another

In my application I am auto-directing from '/' to '/PageOne' like this:
const Home = () => {
const router = useRouter();
useEffect(() => {
router.push('/pageone', undefined, { shallow: true });
}, []);
return <PageOne />;
};
and in my PageOne, I want to use getInitialProps like:
const pageOne = (data) => {
return (
<Layout>
...
</Layout>
);
};
pageOne.getInitialProps = async (
ctx: NextPageContext
): Promise<{ data }> => {
const response = await someAPICall()
return {
data: response.data
};
};
export default pageOne;
This will cause an error in my Home page because I referenced to PageOne using and it is missing the param "data", but I'm not able to pass the data to because the data are not there when rendering Home page.
Shall I call the API to get data in Home page instead of PageOne? If I do so, will refreshing PageOne leads to another API call to get most recent data or the API will be called only when refreshing Home page?
Do not use shallow routing because that is meant to just change the url - a good use case is adding a query string or indicating to the application that something has changed when its bookmarked, e.g: ?chat=true (not your usecase)
Shallow routing allows you to change the URL without running data fetching methods again, that includes getServerSideProps, getStaticProps, and getInitialProps.
It's one of the caveats called out in this page => https://nextjs.org/docs/routing/shallow-routing#caveats
If not already, you would benefit from starting to use global state in your application
https://github.com/vercel/next.js/tree/canary/examples/with-redux
or you can use in-built features:
https://www.basefactor.com/global-state-with-react

Fetching Data from API using NextJS and Material UI React

I am trying to create dynamic pages that shows individual book details (i.e. title/author etc) on a separate page based on a query string of the "id" of each book. However, I am having difficulty in understanding how to make a request to a API endpoint using NextJS that will get the book details based on its "id". I would like to use Material UI as a UI Framework.
ISSUE: When I run npm run dev the book page loads but the book's "props" are not being passed along to the BookAttributes component. The console.log(book) I added in the book page is undefined and the console.log(title) in BookAttributes is undefined as well.
I've tested the API endpoint in POSTMAN and it appears to work.
When I refactor the same code using Semantic UI-React instead of Material UI, the book pages load correctly.
I am using the NextJS Material UI starter template from the Material UI website as a baseline.
I am fairly new to NextJS and Material UI so your assistance and guidance would be greatly appreciated. Thank you for your help on this!
Here is the code I have so. I have tried to keep in clean and simple.
BOOK PAGE (within 'pages' directory)
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
function Book({ book }) {
console.log(book)
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
Book.getInitalProps = async ({ query: { _id } }) => {
const url = 'http://localhost:3000/api/book';
const payload = { params: { _id }}
const response = await axios.get(url, payload)
return { book: response.data }
}
export default Book;
BOOK API ENDPOINT (within 'pages/api' directory)
import Book from '../../models/Book';
import connectDb from '../../utils/connectDb';
connectDb()
export default async (req, res) => {
const { _id } = req.query
const book = await Book.findOne({ _id })
res.status(200).json(book);
}
BOOK ATTRIBUTE COMPONENT (within 'components' directory)
import React from 'react';
function BookAttributes({ title }) {
console.log(title)
return (
<>
<h1>{title}</h1>
</>
)
}
export default BookAttributes;
You should be using dynamic routes here if you want to work with data-fetching methods like getStaticProps or getServerSideProps.
You can create a page like pages/book/[id].js. But to generate the page you have to decide what data-fetching method you want to run. If the data for the page doesn't change very often you can choose to use static-site-generation using getStaticProps which will generate the pages at build time. If the data will be changing a lot you can either do server-side-rendering using getServerSideProps or fetch the data client-side.
Here is an example for your use-case that you can use for server-side-rendering using getServerSideProps, keep in mind the API call inside getServerSideProps might fail so you should have appropriate error handling.
In pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getServerSideProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
Using static-site-generation
Because the page is dynamic you have to provide a list of paths for which nextjs will generate the pages. You can do that by exporting an async function called getStaticPaths.
in pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getStaticPaths = async () => {
// here you have two options if you know all the ids of the books
// you can fetch that data from the api and use all the ids to generate
// a list of paths or show a fallback version of page if you don't know all
// ids and still want the page to be static
// Pseudo code might look like this
const res = await axios.get('api-endpoint-to-fetch-all-the-books')
const paths = res.data.map(book => ({ params: { id: book.id }}))
return {
paths,
fallback: false
}
}
export const getStaticProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
The fallback property in the returned value of getStaticPaths is somewhat important to understand. If you know all the necessary id for the pages you can set the fallback to false. In this case nextjs will simply show a 404 error page for all the paths that were not returned from the function getStaticPaths.
If fallback is set to true nextjs will show a fallback version of page instead of a 404 page for the paths that were not returned from the getStaticPaths function. Now where should you set fallback to true? Let's suppose in your case new books are added to the database frequently, but the data for the books doesn't change very often so you want the pages to be static. In this case, you can set fallback to true and generate a list of paths based on avaliable book ids. For the new books nextjs will first show the fallback version of the page than fetch the data based on the id provided in the request and will send the data as JSON which will be used to render the page in the client.

Resources