Launch-time initialization in Next.js static/exported site - next.js

I'm trying to use Next to power an Electron app. electron-next uses Next's static site mode for its production build, which calls getInitialProps at build-time, rather than launch-time.
start.js (initially rendered page)
import Link from 'next/link'
export default function Start({date}) {
return (
<div>
<div>Date is {date}</div> {/* <- will always be the build time */}
<Link href="/about">
<a>Take me to the About page</a>
</Link>
</div>
)
}
Start.getInitialProps = () => {
return {
date: "" + new Date()
}
}
Interestingly, using Link to navigate elsewhere does, in fact, result in a dynamic getInitialProps call.
about.js
import Link from 'next/link'
export default function About({date}) {
return (
<div>
<div>Date is {date}</div> {/* <- will be the time the link was clicked */}
<div>Important info about this app</div>
</div>
)
}
About.getInitialProps = () => {
return {
date: "" + new Date()
}
}
Is there a non-hacky way to get dynamic behavior for the initial route? I imagine this would have plenty of use cases in static sites, too.

I ended up not using getInitialProps at all. Instead, I'm using a React hook. It works basically like this:
async function useModel() {
const modelRef = useRef(null)
// This hook will render at build-time by Next.js's static site, in which
// case the conditional loading of the model will never happen.
//
// At startup-time, it will be re-renderered on the Electron renderer thread,
// at which time, we'll actually want to load data.
if (process.browser && !modelRef.current) {
const m = new Model()
await m.init() // <- Assumed to have some async fetching logic
modelRef.current = m
}
return modelRef.current
}
Then, the top-level component can easily use the presence of the model to determine what to do next:
function Start() {
const model = useModel()
if (!model) {
return <div>Loading...</div>
} else {
return <MyProperUI model={model} />
}
}
Or, you could easily rig it up to show an unpopulated default UI, or whatever.
So basically, use getInitialProps for code you want to run exactly once, server-side/build-time or client-side. Otherwise, use other means of initialization. As seen here, hooks allow for this with pretty minimal boilerplate.

Related

How can I avoid prop drilling with a headless CMS? The context API I think hurts SEO

