boardgameio / boardgame.io

State Management and Multiplayer Networking for Turn-Based Games
https://boardgame.io
MIT License
10k stars 706 forks source link

SSR: Server-Side Rendering #22

Closed Ponjimon closed 6 years ago

Ponjimon commented 6 years ago

Hello,

I'm currently trying to use boardgame.io together with next.js. Because the Server part exports a Koa server, I went with this example to create a basic project: https://github.com/zeit/next.js/tree/canary/examples/custom-server-koa

However, when I run the code, I get a window is not defined error. So I did some research and it's of course because Next.js is SSR and the Client component requires window to be present. So I made it like this:

const App = Client({
    game: Game,
    multiplayer: true,
});

export default class Index extends React.Component {
    state = {
        isClient: false,
    };

    componentDidMount() {
        this.setState({
            isClient: true,
        });
    }

    render() {
        if (this.state.isClient) {
            return <App />;
        }

        return 'Loading...';
    }
}

According to next.js, the client side code only runs, after componentDidMount has been triggered, so obviously window should be available, but somehow it isn't. Something inside Client seems to run before that happens.

nicolodavis commented 6 years ago

Hello! I'm not familiar with the details of next.js, so will have to do some reading before I can help.

To give you some immediate thoughts, the only component that uses window is Mousetrap, the library that I use to handle keyboard events, so try isolating code in src/client/debug/debug.js that uses it. Will look closer and help you with this when I have more time on the weekend.

tomasreichmann commented 6 years ago

So I've tried isolating those window references, but I couldn`t make it build without errors. To test the server-side rendering, I used Yalc to link the repository as an NPM package which didn't work.

It's not clear how to publish the package correctly. I did yarn prepare && yarn prepublishOnly && yalc publish But I always got some weird errors like

index.js?54be91e:369 babelHelpers is not defined
ReferenceError: babelHelpers is not defined
    at http://localhost:4000/_next/1515594262237/page/game.js:22448:5
    at Client (http://localhost:4000/_next/1515594262237/page/game.js:22568:4)
    at Object.<anonymous> (http://localhost:4000/_next/1515594262237/page/game.js:12640:40)
    at __webpack_require__ (http://localhost:4000/_next/1515594262237/manifest.js:714:31)
    at fn (http://localhost:4000/_next/1515594262237/manifest.js:117:20)
    at Object.<anonymous> (http://localhost:4000/_next/1515594262237/page/game.js:11616:22)
    at Object.module.exports.Object.defineProperty.value (http://localhost:4000/_next/1515594262237/page/game.js:11718:30)
    at __webpack_require__ (http://localhost:4000/_next/1515594262237/manifest.js:714:31)
    at fn (http://localhost:4000/_next/1515594262237/manifest.js:117:20)
    at Object.module.exports.Object.defineProperty.value (http://localhost:4000/_next/1515594262237/page/game.js:11562:18)

I tried removing the whole Debug component which helped with the "window not defined" problem, but then jumped to "document is not defined".

This is the part that deals with window in debug.js but it`s nearly not enough since Mousetrap has it inside as well.

saveState = () => {
     const json = JSON.stringify(this.props.gamestate);
-    window.localStorage.setItem('gamestate', json);
+    if (typeof window !== 'undefined') {
+      window.localStorage.setItem('gamestate', json);
+    } else {
+      console.warn('saveState executed before window was ready');
+    }
+
   }

   restoreState = () => {
-    const gamestateJSON =
-        window.localStorage.getItem('gamestate');
+    let gamestateJSON = null;
+    if (typeof window !== 'undefined') {
+      gamestateJSON = window.localStorage.getItem('gamestate');
+    } else {
+      console.warn('restoreState executed before window was ready');
+    }
+
     if (gamestateJSON !== null) {
       const gamestate = JSON.parse(gamestateJSON);
       this.context.store.dispatch(restore(gamestate));

Server-side rendering is becomming a standard and it`s a problem in many React libraries. So I am giving up and will move probably move to some generic redux wrapper instead. Too bad, looked real nice...

nicolodavis commented 6 years ago

The babelHelpers error was due to a regression in package-lock.json that has since been fixed.

About the rest, I'll take a look once I've finished up the current set of features planned and see if we can get this working.

nicolodavis commented 6 years ago

Also, how were you still having a Mousetrap dependency once you removed debug.js?

tomasreichmann commented 6 years ago

That would be awesome.

I just pasted the code for handling window in debug.js that wasn't enough since Moustrap is imported at the top of the file. Once I removed debug.js altogether, the Moustrap is no longer a problem. But anyway, I guess that is not really a solution, is it?

tomasreichmann commented 6 years ago

Maybe something like this could help... https://www.npmjs.com/package/window-or-global

nicolodavis commented 6 years ago

I think all the code in src should now be SSR-compatible. The examples directory (not included in the NPM) uses react-router, which I need to figure out how to use with SSR in order to properly demonstrate the feature. Any ideas or PR's are appreciated!

nicolodavis commented 6 years ago

Ugh, had to revert. Looks like changing import 'somefile.css' to require('somefile.css') doesn't play well with how Rollup bundles the library.

The strategy I followed initially was to change all the CSS imports to something like:

if (typeof window !== 'undefined') {
  require('somefile.css');
}

Any ideas on how to tackle CSS modules with SSR?

vdfdev commented 6 years ago

So I am trying to do SSR on my project. This is the code I wrote for SSR: https://github.com/Felizardo/turnato/blob/ssr/src/server.tsx#L30

This code is in a separate branch (ssr) because it does not work yet. I am getting this error trying to run the server:

ReferenceError: window is not defined at /home/felizardo/playground/turnato/node_modules/react-json-view/dist/main.js:1:149649 at /home/felizardo/playground/turnato/node_modules/react-json-view/dist/main.js:1:149611

So the error seems to be initially coming from a dependency of boardgame.io: react-json-view. I found this thread with other users having the same issue: https://github.com/mac-s-g/react-json-view/issues/121

In summary, this is happening because they have style loader module of webpack as their dependency. Even if we removed this dependency (react-json-view) from boardgame.io, this would not fix the issue as boardgame.io is itself dependent on style loader. So this goes back to what @nicolodavis was trying to do, avoid loading styles on the server.

Another option suggested on the thread was trying to use https://github.com/kriasoft/isomorphic-style-loader instead.

I will investigate those options further and let you know what I find :). SSR is very important for my project.

vdfdev commented 6 years ago

I got it working! :clap: Just submitted a PR, also managed to do a test so we dont break SSR in the future :laughing: