Having troubles registering route change events on NextJs (SSR enabled) app.
I am using the History Change event trigger in GTM.
Using the react-gtm-module to initialise the Google Tag Manager.
Issue: The event fires on mount, with route eg. www.site.com/product/123
But the event does NOT fire when from the page /product/123/ I navigate to a page like /product/124/.
Product page:
return (
<div>
<div>{props.title}</div>
<Link href="/product/[pid]" as={`/product/124`}>
<a>Another product (should trigger event here, but doesn't)</a>
</Link>
</div>
)
I have tried adding this to _app.js (componentDidMount)
import Router from 'next/router';
...
componentDidMount () {
...
if (process.browser) {
Router.onRouteChangeComplete = url => {
window.history.pushState({}, document.title, url);
};
}
What is the proper way of doing this in NextJs?
You have to inject your tag manager script on every page. Since _document.js is the wrapper for every page, you should add the GTM script to _document.js in the head section as follow:
<Head>
<script dangerouslySetInnerHTML={{
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-*****');`,
}}>
</script>
...
</Head>
These links don't necessarily help with the packages mentioned above, but for other users arriving here later who want a solution without extra packages:
Here is an example repo from Next that shows how to implement GTM.
Attach listeners to route changes with next/router and call your GTM update handler:
const router = useRouter()
useEffect(() => {
router.events.on('routeChangeComplete', gtm.pageview)
return () => {
router.events.off('routeChangeComplete', gtm.pageview)
}
}, [router.events])
Additionally, here is the new (since Next 11) next/script component that has APIs designed to simplify the use of inline scripts.
Related
I use using bootstrap 5 in my NextJs app my installing like this:
npm install bootstrap and imported the necessary files into my project via _app.js like this:
import 'bootstrap/dist/css/bootstrap.css'
...
useEffect(() => {
import("bootstrap/dist/js/bootstrap");
}, []);
Now the problem is that when I open the offcanvas modal and click a link from there, the offcanvas stays open after the page changes, instead of closing. How do I programmatically close the offcanvas when the pathname changes and only when the offcanvas is active. Thank you
NB: I am not using react-bootstrap
Import like this:-
useEffect(() => {
import("bootstrap/dist/js/bootstrap.bundle");
}, []);
Recently I ran into the same problem, but here is how I got things working.
First of I do not think you are importing bootstrap in to your nextjs app the right way, but what works work. However I think you should import bootstrap into your nextjs app this way.
useEffect(() => {
typeof document !== undefined
? require("bootstrap/dist/js/bootstrap")
: null;
}, []);
I don't want to make this too long, so lets dive straight into creating the solution. First you have to create a custom function to close the offCanvas when you click your link.
const topicRef = useRef(null);
const closeOffCanvas = (event) => {
event.stopPropagation();
const bootstrap = require("bootstrap/dist/js/bootstrap");
var myOffcanvas = topicRef.current;
var bsOffcanvas = bootstrap.Offcanvas.getInstance(myOffcanvas);
bsOffcanvas.hide();
};
Now create your link like so and call the function we just created
<Link href={`/article/18004/close-bootstrap-5-offcanvas-in-next-js`} passHref>
<a href="#" onClick={closeOffCanvas}>navigate to page then close offCanvas</a>
</Link>
I hope this helps you
I am using the Sign In With Google button from Google Identity. I have put the HTML from this button documentation page into a React component. Looks like this:
export default function GoogleLoginButton() {
return (
<>
<div
id="g_id_onload"
data-client_id="XXXXXX"
data-auto_prompt="false"
></div>
<div
className="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left"
></div>
</>
);
}
On loading the page the first time the Google sign-in button appears correctly and I can log in. The sign-in button is then replaced by a log-out button. The problem is that when I click the log-out button which should render the Google sign-in button again, it doesn't reappear! Why is that?
I can add that refreshing the page after logging out brings back the Google button.
As Stian says, the google script injects the google button once the app renders. The problem is that if you re-render the component where your button is located it will disappear because the google script was already executed, and won't be executed in future re-renders (it won't inject the google button again).
A different solution is to call window.google.accounts.id.renderButton inside an useEffect, that useEffect should be placed inside the component that is re-rendered. This will re-inject the google button each time the useEffect is called (comoponent is re-rendered).
The first argument the renderButton method receives should be a ref to a div, the google script will inject the sign-in button inside that div.
NOTE: Remember to first initialize the google script calling google.accounts.id.initialize
Here's the example:
const divRef = useRef(null);
useEffect(() => {
if (divRef.current) {
window.google.accounts.id.initialize({
client_id: <YOUR_CLIENT_ID_GOES_HERE>,
callback: (res, error) => {
// This is the function that will be executed once the authentication with google is finished
},
});
window.google.accounts.id.renderButton(divRef.current, {
theme: 'filled_blue',
size: 'medium',
type: 'standard',
text: 'continue_with',
});
}
}, [divRef.current]);
return (
{/* Other stuff in your component... */}
{/* Google sign in button -> */} <div ref={divRef} />
);
The reason it doesn't work is because the accompanying client library doesn't run again on later renders.
On page load the client library runs and injects an iframe where the HTML is. On later renders one can see in the DOM that this doesn't happen; the HTML is present but no iframe.
One solution is to never remove the HTML from DOM. Instead, apply the style display: none to the sign-in button when in need of hiding it.
For TypeScript, I have modified Alex's answer a bit.
First don't forget to add the <script src="https://accounts.google.com/gsi/client" async defer></script> to the the index.html page found in the react public folder.
Create a google_sso.ts file and use this code:
import { useRef, useEffect } from 'react';
declare const google: any;
const GoogleSSO = () => {
const g_sso = useRef(null);
useEffect(() => {
if (g_sso.current) {
google.accounts.id.initialize({
client_id: "xxxxxxxx-koik0niqorls18sc92nburjfgbe2p056.apps.googleusercontent.com",
callback: (res: any, error: any) => {
// This is the function that will be executed once the authentication with google is finished
},
});
google.accounts.id.renderButton(g_sso.current, {
theme: 'outline',
size: 'large',
type: 'standard',
text: 'signin_with',
shape: 'rectangular',
logo_alignment: 'left',
width: '220',
});
}
}, [g_sso.current]);
return (<div ref={g_sso} />);
}
export default GoogleSSO
Then wherever you need the button, use this:
import GoogleSSO from "../common/google_sso";
<GoogleSSO />
I am stuck on this problem for many days. I am using Next.js and have 3 pages.
pages/index.js
pages/categories.js
pages/categories/[slug].js
The categories/[slug].js is using Next.js fetching method name getServerSideProps that runs on each request and used for build dynamic pages on runtime. The categories/[slug].js is rendering a dynamic content on the page that dynamic content comes from the CMS as a response from the API Endpoint. Dynamic content is nothing but a string that contains HTML with <script /> elements.
Note: To fetch the content from the CMS we have to send a POST request with the CMS credentials like username, password, and the page slug for the content. I am using axios library to send a post request and the method is inside post.js file.
post.js:
import axios from 'axios';
const postCMS = async (slug) => {
const url = `${process.env.CMS_API_URL}/render-page/`;
let pageSlug = slug;
// If the pageSlug is not start with `/`, then create the slug with `/`
if (!pageSlug.startsWith('/')) {
pageSlug = `/${pageSlug}`;
}
const head = {
Accept: 'application/json',
'Content-Type': 'application/json'
};
const data = JSON.stringify({
username: process.env.CMS_API_USERNAME,
password: process.env.CMS_API_PASSWORD,
slug: pageSlug
});
try {
const response = await axios.post(url, data, {
headers: head
});
return response.data;
} catch (e) {
return e;
}
};
export default postCMS;
But for the rendering content on the categories/[slug].js page, I am using the Reactjs prop name dangerouslySetInnerHTML to render all the HTML which also contains <script /> elements in the JSON string.
pages/categories/[slug].js:
<div dangerouslySetInnerHTML={{ __html: result.html }} />
The content is loading fine based on each slug. But when I navigate to that category page i.e.pages/categories/index.js.
<Link href="/categories/[slug]" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
It has a <Link /> element and when I click it.
The dynamic content is loading fine but that dynamic content contains accordion and slider elements they are not working. I think <script /> of these elements is not working. But when I refresh the page they work fine. See this.
They also work fine when I set the Link something like this.
<Link href="/categories/online-cloud-storage" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
But after setting the link like the above method, the click is caused to hard reload the page. But I don't want this. Everything should work. When the user clicks on the category link.
Is there a way to fix this?
Why the content elements are not working when you click from the categories/index.js page?
Github repo
Code:
pages/index.js:
import React from 'react';
import Link from 'next/link';
const IndexPage = () => {
return (
<div>
<Link href="/categories">
<a>Categories</a>
</Link>
</div>
);
};
export default IndexPage;
pages/categories/index.js:
import React from 'react';
import Link from 'next/link';
const Categories = () => {
return (
<div>
<Link href="/categories/[slug]" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
</div>
);
};
export default Categories;
pages/categories/[slug].js:
import React from 'react';
import Head from 'next/head';
import postCMS from '../../post';
const CategoryPage = ({ result }) => {
return (
<>
<Head>
{result && <link href={result.assets.stylesheets} rel="stylesheet" />}
</Head>
<div>
<h1>Category page CMS Content</h1>
<div dangerouslySetInnerHTML={{ __html: result.html }} />
</div>
</>
);
};
export const getServerSideProps = async (context) => {
const categorySlug = context.query.slug;
const result = await postCMS(categorySlug);
return {
props: {
result
}
};
};
export default CategoryPage;
The problem here is that <script> tags which are dynamically inserted with dangerouslySetInnerHTML or innerHTML, are not executed as HTML 5 specification states:
script elements inserted using innerHTML do not execute when they are inserted.
If you want to insert new <script> tag after the page has initially rendered, you need to do it through JavaScript's document.createElement('script') interface and appended to the DOM with element.appendChild() to make sure they're executed.
The reason why the scripts don't work after changing routes, but they do work after you refresh the page is tied to Next.js application lifecycle process.
If you refresh the page, Next.js pre-renders the entire website on the server and sends it back to the client as a whole. Therefore, the website is parsed as a regular static page and the <script> tags are executed as they normally would.
If you change routes, Next.js does not refresh the entire website/application, but only the portion of it which has changed. In other words, only the page component is fetched and it is dynamically inserted into existing layout replacing previous page. Therefore, the <script> tags are not executed.
Easy solution
Let some existing library handle the hard work for you by parsing the HTML string and recreating the DOM tree structure. Here's how it could look in jQuery:
import { useEffect, useRef } from 'react';
import $ from 'jquery';
const CategoryPage = ({ result }) => {
const element = useRef();
useEffect(() => {
$(element.current).html($(result.html));
}, []);
return (
<>
<Head>
{result && <link href={result.assets.stylesheets} rel="stylesheet" />}
</Head>
<div>
<h1>Category page CMS Content</h1>
<div ref={element}></div>
</div>
</>
);
};
export const getServerSideProps = async (context) => {
/* ... */
return { props: { result } };
}
Harder solution
You would have to find a way to extract all <script> tags from your HTML string and add them separately to your page. The cleanest way would be to modify the API response to deliver static HTML and dynamic script in two separate strings. Then, you could insert the HTML with dangerouslySetInnerHTML and add script in JS:
const scripts = extractScriptTags(result.html); // The hard part
scripts.forEach((script) => {
const scriptTag = document.createElement('script');
scriptTag.innerText = script;
element.current.appendChild(scriptTag);
});
IMHO, I believe that the script non-proper loading is due to erroneous import of the scripts on Client-Side Rendering (CSR) and Server-Side Rendering (SSR). Read more here, but also take a look on this interesting article. This would also explain the problematic behavior of your link component.
In your case, I understand that this behavior is due to false handling of the lifecycle of the page and its components during CSR, as many scripts might need to properly shared across the navigation of pages, possibly in SSR. I do not have the full picture of what the problem is or extended expertise on NextJS, but I believe that those scripts should be imported in one place and possibly rendered on the server, instead of false importing on each page, falsely letting CSR do the work in a non-NextJS optimized manner.
The suggested way is to use a custom Document implementation (SSR-only) for your application, where you can define the scripts. See here for more details on this. Also I suppose you already have setup a custom App file for your App, where you will use it in your document, for both CSR and SSR rendering common to all pages (See this SO question for more on that).
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
// ...
}
render() {
return (
<Html>
<Head>
{/*Your head scripts here*/}
</Head>
<body>
<Main />
{/*Your body scripts here*/}
<NextScript />
</body>
</Html>
)
}
}
The Head native component does a lot of work on the background in order to setup things for scripts, markup, etc. I suggest you go that way, instead of just adding the scripts into each page directly.
I'm trying to make a Twitter component that loads the tweet from the twitter API and displays its HTML, which will be a <blockquote> and a <script> tag.
The advantage of this, if server side rendered, is that it would work even if the user has privacy settings that block the call to the twitter script: the user will still get the blockquote, which is better than the current behavior (shows nothing).
So, my idea was to do something like this:
import fetch from 'node-fetch'
function Tweet(props) {
return <>
<div dangerouslySetInnerHTML={{
__html: props.__html
}} />
<p>here I am</p>
</>
}
Tweet.getInitialProps = async ctx => {
console.log("here I am on the road again")
const res = await fetch(`https://api.twitter.com/1/statuses/oembed.json?id=${ctx.tweetId}`)
const json = await res.json()
return { __html: json.html }
}
export default Tweet
And then on the pages use it like <Tweet tweetId="blah" />.
I found two problems with my idea though:
As per docs I cant access the tweetId property on getInitialProps
getInitialProps is never called. The <p>here I am</p> appears on the HTML and the log is never printed anywhere.
So, my question is: what I am doing wrong? Is this even possible to do?
Thanks!
As per Next.js documentation, getInitialProps can only be used to a page, and not in a component:
getInitialProps can only be added to the default component exported by a page, adding it to any other component won't work.
I have an app react using CRA, and I am trying to turn it into an SSR app using next. So, since there is little, the only things I changed were:
getInitialProps instead of useEffect and useState
Link from "next / link" instead of using react router dom
But when I click on the link, I get hard refresh.
Here is what is generating the link:
<Link href={post.meta.slug}>
<a>{post.title}</a>
</Link>;
I also tried with href={post.meta.slug} as={post.meta.slug}.
In my pages directory i have:
index.jsx
[slug].jsx
And this is how I get the post in [slug].jsx:
const PostPage = ({ post }) => {
return <Base>{post ? <Post post={post} /> : null}</Base>;
};
PostPage.propTypes = {
post: PropTypes.object,
};
PostPage.getInitialProps = async ({ query }) => {
const post = await getPostBySlug(query.slug);
return { post };
};
And so far I couldn't identify the error.
Here is the complete code: https://gitlab.com/flakesrc/blog-webstation-next
If you want to clone:
git clone https://gitlab.com/flakesrc/blog-webstation-next.git
cd blog-webstation-next
npm install
npm run dev
Have you tried this format for your Link?
<Link href='/[slug]' as={`/${post.meta.slug}`}>
<a>{post.title}</a>
</Link>
Here is a good example of this type of routing for dynamic pages as well as the section in the docs that also speaks to this a bit.