Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

nextjs Dynamic route rendering content not working

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;
like image 540
Ven Nilson Avatar asked Sep 24 '20 19:09

Ven Nilson


1 Answers

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);
});
like image 180
83C10 Avatar answered Oct 07 '22 20:10

83C10