NextJS Track Mounted Components in SSR
Asked Answered
M

2

8

The following applies to SSR via NextJS.

I'm using React's context to keep track of IDs of certain mounted components. The gist is

class Root extends React.Component {
  getChildContext () {
    return {
      registerComponent: this.registerComponent
    }
  }

  registerComponent = (id) => {
    this.setState(({ mountedComponents }) => {
      return { mountedComponents: [...mountedComponents, id ] }
    })
  }

  ...
}

class ChildComponent {
  static contextTypes = { registerComponent: PropTypes.func }

  constructor(props) {
    super(props)

    props.registerComponent(props.id)
  }
}

unfortunately this only works client-side. this.state.mountedComponents is always [] on the server. Is there another way to track these components server-side? Basically I need the ids to provide to a script to run in the head of the document - waiting until the client app mounts, runs, and appends to the head manually is a little too slow.

update

here's a quick example repo: https://github.com/tills13/nextjs-ssr-context

this.context is undefined in the constructor of Child, if I move it to componentDidMount (currently set up this way in the repo), it works, but I'd like this to be resolved server-side. I'm not dead-set on context, if there's another way to do this, I'm all ears.

Millstream answered 11/6, 2018 at 19:32 Comment(5)
Which version of nextjs are you using? These context's APIs were experimental. With react 16 there are new APIs.Aircraft
I'm using Next 6.0. "New" context APIs (React.createContext) don't work at all in SSR. Discussed in github.com/zeit/next.js/issues/4182 / github.com/zeit/next.js/issues/4194Millstream
try moving the function call to componentDidMount from the constructorEngrail
@gandharvgarg that defeats the purpose as componentDidMount only runs clientside. I need this to be done serverside.Millstream
@TarunLalwani updatedMillstream
S
3

I have dig into this and my feeling is that this is not possible for multiple reason.

The setState update is async and won't be executed in your case.

The render of your provider will happen even before the state gets updated, so registration code will be execute later and the render function doesn't have the latest state

I put different console.log and below is what I got

Provider constructed with { mountedComponents: [],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
getDerivedStateFromProps
Renderring Provider Component { mountedComponents: [],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
data loaded
constructor Child { id: '1' } { registerComponent: [Function: value] }
Register Component called 1 { mountedComponents: [],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 1
constructor Child { id: '2' } { registerComponent: [Function: value] }
Register Component called 2 { mountedComponents: [ '1' ],
  name: 'tarun-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 2
constructor Child { id: '3' } { registerComponent: [Function: value] }
Register Component called 3 { mountedComponents: [ '1', '2' ],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 3
constructor Child { id: '4' } { registerComponent: [Function: value] }
Register Component called 4 { mountedComponents: [ '1', '2', '3' ],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 4

The information is actually only available when the componentDidMount occurs. But since in case of server side rendering that doesn't happen, you don't get the data.

I am digging further to see if any hacks are available, but as of now I am not sure

Sesquiplane answered 24/6, 2018 at 14:1 Comment(0)
P
2

Using the context API inside the contructor is possible, you just need to pass it as a parameter inside both the constructor and the super method as below:

export class Child extends React.Component {
  static contextTypes = { registerComponent: PropTypes.func };

  constructor(props, context) {
    super(props, context);
    context.registerComponent(props.id);
  }

  render() {
    return <div id={this.props.id} />;
  }
}

update:
so, the problem is related to the first render on the server that doesn't support any interractivity, this approche will prevent any setState to be considered on the server, in your example the <Provider /> wouldn't give as any hand, so we need a workaround that will be a hack in any case.

the solution i got is having a new window.ids with every tracked component render(using the Head provided from nextjs):

export class Child extends React.Component {
  registerComponent = (id) => `
    if (window.ids) {
      window.ids = [...window.ids, ${id}];
    } else window.ids = [${id}];
  `;

  render() {
    return (
      <Fragment>
        <Head>
          <script
            className="track"
            dangerouslySetInnerHTML={{
              __html: `${this.registerComponent(this.props.id)}`,
            }}
          />
        </Head>
        <div id={this.props.id} />
      </Fragment>
    );
  }
}

so the window.ids variable will be available before the <App /> renders.
here is a repos link to test it.

one other solution could be using a global variable on the server, then in each tracked component we could mutate that global varibale using the componentWillMount life cycle hook since it will be executed on the server only, then inject those ids on the html template <head />, but this is possible if we are executing the renderToString method.

second solution: using the pages/_document instead of pages/_app so we get access to the server before it renders to string!:
this is the repo branch: origin/workaround-using-document

-Child component:

export class Child extends React.Component {
  render() {
    return (
      <Fragment>
        <Head>
          <meta className="track" id={this.props.id} />
        </Head>
        <div id={this.props.id} />
      </Fragment>
    );
  }
}

-Document Component(replacing the App):

export default class MyDocument extends Document {
  render() {
    const { head } = this.props;
    const idsFromHead = head.reduce(
      (acc, { props }) =>
        (props.className.includes('track') && [...acc, props.id]) || acc,
      []
    );

    return (
      <html>
        <Head>
          <script
            dangerouslySetInnerHTML={{
              __html: `window.ids=[${idsFromHead}]`,
            }}
          />
        </Head>
        <body className="custom_class">
          <h3>{idsFromHead}</h3>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}

this approche works 100% on the server since we could capture the tracked ids before server rendrering occurs. enter image description here

but the <NextScript /> is generating a warning(don't know why, could be a bug from nextjs):
enter image description here

Prine answered 23/6, 2018 at 2:47 Comment(5)
What you say is correct, but have you tested to see if this really solves the problem?Sesquiplane
@TarunLalwani yes even the <Provider /> will be first rendred before any tracked component, i have a work around so i will update the response with details.Prine
hm. interesting - as it stands, that second method works but has wonky behaviour between page transitions.Millstream
yup, the hydration transition is bizarre, if i remove the <NextScript /> i will get a clean html SSR rendred with the <head> tag(usefull to inject scripts, styles and metadata) and without warnings, but without react components on the dom, even the pages/_app.js document uses the <NextScript /> under the hood to inject the bundled js to bring life to dead components, something is not ok with next!Prine
@TylerSebastian, tried to dig more on that warning but i couldn't reproduce it again, it has been disappeared and i think it was related to some cashed things!, i just get an HMR warning in firefox, but it is ok with chrome, could you verify that ?Prine

© 2022 - 2024 — McMap. All rights reserved.