15

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.

CMS Response

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.

Preview the problem

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;
Ven Nilson
  • 969
  • 4
  • 16
  • 42
  • Do you have any error in console when you click on accordion ? (In the not working version) – dna Sep 27 '20 at 09:19
  • Can you please provide the CMS url? If it's possible, can you provide the username & password? –  Sep 27 '20 at 09:32
  • @dna No, There is not any console error. I think `dangerouslySetInnerHTML={{ __html: result.html }}` is not running javascript `` and `` element always runs on the client-side. – Ven Nilson Sep 27 '20 at 09:40
  • @TopTalent it is confidential credentials. – Ven Nilson Sep 27 '20 at 09:43
  • @VenNilson How can I test your git repo? –  Sep 27 '20 at 09:46
  • What library are you using for the accordion ? does the html sent back from the server include javascript/script tags? If your script that initializes the accordion is on the main page and has already run, then it would need to be run again, each time you load new content in the page or refresh aspects of the DOM. This could explain why it works on a complete page refresh, the accordion elements are now in the DOM when the accordion script runs so that it can bind events to these elements. – ggordon Sep 27 '20 at 13:42
  • @ggordon yes the HTML sent back from the server includes javascript/script tags. – Ven Nilson Sep 27 '20 at 14:39
  • I wanted to determine whether your accordion library was executing after a page load event which may explain why it loads the page correctly after a browser refresh. You could extract the script tags and manually run the scripts after the component mounts using reacts [useEffect](https://reactjs.org/docs/hooks-effect.html) – ggordon Sep 27 '20 at 16:23
  • 1
    I think this [stackoverflow question](https://stackoverflow.com/questions/35614809/react-script-tag-not-working-when-inserted-using-dangerouslysetinnerhtml) will help you. Actually, script tag does not work inside dangerouslySetInnerHTML, so you need to run the scripts after the component is mounted. – Richard Zhan Sep 27 '20 at 22:50
  • You can add code in a `script` tag using `dangerouslySetInnerHTML`, but I believe the problem in your case is in where you set up your scripts, not how. Please check out my answer. – Nick Louloudakis Oct 04 '20 at 19:10
  • @VenNilson I am running into similar issue. Where there any findings or fix for this? – Amit Jun 08 '21 at 07:33

2 Answers2

2

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);
});
83C10
  • 1,112
  • 11
  • 19
0

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.

Nick Louloudakis
  • 5,856
  • 4
  • 41
  • 54