I have a menu component that appears globally. What is the best practice for getting data into that component?
I'm trying to take advantage of static generation that Next.js offers but all data fetching guidance from the Next.js team relates to pages. getStaticProps and getStaticPaths seem to pertain to page generation, not data for components. Is their SWR package the right answer, or Apollo Client?
Typically in hooks-based React, I'd just put my data call into useEffect. I'm not sure how to reason this out being that everything is rendered at build time with Next.
This is such a tricky problem, I think we need to lay out some background before a solution comes into focus. I'm focusing in the React.js world but a lot of this would apply to Vue/Nuxt I'd imagine.
Background / Static Generation Benefits:
Gatsby and Next are focused on generating static pages, which vastly improves performance and SEO in React.js sites. There is a lot of technical overhead to both platforms beyond this simple insight but let's start with this idea of a digital machine pumping out fancy HTML pages for the browser.
Data Fetching for Pages
In the case of Next.js (as of v9.5), their data fetching mechanism getStaticProps does most of the heavy lifting for you but it's sandboxed to the /pages/ directory. The idea is that it does the data fetching for you and tells the Next.js page generator in Node about it during build time (instead of doing it component-side in a useEffect hook - or componentDidMount). Gatsby does much the same with their gatsby-node.js file, which orchestrates the data fetching for page building in concert with a Node server.
What about Global Components that need data?
You can use both Gatsby and Next to produce any kind of website but a huge use case are CMS-driven websites, because so much of that content is static. These tools are an ideal fit to that use case.
In typical CMS sites, you will have elements that are global - header, footer, search, menu, etc. This is where static generation faces a big challenge: how do I get data into dynamic global components at build time? The answer to this question is... you don't. And if you think about this for a minute it makes sense. If you had a 10K page site, would you want to trigger a site-wide rebuild if someone adds a new nav item to a menu?
Data Fetching for Global Components
So how do we get around this? The best answer I have is apollo-client and to do the fetch client side. This helps us for a number of reasons:
For small size queries, the performance impact is negligible.
If we need to rebuild pages for changes at the CMS layer, this slides by Next/Gatsby's detection mechanisms, so we can make global changes without triggering gigantic site-wide rebuilds.
So what does this actually look like? At the component level, it looks just like a regular Apollo-enhanced component would. I usually use styled-components but I tried to strip that out so you can could better see what's going on.
import React from 'react'
import { useQuery, gql } from '#apollo/client'
import close from '../public/close.svg'
/**
* <NavMenu>
*
* Just a typical menu you might see on a CMS-driven site. It takes in a couple of props to move state around.
*
* #param { boolean } menuState - lifted state true/false toggle for menu opening/closing
* #param { function } handleMenu - lifted state changer for menuState, handles click event
*/
const NAV_MENU_DATA = gql`
query NavMenu($uid: String!, $lang: String!) {
nav_menu(uid: $uid, lang: $lang) {
main_menu_items {
item {
... on Landing_page {
title
_linkType
_meta {
uid
id
}
}
}
}
}
}
`
const NavMenu = ({ menuState, handleMenu }) => {
// Query for nav menu from Apollo, this is where you pass in your GraphQL variables
const { loading, error, data } = useQuery(NAV_MENU_DATA, {
variables: {
"uid": "nav-menu",
"lang": "en-us"
}
})
if (loading) return `<p>Loading...</p>`;
if (error) return `Error! ${error}`;
// Destructuring the data object
const { nav_menu: { main_menu_items } } = data
// `menuState` checks just make sure out menu was turned on
if (data) return(
<>
<section menuState={ menuState }>
<div>
{ menuState === true && (
<div>Explore</div>
)}
<div onClick={ handleMenu }>
{ menuState === true && (
<svg src={ close } />
)}
</div>
</div>
{ menuState === true && (
<ul>
{ data.map( (item) => {
return (
<li link={ item }>
{ item.title }
</li>
)
})}
</ul>
)}
</section>
</>
)
}
export default NavMenu
Set Up for Next to Use Apollo
This is actually really well documented by the Next.js team, which makes me feel like I'm not totally hacking the way this tool is supposed to work. You can find great examples of using Apollo in their repo.
Steps to get Apollo into a Next app:
Make a custom useApollo hook that sets up the connection to your data source (I put mine in /lib/apollo/apolloClient.js within Next's hierarchy but I'm sure it could go elsewhere).
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, SchemaLink, HttpLink } from '#apollo/client'
let apolloClient
// This is mostly from next.js official repo on how best to integrate Next and Apollo
function createIsomorphLink() {
// only if you need to do auth
if (typeof window === 'undefined') {
// return new SchemaLink({ schema })
return null
}
// This sets up the connection to your endpoint, will vary widely.
else {
return new HttpLink({
uri: `https://yourendpoint.io/graphql`
})
}
}
// Function that leverages ApolloClient setup, you could just use this and skip the above function if you aren't doing any authenticated routes
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(),
cache: new InMemoryCache(),
})
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
// Restore the cache using the data passed from getStaticProps/getServerSideProps
// combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState })
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
// This is goal, now we have a custom hook we can use to set up Apollo across our app. Make sure to export this!
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
Modify _app.js in the /pages/ directory of Next. This is basically the wrapper that goes around every page in Next. We're going to add the Apollo provider to this, and now we can globally access Apollo from any component.
import { ApolloProvider } from '#apollo/react-hooks'
import { useApollo } from '../lib/apollo/apolloClient'
/**
* <MyApp>
*
* This is an override of the default _app.js setup Next.js uses
*
* <ApolloProvider> gives components global access to GraphQL data fetched in the components (like menus)
*
*/
const MyApp = ({ Component, pageProps }) => {
// Instantiates Apollo client, reads Next.js props and initialized Apollo with them - this caches data into Apollo.
const apolloClient = useApollo(pageProps.initialApolloState)
return (
<ApolloProvider client={ apolloClient }>
<Component {...pageProps} />
</ApolloProvider>
)
}
export default MyApp
And now you can get dynamic data inside of your components using Apollo! So easy right ;) HA!
For global data fetching in NextJS, I use react-query and there is no need for a global state because it lets you to cache the data. Let's say you have a blog with categories and you want to put the category names in the navbar as a dropdown menu. In this case you can call the API to fetch the data with react-query from the navbar component and cache it. The navbar data will be available for all pages.
Related
In React server components official GitHub example repo at exactly in this line here they are using response.readRoot().
I want to create a similar app for testing something with RSC's and it seems like the response does not contain the .readRoot() function any more (because they have updated that API in the react package on npm and I cannot find anything about it!). but it returns the tree in value property like below:
This means that whatever I render in my root server component, will not appear in the browser if I render that variable (JSON.parse(value) || not parsed) inside of my app context provider.
How can I render this?
Basically, if you get some response on the client side (in react server components) you have to render that response in the browser which has the new state from server but since I don't have access to readRoot() any more from response, what would be the alternative for it to use?
I used a trick o solve this issue, but one thing to keep in mind is that they are still unstable APIs that react uses and it's still recommended not to use React server component in the production level, uses it for learning and test it and get yourself familiar with it, so back to solution:
My experience was I had a lot of problems with caching layer they are using in their depo app. I just removed it. My suggestion is to not use it for now until those functions and APIs become stable. So I Removed it in my useServerResponse(...) function, which in here I renamed it to getServerResponse(...) because of the hook I created later in order to convert the promise into actual renderable response, so what I did was:
export async function getServerResponse(location) {
const key = JSON.stringify(location);
// const cache = unstable_getCacheForType(createResponseCache);
// let response = cache.get(key);
// if (response) return response;
let response = await createFromFetch(
fetch("/react?location=" + encodeURIComponent(key))
);
// cache.set(key, response);
return response;
}
and then creating a hook that would get the promise from the above function, and return an actual renderable result for me:
export function _useServerResponse(appState) {
const [tree, setTree] = useState(null);
useEffect(() => {
getServerResponse(appState).then((res) => {
setTree(res);
});
}, [appState]);
return { tree };
}
and finally in my AppContextProvider, I used that hook to get the react server component tree and use that rendered tree as child of my global context provider in client-side like below:
import { _useServerResponse } from ".../location/of/your/hook";
export default function AppContextProvider() {
const [appState, setAppState] = useState({
...someAppStateHere
});
const { tree } = _useServerResponse(appState);
return (
<AppContext.Provider value={{ appState, setAppState }}>
{tree}
</AppContext.Provider>
);
}
I know that this is like a workaround hacky solution, but it worked fine in my case, and seems like until we get stable APIs with proper official documentation about RSCs, it's a working solution for me at least!
I'm learning Relay now and I was wondering if there is a way to access the state without having to pass down props. I thought that Relay used React Context, however, in all the examples they seem to be using props to pass down state instead of directly accessing the Context store. Is it possible to access state through Context? If so, is it considered bad practice?
My concern is that I will start to have a lot of props being passed down to components. In addition, it is difficult to pass down props to certain components in my application.
It is a bad practice to pass down Relay state using context, redux, or some other kind of external store. One of the core features of the library is making sure all components who need a piece of data get that piece, and no other pieces. This is one of the reasons Relay uses a compiler.
With Relay, developers use a declarative language called GraphQL to specify what data each component needs, but not how to get it.
[The compiler] allows components to be reasoned about in isolation, making large classes of bugs impossible
Subcomponents should use fragments. For example:
// User.tsx
interface UserProps {
initialQueryRef: PreloadedQuery<UserQuery>
}
export default function User({ initialQueryRef }: UserProps) {
const { user } = usePreloadedQuery(
graphql`
query UserQuery($uid: String!) {
user(uid: $uid) {
uid
...Avatar_fragment
...Biography_fragment
# ...
}
}
`,
initialQueryRef
)
return (
<div>
<Avatar user={user} />
<Biography user={user} />
{/* ... */}
</div>
)
}
// Biography.tsx
interface BiographyProps {
user: Biography_fragment$key // compiler generated type
}
export default function Biography({ user }: BiographyProps) {
const { shortBio, longBio } = useFragment(
graphql`
fragment Biography_fragment on User {
shortBio
longBio
}
`,
user
)
// ...
}
Relay will take care of updating the components that use fragments whenever the fragment data changes due to mutations, subscriptions, etc. You should never have to think about managing their state programmatically.
So I haven't found this answer anywhere and I would like to know this so that's why posting here. Suppose I have a blog for which I want to use SSG and ISR. SSG for the homepage and ISR for the individual posts.
So if I generated the homepage at build time with SSG to display 10 posts with the following code:
index.js file:
export const getStaticProps = async () => {
const posts = await get10PostsFromDB();
return {
props: {
posts
}
}
}
And then, if I use this code for each post and also use getStaticPaths to generate those exact 10 posts:
[post].js
export const getStaticPaths = async () => {
const posts = await get10PostsFromDB();
const paths = posts.map( ( item ) => ( {
params: {
slug: item.slug
}
} ) );
return {
paths,
fallback: 'blocking'
};
}
export const getStaticProps = async ({ params }) => {
const post = await getASinglePostFromDB( params.slug );
return {
props: {
post
},
revalidate: 1,
notFound: true
}
}
Will there be duplicate SSG generated pages as 10 are generated at build time for the homepage and then again those 10 pages are generated for getStaticPaths as well? Or what I'm thinking is totally wrong?
I think you have a bit of a misunderstanding of what getStaticPaths and getStaticProps do.
Short answer: no, it will not duplicate the pages.
Let me explain why:
getStaticProps vs getStaticPaths
getStaticProps (Static Generation): Fetch data at build time.
getStaticPaths (Static Generation): Specify dynamic routes to pre-render pages based on data.
What does getStaticProps do?
NextJS has this built-in page pre-rendering but this built-in process has a flaw if you wanna call it like this.
Take this code for example:
const HomePage = () => {
const [loadedPosts, setLoadedPosts] = useState();
useEffect(() => {
setLoadedPosts(DUMMY_POSTS);
}, []);
return (
loadedPosts ? (
<PostList posts={loadedPosts} />
) : ''
)
}
Note: This is a simple demo to convey the concept. In normal cases this issue will occur when you fetch data from an API in the useEffect hook.
Explanation: Let's imagine we are rendering a list of posts. DUMMY_POSTS is our data that comes from the db for example.
What happens here is that the useEffect hook will wait for the component to render before updating out loadedPosts.
When NextJS pre-renders a page it takes the snapshot of the first component rendering cycle as its content and that might be missing crucial data. This means that NextJS does not update the snapshot of our code AFTER the new data has come. This as you know is bad for SEO.
After this HTML page was received on the client, React will actually take over, the page is hydrated as this process is called, which means that now React will turn this into a single page application, take over control, and then this useEffect function will be executed, data might be fetched and the page might be updated - not on the server, not on the pre-rendered page but instead after this page was received in the browser. I.e no SSR.
Here's where getStaticProps() comes to play.
const HomePage = (props) => {
return (
<PostList posts={props.posts} />
)
}
export function getStaticProps() {
// fetch data from an API
return {
props: {
posts: DUMMY_POSTS
},
revalidate: 10
};
};
export default HomePage;
getStaticProps runs at build time and it fetches the data DUMMY_POSTS and then assigns it to props. This is then passed as props of the HomePage component where we can access props.posts.
What is getStaticPaths?
getStaticPaths is a function that you need to export in a page component file that is:
A dynamic page and
Is using getStaticProps
You DO NOT need it in a file that is using getServerProps and DO NOT NEED IT WITHOUT getStaticProps
Here I will use a very simple example to explain, but normally you generate the paths dynamically as you have done in your example:
export async function getStaticPaths() {
return {
fallback: false,
paths: [
{
params: {
postdId: 'p1'
}
},
{
params: {
postId: 'p2'
}
},
]
};
};
In the above example, we have two posts with IDs p1 and p2.
With getStaticProps a page is generated during the build process. This means that Next.js needs to pre-generate all versions for all dynamic pages in advance for all supported dynamic paths. Since it's dynamic Next.js needs to know for which IDs it needs to pre-generate a page. These functions are run during the build process NOT when a client visits the page. I.e. without getStaticPaths these dynamic pages will not be pre-generated on the server which kills the purpose of Next.js.
getStaticPaths tells Next.js what are the exact dynamic paths (in our case which post IDs) it needs to pre-generate. If the user enters an ID for which we did not pre-generate a page they will see a 404 error.
getStaticPaths returns an object where we describe all of the dynamic segment values. It has a paths property, which is an array that takes an object for each version of the dynamic page. This object has a params property, which is also an object that holds all of the key-value pairs that lead to your dynamic page.
TLDR;
getStaticProps fetches the data at build time before the component has loaded so that Next.js can pre-render the page WITH the data that we are fetching.
getStaticPaths tells Next.js about dynamic pages that need to be pre-rendered, because Next.js will not pre-render them on its own as it does not know exactly the dynamic IDs (routes). With this function you are basically telling Next.js how your dynamic routes are called so it knows what to pre-render. Otherwise it will show a 404 page.
Conclussion:
NextJS does not generate duplicate pages at build time.
EDIT:
In regards to pre-generating pages with getStaticPaths...
You DO NOT need to pre-generate every single dynamic route. In order to be able to load the other routes without a 404 error, you can add fallback: true or fallback: 'blocking'
These options can help in cases where you have a lot of pages that need to be pre-generated. In the case we have a website like Amazon with millions of products, pre-rendering all of them might take super long and there may be products that are rarely visited. Therefore, pre-generating such products is a waste of resources.
In such cases, we can set fallback: true and pre-generate some pages, not all.
export async function getStaticPaths() {
return {
fallback: true,
paths: [
{
params: {
prodId: 'p1'
}
}
]
}
}
Even if we don't list all of the pages, this setting tells NextJS that other pages can be valid and should be loaded when they are visited. They are generated just in time. This allows to pre-generate highly visited pages and pre-generate other pages only when they are needed.
However, if the request with the unsupported ID is sent directly (not internally through clicking on a product), it will return a 404. In such cases, we need to have a fallback case in our component.
const ProductDetailPage = ({ loadedProducts }) => {
if (!loadedProducts) {
return <p>Loading...</p>
}
return (
<>
<h1>{loadedProducts.title}</h1>
<p>{loadedProducts.description}</p>)
</>
)
}
In the above code, the user will see the Loading indicator instead of a 404 page, until the data comes and is then displayed on the page.
An alternative would be to set fallback: 'blocking'. In those cases, we don't need a fallback in the component, but the response will take a little longer.
const ProductDetailPage = ({ loadedProducts }) => {
return (
<>
<h1>{loadedProducts.title}</h1>
<p>{loadedProducts.description}</p>)
</>
)
}
export async function getStaticPaths() {
return {
fallback: 'blocking,
paths: [
{
params: {
prodId: 'p1'
}
}
]
}
}
It depends on your case. Sometimes you need to show something quickly (fallback: true). Sometimes it's worth waiting for it and you don't want to show an incomplete page to your visitors (fallback: 'blocking').
Read more about NextJS's Data fetching here.
I want to use the same application for different customers, where there's a different database for each (which could be on-premise).
As a result, I have no data at the build phase (e.g. in CI/CD) that I could use to create static sites.
I thought about skipping generating the sites in getStaticProps via an environment variable.
When building the sites, I could tell Next.js to not use any data, something like this:
export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
const isBuildPhase = process.env.IS_BUILD;
const data = isBuildPhase ? null : await fetchData();
return {
props: {
data: data ?? null,
},
revalidate: 5 * 60,
};
};
Now, I want to create the sites only at runtime (with next start), because at runtime, the built application has access to its database. In each getStaticProps the environment variable will be configured so data is fetched. When the application starts initially, it will generate all static sites when they're accessed.
Are there big downsides with this approach?
Are there maybe better solutions to this problem?
We had a similar requirement where the app needed to serve multiple tenants pages, but each tenant data is different. Which meant the app did not have access to data at build time and only runtime.
We leveraged getStaticPaths and getStaticProps to do this.
getStaticPaths
This method returns a fallback option (true || false || blocking), which we can use to decide to show a loader on the UX or block until the actual page loads.
export async function getStaticPaths() {
// Return empty paths because we don't want to generate anything on build
// { fallback: blocking } will server-render pages
// on-demand if the path doesn't exist.
return {
paths: [],
fallback: 'blocking',
};
}
When using this method, there isn't a need to maintain IS_BUILD env variable. (Depending on your specific use-case you may choose to use it or not)
getStaticProps
This method does the actual data fetching based on the URL path params for a tenant.
export async function getStaticProps({ params }) {
// Run your data fetching code here
const data = await fetch(params);
return {
props: data,
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every 10 seconds
revalidate: 10, // In seconds
notFound: !data,
};
}
We also leverage Incremental Static Regeneration, to make sure that we refresh the page every revalidate seconds.
Pages
Single Template Tenants
When all the tenants share the same components, it's pretty straight forward:
const SingleTenantPage = ({ data }) => {
return <Component {...data} />;
};
export default SingleTenantPage;
Multi-Template Tenants
The actual page, just parses the props (passed down from getStaticProps), and then loads the appropriate component for that page.
This way we leverage a single route for multiple tenants. example.com/app/tenantA, example.com/app/tenantB, example.com/app/tenantC all 3 routes can be served out of pages/app/[slug]/index.js.
Using dynamic imports, we make sure that only the component for a specific tenant is loaded. (also helps in code splitting)
// Dynamic Import so we load only the required bundles
const templates = {
tenantA: dynamic(() => import(`../templates/tenantA`)),
tenantB: dynamic(() => import(`../templates/tenantB`)),
tenantC: dynamic(() => import(`../templates/tenantC`)),
};
const MultiTenantPage = ({ data }) => {
// Provided template is present in the data
// template: 'tenantA' || 'tenantB' || 'tenantC'
const { template, ...rest } = data || {};
// If the template doesn't exist, show a 404 Page instead
const Component = templates[template] || (() => <Error statusCode="404" />);
return <Component {...rest} />;
};
export default MultiTenantPage;
The multi-tenant page works well for us. We use a larger revalidate value because data isn't changing that often. Only caveat we had were a little bit complex test cases because of all the dynamic imports.
The above solution is very much similar to what you've considered for your use-case, albeit without an additional env variable (IS_BUILD)
I'm using next.js and apollo with react hooks.
For one page, I am server-side rendering the first X "posts" like so:
// pages/topic.js
const Page = ({ posts }) => <TopicPage posts={posts} />;
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const posts = await apolloClient.query(whatever);
return { posts };
};
export default Page;
And then in the component I want to use the useQuery hook:
// components/TopicPage.js
import { useQuery } from '#apollo/react-hooks';
export default ({ posts }) => {
const { loading, error, data, fetchMore } = useQuery(whatever);
// go on to render posts and/or data, and i have a button that does fetchMore
};
Note that the useQuery here executes essentially the same query as the one I did server-side to get posts for the topic.
The problem here is that in the component, I already have the first batch of posts passed in from the server, so I don't actually want to fetch that first batch of posts again, but I do still want to support the functionality of a user clicking a button to load more posts.
I considered the option of calling useQuery here so that it starts at the second "page" of posts with its query, but I don't actually want that. I want the topic page to be fully loaded with the posts that I want (i.e. the posts that come from the server) as soon as the page loads.
Is it possible to make useQuery work in this situation? Or do I need to eschew it for some custom logic around manual query calls to the apollo client (from useApolloClient)?
Turns out this was just a misunderstanding on my part of how server side rendering with nextjs works. It does a full render of the React tree before sending the resulting html to the client. Thus, there is no need to do the "first" useQuery call in getInitialProps or anything of the sort. It can just be used in the component alone and everything will work fine as long as getDataFromTree is being utilized properly in the server side configuration.