I want to use a headless CMO in my NextJs app (e.g. Sanity.io).
The content is especially important for SEO.
If I see it correctly, I can only receive the data on page-level via getStaticProps server-side to pre-render it that way (important for SEO).
If I now want to send the data from the page component to a deeply nested child, it's awkward via prop drilling.
My first thought was to use React's Context API (see code).
However, I suspect that during the build the state of the Context API does not take over the values (The SEO text for example).
So the pre-rendered page does not have the SEO content of the headless CMO.
Is there a way to send the values of the headless CMO to deeply nested children via getStaticProps without prop drilling? Or is the context API ok for this in terms of SEO / pre-render?
//pages/index.js
export default function Home({textFromGetStaticProps}) {
const value = useAppContext();
let {seotext, setSeotext} = value.content;
console.log("The State is currently: " + seotext);
console.log("The value of getStaticProps is currently: " + textFromGetStaticProps);
//Can not set this in useEffect since only runs on ClientSide!
setSeotext(() =>{
console.log("---> setCount läuft");
return textFromGetStaticProps;
})
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>
The SEO Text is {seotext}
</h1>
</main>
</div>
)
}
//Fetch headless CMO Date via getStaticProps
export async function getStaticProps(context) {
console.log("I am running Static Props");
//API Fetch of headless CMO
return {
props: {textFromGetStaticProps: "SEO Text aus StaticProps"}, // will be passed to the page component as props
}
}
//appContext.js
const AppContext = createContext();
export function AppWrapper({ children }) {
const [seotext, setSeotext] = useState("SEO Text");
const test = {seotext, setSeotext}
console.log("I am Running AppContext: " + test);
return (
<AppContext.Provider value={{
content: test,
}}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
```
Future versions of Next.js will make this much easier but in the meantime you could try using SWR or React Query to fetch data for pre-rendering and then query inside nested components.
There are a few ways to achieve this. The first that comes to mind would be to use something like the React Context API. However, you could also use something like Redux.
Here is an example using the Context API:
import React, { createContext, useContext, useState } from 'react';
const AppContext = createContext();
export function AppWrapper({ children }) {
const [seotext, setSeotext] = useState("SEO Text");
const test = {seotext, setSeotext}
console.log("I am Running AppContext: " + test);
return (
<AppContext.Provider value={{
content: test,
}}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}

Storyblok React Bridge Restarting Preview On Data Change

I want to use the new Storyblok React bridge with Next.js. In my case, there is a component directly in the _app.js (which should not change between route changes), so it looks like this:
function MyApp({ Component, pageProps: { pageData, globalData } }) {
const story = useStoryblokState(globalData.story)
console.log('rendering app')
return (
<>
<Collage story={story} />
{/* <Component {...pageData} /> */}
</>
);
}
The corresponding getStaticProps looks like this:
export async function getStaticProps() {
let sbParams = {
version: "draft", // or 'published'
};
const storyblokApi = getStoryblokApi();
let { data: pageData } = await storyblokApi.get(`cdn/stories/home`, sbParams);
let { data: globalData } = await storyblokApi.get(
`cdn/stories/collage`,
sbParams
);
return {
props: {
pageData,
globalData,
},
revalidate: 3600,
};
}
Here everything works as intended (i.e. changes to data will only rerender the collage component but not touch anything else), but as soon as I uncomment the main component, in the Storyblok preview (with localhost:3000), changing any data will restart the iFrame, just like it would be the case if no React bridge is used. I can even see that the data changes are applied just before it restarts. Any idea how to fix this?
useStoryblokState() calls useStoryblokBridge() from the #storyblok/js package. Despite its name, useStoryblokBridge() is not a react hook, but an ordinary function with side effects. It subscribes to events from the Storyblok bridge here. If the story that was passed as argument isn't the same story that is sent to the event listener via the Storyblok bridge, the event listener will reload the entire page.
https://github.com/storyblok/storyblok-js/blob/main/lib/index.ts#L40
This means that if you call useStoryblokBridge() with two different stories, the window will reload.

How can I disable scrolling when there's a modal using nextjs tailwind?

I currently have this code set up but I don't think this is the correct one to use as it gives me "document is not defined".
export default function Modal() {
const [modal, setModal] = useState(false);
const toggleModal = () => {
setModal(!modal);
};
// BELOW IS THE ERROR
if (modal) {
document.body.classList.add('active-modal');
} else {
document.body.classList.remove('active-modal');
}
I didn't test the result but try to write the if statement inside useEffect() hook. I think initially the document object is unknown to nextJs same goes for the global window object!
For side-effects always try to use useEffect() hook. useEffect() runs after the component is mounted on the DOM.

Next.js getInitialProps not rendering on the index.js page

I really can't figure out what is wrong with this code on Next.js.
index.js :
import { getUsers } from "../utils/users";
import React from "react";
Home.getInitialProps = async (ctx) => {
let elements = [];
getUsers().then((res) => {
res.map((el) => {
elements.push(el.name);
});
console.log(elements);
});
return { elements: elements };
};
function Home({ elements }) {
return (
<div>
{elements.map((el, i) => {
<p key={i}>{el}</p>;
})}
</div>
);
}
export default Home;
This doesn't render anything on my main page but still console logs the right data on server side (inside the vscode console). I really can't figure out what's going on, I followed precisely the article on the next.js site.
The getUsers function is an async function that returns an array of objects (with name,surname props), in this case in the .then I'm grabbing the names and pushing them into an array that correctly logs out to the console.
How can I make this data that I get render on the page?? Surely something to do with SSR.
The problem is using async function. Try as following.
...
elements = await getUsers();
...
In your code, component is rendered before response is finished. So the data is not rendered. Suggest using "async...await...". Infact "async" and "await" are like a couple of one.

How to Manage a Navigation Menu in NextJS with WordPress

I'm building a NextJS app using headless WordPress with GraphQL. It's not clear from the documentation where I should be calling the query to create the site navigation.
https://github.com/lfades/next.js/tree/examples/cms-wordpress/examples/cms-wordpress
The navigation is controlled dynamically by WordPress Menus (Appearance > Menus) on the backend and I can successfully access these menuItems via GraphQL without any issue on the index.js and posts/[slug].js page templates in Next JS.
// index.js
export default function Index({ primaryMenu = [] }) {
return (
<Layout>
<Header>
{primaryMenu.map((item, index) => {
return (<a href={item.url}>{item.label}</a>)
)}
</Header>
</Layout>
);
}
export async function getStaticProps() {
const primaryMenu = await getPrimaryMenu(); // Get menu via GraphQL
return {
props: { primaryMenu },
};
}
The issue I'm having with this is I am repeating the getStaticProps function on each template and I should be able to use some sort of global query for this, either in the <header/> component itself or another method. I'm unable to find documentation on how to do this and it doesn't work in components.
Any guidance (or examples) on where a global query such as a dynamic Navigation query would live in a NextJS app is appreciated.
There are a couple of ways you can do it:
You can menuItems query with useQuery() from #apollo/client inside the Layout component so that its available to all pages which are wrapped inside the Layout. However the problem with this is that, there will be a load time and the data won't be prefetched and readily available like we can do with getServerSideProps() ( at page level ). Because this will be at component level.
import { useQuery } from "#apollo/client";
export default function Layout () {
const { loading, data } = useQuery( GET_MENU_QUERY )
return {...}
}
You can use swr that uses caching strategy. There is blog that explains how to use it
I battled this for a while (for JD site) with redux and wp rest, but I think theory should be the same for gql + apollo client.
You need to override Next App _app with a custom class that extends App.
And you might need to inject an instance of apollo client into AppContext using a HOC. I used this wrapper for Redux. Would need to be modelled after that.
Edit: (Looks like someone has made it already)
// export default withRedux(makeStore)(MyApp);
export default withApollo(apolloClient)(MyApp); ???
Then in your App getInitialProps, you can make query to get menu. By default apollo client query will grab cached value if it's in the cache store already I believe.
static async getInitialProps(appContext) {
const { isServer, pathname, apollo? } = appContext.ctx;
// do menu query
const menu = apollo.query???
// Redux version
// const state = store.getState();
// let main_menu = state.menu;
// if (!state.menu) {
// const menu = await apiService().getMenu("main");
// main_menu = menu;
// store.dispatch({ type: "SET_MENU", payload: menu });
// }
...
// call the page's `getInitialProps` and fills `appProps.pageProps`
const initialProps = await App.getInitialProps(appContext);
const appProps: any = {
...initialProps,
menu: main_menu
};
return appProps;
}
Now menu is in the page props of the App Component, which can be passed down.
Or you can use apollo client to make the query again in a child component. So when you make the query again, in header or whatever, it will take the cached response provided it's the same query.
I made an endpoint for menus that included the template name + post slug along with the menu items and mapped the wp templates to next routes.
const menu = useSelector((state: any) => state.menu);
const menuItems = menu.map((item: any) => {
const path = getTemplatePath(item.template);
return (
<Link key={item.slug} href={`/${path}`} as={`/${item.slug}`} scroll={false}>
<a>{item.title}</a>
</Link>
);
});

Resources