Taylor Austin
Taylor Austin

Reputation: 5987

Server Side Rendering with React Router Dom getting error: Warning: React.createElement: type is invalid -- expected a string

Exact Error:

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for co
mposite components) but got: object.
    in Posts
    in Connect(Posts)
    in Route
    in Switch
    in div
    in App
    in Route
    in Switch
    in div
    in Router
    in StaticRouter
    in Provider

I am getting error with doing server side rendering with react router 4. The error doesn't really point to anything in particular, but I think this comes from how I use the routes and exporting the components. I will post relevant code to this problem.

For client code I will post one component container since I export all of them in the same way. I just want you to get a since on how this is done. The component itself works just fine so I will be posting the export code at the bottom.

Note: this is a full working demo project (https://github.com/tbaustin/demo-SSR-RR4) and I use this same technique. I am also using turbo which is a scaffold type of system for react/redux. You will see that the app.js looks a little different than a normal express application, but I ensure you it works the same.

SERVER:

app.js:

require('babel-core/register')({
  presets: ['env', 'react', 'stage-0', 'stage-1']
});

const pkg_json = require('./package.json');
const vertex = require('vertex360')({ site_id: pkg_json.app });
var renderer = require('./renderer.js');

// initialize app
const app = vertex.app();

// import routes
const index = require('./routes/index');
const api = require('./routes/api');

// hopefully will be used on every Route, this should handle SSR RR4
app.use(renderer);

// set routes
app.use('/', index);
app.use('/api', api); // sample API Routes

module.exports = app;

renderer.js:

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import serialize from 'serialize-javascript';
import { Helmet } from 'react-helmet';
import { matchRoutes } from 'react-router-config';

import routes from './src/routes';
import createStore from './src/stores';

function handleRender(res, req) {
  const store = createStore.configure(null); // create Store in order to get data from redux

  const promises = matchRoutes(routes, req.path)
    .map(({ route }) => {
      // Matches the route and loads data if loadData function is there
      return route.loadData ? route.loadData(store) : null;
    })
    .map(promise => {
      if (promise) {
        return new Promise((resolve, reject) => {
          promise.then(resolve).catch(resolve); // lets all data load even if route fails
        });
      }
    });

  Promise.all(promises).then(() => {
    const context = {};
    if (context.url) {
      return res.redirect(301, context.url); // redirect for non auth users
    }

    if (context.notFound) {
      res.status(404); // set status to 404 for unknown route
    }

    const content = renderToString(
      <Provider store={store}>
        <StaticRouter location={req.path} context={context}>
          <div>{renderRoutes(routes)}</div>
        </StaticRouter>
      </Provider>
    );

    const initialState = serialize(store.getState());

    const helmet = Helmet.renderStatic();

    res.render('index', { content, initialState, helmet });
  });
}

module.exports = handleRender;

index.mustache:

<!DOCTYPE html>
<html dir="ltr" lang="en-US">
<head>
    {{{ helmet.title }}}
    {{{ helmet.meta }}}
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <link href="https://fonts.googleapis.com/css?family=Pathway+Gothic+One:300,400,500,600,700|Lato:300,400,400italic,600,700|Raleway:300,400,500,600,700|Crete+Round:400italic" rel="stylesheet" type="text/css" />
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link rel="stylesheet" href="/dist/css/style.min.css" type="text/css" />
</head>
<body>
    <div id="root">{{{ content }}}</div>


    <!-- Turbo library imports: jQuery, Turbo CDN, sample app.js -->
    <script>
        window.INITIAL_STATE = {{{ initialState }}}
    </script>
    <script type="text/javascript" src="/dist/js/vendor.min.js"></script>
    <script type="text/javascript" src="https://cdn.turbo360-dev.com/dist/turbo.min.js"></script>
    <script type="text/javascript" src="/dist/bundle/commons.js"></script>
    <script type="text/javascript" src="/dist/bundle/app.js"></script> <!-- React code bundle -->
</body>
</html>

CLIENT:

index.js:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import store from './stores';
import routes from './routes';

const initialState = window.INITIAL_STATE;

ReactDOM.hydrate(
  <Provider store={store.configure(initialState)}>
    <BrowserRouter>
      <div>{renderRoutes(routes)}</div>
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

Root App component:

import React from 'react';
import { renderRoutes } from 'react-router-config';

import Header from './partials/Header';
import actions from '../actions';

const App = ({ route }) => {
  return (
    <div>
      <Header />
      {renderRoutes(route.routes)}
    </div>
  );
};

export default {
  component: App,
  loadData: ({ dispatch }) => dispatch(actions.currentUser())
};

Posts.js:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import swal from 'sweetalert';
import { Link } from 'react-router-dom';

import { CreatePost } from '../view';
import { Account } from '../containers';
import actions from '../../actions';
import { DateUtils } from '../../utils';

class Posts extends Component {
  componentDidMount() {
    if (this.props.post.all == null) {
      this.props
        .fetchPosts({})
        .then(response => {
          return null;
        })
        .catch(err => {
          console.log(err);
        });
    }
    if (this.props.reply.all == null) {
      this.props
        .getReplies({})
        .then(() => {
          return null;
        })
        .catch(err => {
          console.log(err);
        });
    }
  }

  createPost(params) {
    const { currentUser } = this.props.user;
    if (currentUser == null) {
      swal({
        title: 'Oops...',
        text: 'Please Login or Register before posting',
        type: 'error'
      });
      return;
    }

    const updated = Object.assign({}, params, { profile: currentUser });

    this.props
      .createPost(updated)
      .then(data => {
        swal({
          title: 'Post Created',
          text: `Title: ${data.title}`,
          type: 'success'
        });
      })
      .catch(err => {
        console.log(err);
      });
  }

  render() {
    const posts = this.props.post.all;
    const { currentUser } = this.props.user;
    return (
      <div>
        <div className="row">
          <div className="col-sm-8">
            <div className="card-columns">
              {posts == null
                ? null
                : posts.map(post => {
                    return (
                      <div
                        key={post.id}
                        className="card text-white bg-dark mb-3"
                        style={{ maxWidth: '20rem' }}
                      >
                        <div className="card-header">
                          <Link to={`/post/${post.id}`}>
                            <img className="card-img-top" src={post.image} alt="Card image cap" />
                          </Link>
                        </div>
                        <div className="card-body text-white">
                          <h4 className="card-title" style={{ color: 'white' }}>
                            {post.title.length > 17 ? post.title.substr(0, 17) + '...' : post.title}
                          </h4>
                          <p className="card-text">
                            {post.text.length > 30 ? post.text.substr(0, 30) + '...' : post.text}
                          </p>
                          <span>
                            ~{' '}
                            <Link to={`/profile/${post.profile.id}`} style={{ color: 'white' }}>
                              <strong>{post.profile.username || 'Anonymous'}</strong>
                            </Link>
                          </span>
                        </div>
                        <div className="card-footer">
                          <small className="text-muted">
                            {DateUtils.relativeTime(post.timestamp)}
                          </small>
                        </div>
                      </div>
                    );
                  })}
            </div>
          </div>
          <div className="col-sm-4">
            <div className="row">
              <div className="col-sm-12">
                <Account />
              </div>
            </div>
            {currentUser == null ? null : (
              <div className="row" style={{ marginTop: '25px' }}>
                <div className="row">
                  <div className="col-sm-12">
                    <h3>Create a Post</h3>
                  </div>
                </div>
                <div className="row">
                  <div className="col-sm-12">
                    <CreatePost onCreate={this.createPost.bind(this)} />
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }
}

const stateToProps = state => {
  return {
    post: state.post,
    user: state.user,
    reply: state.reply
  };
};

const dispatchToProps = dispatch => {
  return {
    createPost: params => dispatch(actions.createPost(params)),
    fetchPosts: params => dispatch(actions.fetchPosts(params)),
    getReplies: params => dispatch(actions.getReplies(params))
  };
};

const loadData = store => {
  return store.dispatch(actions.fetchPosts());
};

export default {
  loadData: loadData,
  component: connect(stateToProps, dispatchToProps)(Posts)
};

CLIENT/SERVER ROUTES:

import React from 'react';

import { Post, Posts, Profile, NotFound } from './components/containers';
import App from './components/App';

export default [
  {
    ...App,
    routes: [
      {
        ...Posts,
        path: '/',
        exact: true
      },
      {
        ...Post,
        path: '/post/:id'
      },
      {
        ...Profile,
        path: '/profile/:id'
      },
      {
        ...NotFound
      }
    ]
  }
];

Upvotes: 2

Views: 1938

Answers (1)

nem035
nem035

Reputation: 35491

My best guess would be that, within Posts.js, this code:

<div className="col-sm-12">
  <Account />
</div>

should be:

<div className="col-sm-12">
  <Account.component />
</div>

Or you could extract the component part at a higher level somewhere.

I'm basing this from the fact that Account is imported from containers, and other parts of your code expect exported containers to be of the form { component, loadData }.

If not, the other culprit could be CreatePost.

Upvotes: 1

Related Questions