3

I'm trying to set up server-side rendering with the newest version of react-router v.4. I followed this tutorial https://react-router.now.sh/ServerRouter.

I get following error when I refresh browser: Invariant Violation: React.Children.only expected to receive a single React element child.

my routes.jsx file:

export default () =>
  <div>
    <Header />
    <Match pattern="/" component={Home} />
    <Match pattern="/about" component={About} />
    <Miss component={NotFound} />
  </div>;

and in index.jsx I'm rendering app

import  BrowserRouter from 'react-router';    
import Routes from './routes';

ReactDOM.render(<BrowserRouter> <Routes /> </BrowserRouter>, document.getElementById('app'));

Now as server I'm using express.js. Here is my configuration:

import  Routes  from '../routes';

server.use((req, res) => {
  const context = createServerRenderContext();
  let markup = renderToString(
    <ServerRouter location={req.url} context={context} > <Routes /> </ServerRouter>);
  const result = context.getResult();

  if (result.redirect) {
    res.writeHead(301, {
      Location: result.redirect.pathname,
    });
    res.end();
  } else {
    if (result.missed) {
      res.writeHead(404);
      markup = renderToString(
        <ServerRouter location={req.url} context={context}> <Routes /> </ServerRouter>);
    }
    res.write(markup);
    res.end();
  }
});

I didn't find any tutorial for server-rendering with this version of react-routes except official. Can anyone help me what I'm doing wrong ? thanks.

5 Answers 5

3

Solved !

First problem was that I had spaces around <Routes /> tag.

Correct solution:

<ServerRouter location={req.url} context={context}><Routes /></ServerRouter>);

Second problem was in included <Header /> tag in routes.jsx file.

I had the following error (Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. Check the render method of StatelessComponent)

File Header.jsx contained the following line of code:

import Link  from 'react-router';

Correct solution: (I forgot to put curly brackets ):

 import { Link } from 'react-router';
Sign up to request clarification or add additional context in comments.

Comments

1

The big issue is that the<BrowserRouter> is only expected to have one child, so you should wrap it's children in a div. This is done so that React Router is environment agnostic (you can't render a div in React Native, so RR expects you to include the appropriate wrapper).

export default () =>
  <BrowserRouter>
    <div>
      <Header />
      <Match pattern="/" component={Home} />
      <Match pattern="/about" component={About} />
      <Miss component={NotFound} />
    </div>
  </BrowserRouter>;

As a secondary issue, you are including the <BrowserRouter> in your <App> component, so it will be rendered on the server. You do not want this. Only the <ServerRouter> should be rendered on the server. You should move the <BrowserRouter> further up your client side component hierarchy to avoid this.

// App
export default () =>
  <div>
    <Header />
    <Match pattern="/" component={Home} />
    <Match pattern="/about" component={About} />
    <Miss component={NotFound} />
  </div>;

// index.js
render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('app'))

7 Comments

thanks for answer. I fixed this issues but still get same error.
Could you update your question with the current state of your code?
I updated my code and remove App.jsx file because it wasn't neccesary to use it anymore.
What is the full error? Does it reference a specific component or a line in your code?
Remove the spaces from around your <Routes /> component. Those are being interpreted as spaces when your JSX is being converted to React.createElement expressions.
|
1

because BrowserRouter doesn't exist on react-router, try install and import it from react-router-dom

Comments

0

I believe the answers listed above is outdated. As of today, the official react-router docs suggest using StaticRouter instead of ServerRouter for server side rendered apps.

A fantastic documentation can be found here.

Comments

-1

For anybody coming later, Ryan Florence has added a snippet of how to accomplish this.

SSR in React Router v4

// routes.js
const routes = [
  {
    path: '/',
    component: Home,
    exact: true
  },
  {
    path: '/gists',
    component: Gists
  },
  {
    path: '/settings',
    component: Settings
  }
]

// components
class Home extends React.Component {

  // called in the server render, or in cDM
  static fetchData(match) {
    // going to want `match` in here for params, etc.
    return fetch(/*...*/)
  }

  state = {
    // if this is rendered initially we get data from the server render
    data: this.props.initialData || null
  }

  componentDidMount() {
    // if rendered initially, we already have data from the server
    // but when navigated to in the client, we need to fetch
    if (!this.state.data) {
      this.constructor.fetchData(this.props.match).then(data => {
        this.setState({ data })
      })
    }
  }

  // ...
}

// App.js
const App = ({ routes, initialData = [] }) => (
  <div>
    {routes.map((route, index) => (
      // pass in the initialData from the server for this specific route
      <Route {...route} initialData={initialData[index]} />
    ))}
  </div>
)

// server.js
import { matchPath } from 'react-router'

handleRequest((req, res) => {
  // we'd probably want some recursion here so our routes could have
  // child routes like `{ path, component, routes: [ { route, route } ] }`
  // and then reduce to the entire branch of matched routes, but for
  // illustrative purposes, sticking to a flat route config
  const matches = routes.reduce((matches, route) => {
    const match = matchPath(req.url, route.path, route)
    if (match) {
      matches.push({
        route,
        match,
        promise: route.component.fetchData ?
          route.component.fetchData(match) : Promise.resolve(null)
      })
    }
    return matches
  }, [])

  if (matches.length === 0) {
    res.status(404)
  }

  const promises = matches.map((match) => match.promise)

  Promise.all(promises).then((...data) => {
    const context = {}
    const markup = renderToString(
      <StaticRouter context={context} location={req.url}>
        <App routes={routes} initialData={data}/>
      </StaticRouter>
    )

    if (context.url) {
      res.redirect(context.url)
    } else {
      res.send(`
        <!doctype html>
        <html>
          <div id="root">${markup}</div>
          <script>DATA = ${escapeBadStuff(JSON.stringify(data))}</script>
        </html>
      `)
    }
  }, (error) => {
    handleError(res, error)
  })
})

// client.js
render(
  <App routes={routes} initialData={window.DATA} />,
  document.getElementById('root')
)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.