Taylor A. Leach
Taylor A. Leach

Reputation: 2324

How to prevent parent component from re-rendering with React (next.js) SSR two-pass rendering?

So I have a SSR app using Next.js. I am using a 3rd party component that utilizes WEB API so it needs to be loaded on the client and not the server. I am doing this with 'two-pass' rendering which I read about here: https://itnext.io/tips-for-server-side-rendering-with-react-e42b1b7acd57

I'm trying to figure out why when 'ssrDone' state changes in the next.js page state the entire <Layout> component unnecessarily re-renders which includes the page's Header, Footer, etc.

I've read about React.memo() as well as leveraging shouldComponentUpdate() but I can't seem to prevent it from re-rendering the <Layout> component.

My console.log message for the <Layout> fires twice but the <ThirdPartyComponent> console message fires once as expected. Is this an issue or is React smart enough to not actually update the DOM so I shouldn't even worry about this. It seems silly to have it re-render my page header and footer for no reason.

In the console, the output is:

Layout rendered
Layout rendered
3rd party component rendered

index.js (next.js page)

import React from "react";
import Layout from "../components/Layout";
import ThirdPartyComponent from "../components/ThirdPartyComponent";

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      ssrDone: false
    };
  }

  componentDidMount() {
    this.setState({ ssrDone: true });
  }

  render() {
    return (
      <Layout>
        {this.state.ssrDone ? <ThirdPartyComponent /> : <div> ...loading</div>}
      </Layout>
    );
  }
}

export default Home;

ThirdPartyComponent.jsx

import React from "react";

export default function ThirdPartyComponent() {
  console.log("3rd party component rendered");
  return <div>3rd Party Component</div>;
}

Layout.jsx

import React from "react";

export default function Layout({ children }) {
  return (
    <div>
      {console.log("Layout rendered")}
      NavBar here
      <div>Header</div>
      {children}
      <div>Footer</div>
    </div>
  );
}

Upvotes: 5

Views: 14192

Answers (3)

Nicola Gaioni
Nicola Gaioni

Reputation: 178

I had a similar issue recently, what you can do is to use redux to store the state that is causing the re-render of the component.

Then with useSelector and shallowEqual you can use it and change its value without having to re-render the component.

Here is an example

import styles from "./HamburgerButton.module.css";
import { useSelector, shallowEqual } from "react-redux";
const selectLayouts = (state) => state.allLayouts.layouts[1];

export default function HamburgerButton({ toggleNav }) {
    let state = useSelector(selectLayouts, shallowEqual);
    let navIsActive = state.active;
    console.log("navIsActive", navIsActive); // true or false

const getBtnStyle = () => {
    if (navIsActive) return styles["hamBtn-active"];
    else return styles["hamBtn"];
};

return (
    <div
        id={styles["hamBtn"]}
        className={getBtnStyle()}
        onClick={toggleNav}
    >
        <div className={styles["stick"]}></div>
    </div>
);
}

This is an animated button component that toggles a sidebar, all wrapped inside a header component (parent)

Before i was storing the sidebar state in the header, and on its change all the header has to re-render causing problems in the button animation.

Instead i needed all my header, the button state and the sidebar to stay persistent during the navigation, and to be able to interact with them without any re-render.

I guess now the state is not in the component anymore but "above" it, so next doesn't start a re-render. (i can be wrong about this part but it looks like it)

Note that toggleNav is defined in header and passed as prop because i needed to use it in other components as well. Here is what it looks like:

 const toggleNav = () => {
    dispatch(toggleLayout({ id: "nav", fn: "toggle" }));
}; //toggleLayout is my redux action

I'm using an id and fn because all my layouts are stored inside an array in redux, but you can use any logic or solution for this part.

Upvotes: 1

yachaka
yachaka

Reputation: 5579

What you could do, is define a new <ClientSideOnlyRenderer /> component, that would look like this:

const ClientSideOnlyRenderer = memo(function ClientSideOnlyRenderer({
  initialSsrDone = false,
  renderDone,
  renderLoading,
}) {
  const [ssrDone, setSsrDone] = useState(initialSsrDone);

  useEffect(
    function afterMount() {
      setSsrDone(true);
    },
    [],
  );

  if (!ssrDone) {
    return renderLoading();
  }

  return renderDone();
});

And you could use it like this:

class Home extends React.Component {
  static async getInitialProps({ req }) {
    return {
      isServer: !!req,
    };
  };

  renderDone() {
    return (
      <ThirdPartyComponent />
    );
  }
  renderLoading() {
    return (<div>Loading...</div>);
  }

  render() {
    const { isServer } = this.props;

    return (
      <Layout>
        <ClientSideOnlyRenderer
          initialSsrDone={!isServer}
          renderDone={this.renderDone}
          renderLoading={this.renderLoading}
        />
      </Layout>
    );
  }
}

This way, only the ClientSideOnlyRenderer component gets re-rendered after initial mount. 👍

Upvotes: 4

Mohamed Ramrami
Mohamed Ramrami

Reputation: 12701

The Layout component re-renders because its children prop changed. First it was <div> ...loading</div> (when ssrDone = false) then <ThirdPartyComponent /> (when ssrDone = true)

Upvotes: 1

Related Questions