React-router: Broken SSR: You should not use <Link> outside a <Router> when I'm clearly using <Link> inside a <Router>

Created on 15 Jun 2019  路  5Comments  路  Source: ReactTraining/react-router

Version

V5.0.1

Test Case

https://github.com/modsoussi/react-ssr

Steps to reproduce

  1. git clone https://github.com/modsoussi/react-ssr.git
  2. cd react-ssr
  3. npm i
  4. npm start
  5. Go to localhost:5000/
  6. Check logs for invariant error

Expected Behavior

Expect app to render. This is the code for App:

class App extends React.Component {
  render() {
    return (
      <div>
        <nav>
          <ul>
            <li><Link to="/">Hello</Link></li>
            <li><Link to="/bye">Bye</Link></li>
          </ul>
        </nav>

        <Route path="/" exact component={Hello} />
        <Route path="/bye" component={Bye} />
      </div>
    )
  }
}

On the client, it's rendered inside a BrowserRouter, and on the server it's rendered inside a StaticRouter.

  • Client:
hydrate(
    <BrowserRouter>
      <App />
    </BrowserRouter>, document.getElementById('app-root'));
  • Server:
const component = require('./dist');
const context = {};

const app = React.createElement(
  StaticRouter,
  { location: req.url, context: context },
  React.createElement(component)
);
const markup = renderToStaticMarkup(app);

Actual Behavior

renderToStaticMarkup(app) fails and throws Error: Invariant failed: You should not use <Link> outside a <Router>.

Most helpful comment

OK, so you're using two instances of the Router context pair. (Side note: I'll explain things fully here, but I'm going to have to turn this into a saved response...)

There is a new React.createContext API that we use in 5.0 to replace the legacy context usage. This involves creating a "context" pair, a set of Provider and Consumer components. We create that in a singleton module that is imported into Router and Link directly. In this new API, you have to use the _exact_ same instance. Two separately-created contexts will never match up and won't be visible to each other.

In your server, you're importing from your build output and from the react-router-dom module. This means that within your bundle, it's importing the bundle copy of context and in the server render, it's importing directly from the module (in node_modules). This results in the Provider in Router not being visible to the Consumer in Link.

How you solve this is up to you. The most ideal option is to not import your bundle, but import from src directly. (Note: that might involve using @babel/register if you're using JS features incompatible with your version of Node.) Another option is to export react-router-dom from your bundle and use that in your server renderer so that you have the same context pair. I can't tell you what is the best option, since you need to evaluate any tradeoffs.

One thing to add: This isn't fundamentally a React Router or even a React problem. Anything that uses a singleton module would be affected by this if you reference that singleton in your bundle and your server separately. It's the nature of JS itself, so the fixes I suggest are best for avoiding problems with other libraries as well.

All 5 comments

Similar issue - fixed by downgrading react-router-dom to 5.0.0.

Downgrading react-router-dom to 5.0.0 didn't work

OK, so you're using two instances of the Router context pair. (Side note: I'll explain things fully here, but I'm going to have to turn this into a saved response...)

There is a new React.createContext API that we use in 5.0 to replace the legacy context usage. This involves creating a "context" pair, a set of Provider and Consumer components. We create that in a singleton module that is imported into Router and Link directly. In this new API, you have to use the _exact_ same instance. Two separately-created contexts will never match up and won't be visible to each other.

In your server, you're importing from your build output and from the react-router-dom module. This means that within your bundle, it's importing the bundle copy of context and in the server render, it's importing directly from the module (in node_modules). This results in the Provider in Router not being visible to the Consumer in Link.

How you solve this is up to you. The most ideal option is to not import your bundle, but import from src directly. (Note: that might involve using @babel/register if you're using JS features incompatible with your version of Node.) Another option is to export react-router-dom from your bundle and use that in your server renderer so that you have the same context pair. I can't tell you what is the best option, since you need to evaluate any tradeoffs.

One thing to add: This isn't fundamentally a React Router or even a React problem. Anything that uses a singleton module would be affected by this if you reference that singleton in your bundle and your server separately. It's the nature of JS itself, so the fixes I suggest are best for avoiding problems with other libraries as well.

Take a look at my answer for a possible solution here:

https://github.com/ReactTraining/react-router/issues/6769#issuecomment-505435886

Exporting StaticRouter from my bundle did the trick. I import it and use it in my server's code.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ackvf picture ackvf  路  3Comments

maier-stefan picture maier-stefan  路  3Comments

sarbbottam picture sarbbottam  路  3Comments

imWildCat picture imWildCat  路  3Comments

stnwk picture stnwk  路  3Comments