Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server rendering methods other than renderToString(headTags) #14

Closed
Maarten88 opened this issue Nov 24, 2017 · 4 comments · Fixed by #18
Closed

Server rendering methods other than renderToString(headTags) #14

Maarten88 opened this issue Nov 24, 2017 · 4 comments · Fixed by #18

Comments

@Maarten88
Copy link

I implemented react-head in a small project, and got it working, great!

I did have some trouble however before I got it working, and had to make some modifications, I wonder if this may help others.

Because my server setup is somewhat different, I wanted to render the complete html using a react template that includes the complete head element. But I found that the HeadTag component only works correctly when the collected elements are rendered using renderToString(headTags), as shown in the docs/sample. HeadTag selects elements for client-side replacement by querying with [data-reactroot=""], which is added only to root elements by the renderToString function.

Doing it any other way breaks it. Rendering the headTags using a plain <head>{headTags}</head> does not insert the data-reactroot="" attribute, causing duplicate elements. Using <head>{renderToString(headTags)}</head> escapes it. Removing the [data-reactroot=""] selector breaks the clientside. In the end, I solved it by adding a specific data-ssr="" attribute on the server component and selecting on that. This makes HeadTag independent from the way it is rendered on the server. I also added a key to the client-side version, to get rid of some React warnings.

The result is this (it's also in typescript):

interface HeadTagProps {
    tag?: string;
}

export default class HeadTag extends React.Component<HeadTagProps & (React.MetaHTMLAttributes<{}> | React.LinkHTMLAttributes<{}>)> {
    static contextTypes = {
        reactHeadTags: PropTypes.object,
    };

    static propTypes = {
        tag: PropTypes.string,
    };

    static defaultProps = {
        tag: 'meta',
    };

    state = {
        canUseDOM: false,
    };

    componentDidMount() {
        // eslint-disable-next-line react/no-did-mount-set-state
        this.setState({ canUseDOM: true });

        const { tag, children, ...rest } = this.props; // eslint-disable-line react/prop-types
        const ssrTags = document.head.querySelector(`${tag}${buildSelector(rest)}[data-ssr=""]`);

        /* istanbul ignore else */
        if (ssrTags) {
            ssrTags.remove();
        }
    }

    render() {
        const { tag: Tag, ...rest } = this.props;

        if (this.state.canUseDOM) {
            const Comp = <Tag key={`${Tag}${Object.keys(rest).filter(key => key !== "content" && key !== "children").map(key => ':' + key).join('')}`} {...rest} />;
            return ReactDOM.createPortal(Comp, document.head);
        }

        // on client we don't require HeadCollector
        if (this.context.reactHeadTags) {
            const ServerComp = <Tag data-ssr="" {...rest} />;
            this.context.reactHeadTags.add(ServerComp);
        }

        return null;
    }
}

It's only three small changes, but server rendering is much more flexible, now I can render like this:

....
const GlobalScript = ({ state }) => <script dangerouslySetInnerHTML={{ __html: "window.initialReduxState = " + JSON.stringify(state) }} />

const Html = ({ headTags, appString, state }) => (
    <html lang="en">
        <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <link rel="stylesheet" href="/dist/site.css" />
            <base href="/" />
            {headTags}
        </head>
        <body>
            <div id="react-app" dangerouslySetInnerHTML={{__html: appString}} />
            <GlobalScript state={state} />
            <script src="/dist/vendor.js"></script>
            <script src="/dist/main-client.js"></script>
        </body>
    </html>
    );

// Once any async tasks are done, we can perform the final render
// We also send the redux store state, so the client can continue execution where the server left off
params.domainTasks.then(() => {
    let headTags: React.ReactElement<any>[] = [];
    const appString = renderToString(<App headTags={headTags} />); 

    resolve({
        html: renderToStaticMarkup(<Html headTags={headTags} appString={appString} state={store.getState()} />)
    });
}, reject); // Also propagate any errors back into the host application
@tizmagik
Copy link
Owner

Thanks @Maarten88 this is a great write up. I appreciate you taking the time!

I think this might be related to #15 — I will dig into this more tomorrow and should hopefully have a built in fix soon.

Thanks again!

@tizmagik
Copy link
Owner

I proposed a solution to this here, #15 (comment) would love to hear your thoughts @Maarten88

@Maarten88
Copy link
Author

Great, your proposal is exactly the same change that I made (except for the name of the attribute). It did not work without any attribute when I tried that.

If you want, I could push my updated version to github for comparison? I ported your code typescript and made a small change so it works for IE. It has been running without problems for a few weeks now, (on a site with with very low traffic however).

@jamesjjk
Copy link

@Maarten88 Great solution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants