Storyblok React Bridge Restarting Preview On Data Change - next.js

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.

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);
}

VurRouter transitions on more than one router

I prepared a boiled-down example on stackblitz:
https://stackblitz.com/edit/quasarframework-vy4eiw?file=README.md
The problem I try to resolve is this:
A quasar 2 app build with vite and vue 3 (and GSAP) uses layouts
Currently there are 2 layouts: StartpageLayout for the startpage at route ´/´and MainpageLayout for all the other pages at route ´/main´ and any children of it (/main/:child´`)
The MainpageLayout also contains the navigation menu
The navigation menu should be created (later on with an animation) when any route starting with ´/main´ is hit and destroyed, when there is a change to any other route
While navigating through any ´/main[/:child]´ route, the nav menu shall remain "stable" (not rebuild or anything like that)
The app uses 2 router-views for this, one in App.vue, one in MainLayout.vue. Changes between those states should mainly be handled in onBeforeRouteLeave and onBeforeRouteUpdate
To check, whether the app is in a "layout context", the routes have a meta.layoutKey, which is used in router guards to check, whether sth changed or not:
// Example: src/layouts/MainLayout.vue
onBeforeRouteUpdate((to, from, next) => {
console.log('%cMAIN_LAYOUT: onBeforeRouteUpdate invoked', consColRouter);
// compare meta.layoutKeys of routes
if (from.meta.layoutKey !== to.meta.layoutKey) {
console.warn(' YES, invoke router guard onBeforeRouteUpdate - animate!');
next() // this would be actually be called form an onComplete animation callback
} else {
console.log(' NOPE, do not invoke router guard onBeforeRouteUpdate');
next() // invoked as written
}
})
A pinia store manages state that (should) remember(s) activateMenu:
// Pinia store "pageTransitions.js" (composition API)
import { ref, reactive, computed } from 'vue'
import { defineStore } from 'pinia'
export const usePageTransitionsStore = defineStore('pageTransitions', () => {
// Pinia state in composition API
const pageTransitions = ref({
parent: false,
activateMenu: false
})
const setPageTransitions = (level, bool) => {
switch(level) {
case 'parent': {
pageTransitions.value.parent = bool
break
}
default: { console.log('default... must never happen!'); }
}
}
const setActivateMenu = (bool) => {
pageTransitions.value.activateMenu = bool
}
return {
pageTransitions,
setPageTransitions,
setActivateMenu
}
})
If store.pageTransitions.activateMenu is true, show the menu, if false, remove it. It is imported in MainLayout in order to use the activateMenu constant to manage the state of the nav menu. The onMount method sets this store variable to true. And it should be set to false in a ònBeforeRouteLeave`... (not yet implemented)
While the change from the startpage at ´/´to the MainPage at ´/main´ and vice versa works fine (even with animation, due to the store variable store.pageTransitions.parent), I keep having troubles with changes from ´/main´ to any child route ´/main/:child´ and vice versa. E.g. when the app is at /main and the user clicks on ´items 101´, the whole MainLayout is reloaded - also App.vue runs through its onAppear hooks again (see console) – and the nav is set to false again.
The goal is to not influence the MainLayout not its nested nav menu at all.
I wonder, why those reloads happen? MainLayout's onBeforeRoute checks against meta.layoutKey which does not change. But then I also observe that the pinia store gets loaded again, and the actiavteMenu var is set up false again...
Does anybody see my error(s)?

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>
);
});

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

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.

Resources