Server Side Rendering with React and Apollo

For some days now, my app is breaking. I first get
Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports. - Error, followed by:
Uncaught TypeError: Cannot read property 'suppressHydrationWarning' of null

Here is my ssr:

import React from 'react';
import { ApolloClient, ApolloProvider, createHttpLink } from '@apollo/client';
import { renderToStringWithData, getDataFromTree } from '@apollo/client/react/ssr';
import 'isomorphic-fetch';
import { Meteor } from 'meteor/meteor';
import { onPageLoad, Sink } from 'meteor/server-render';
import { Helmet } from 'react-helmet';
import { StaticRouter } from 'react-router';

import checkIfBlacklisted from '@/modules/server/checkIfBlacklisted';
import InMCache from '@/modules/cache';
import App from '@/ui/layouts/App';

onPageLoad(async (sink: Sink) => {

  const apolloClient = new ApolloClient({
    ssrMode: true,
    link: createHttpLink({
      uri: Meteor.settings.public.graphQL.httpUri,
    }),
    cache: InMCache, // : new InMemoryCache(),
  });

  const app = (
    <ApolloProvider client={apolloClient}>
      <StaticRouter location={sink.request?.url} context={{}}>
        <App />
      </StaticRouter>
    </ApolloProvider>
  );

  const content = await renderToStringWithData(app);
  const initialState = apolloClient.extract();
  const helmet = Helmet.renderStatic();

  sink.appendToHead(helmet.meta.toString());
  sink.appendToHead(helmet.title.toString());
  sink.renderIntoElementById('react-root', content);
  sink.appendToBody(`
    <script>
      window.__APOLLO_STATE__ = ${JSON.stringify(initialState).replace(/</g, '\\u003c')}
    </script>
  `);
});

Client:

import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { hydrate, render } from 'react-dom';
import { BrowserRouter, Switch } from 'react-router-dom';

import '@/modules/whiteList';

import App from '@/ui/layouts/App';
import apolloClient from './apollo';

Accounts.onLogout(() => apolloClient.resetStore());
Meteor.startup(() => {
  const target = document.getElementById('react-root');

  const app = (
    <ApolloProvider client={apolloClient}>
      <BrowserRouter>
        <Switch>
          <App />
        </Switch>
      </BrowserRouter>
    </ApolloProvider>
  );

  return !window.noSSR ? hydrate(app, target) : render(app, target);

I am still wondering what has changed as the app used to work just fine. I never got this error befor.

This a common React error, you must have forgot to export properly one of your components.
It may have worked before, because the component was exported correctly and saved in your app or browser cache.

Since the export is an object, I think you’re trying to import a named export as a default export

your doing : import Component from '...' instead of import { Component } from '...'

1 Like

Also react-helmet won’t work properly if you’re using GQL data, you should move to react-helmet-async. Check out our React/Apollo SSR boilerplate here : GitHub - Gorbus/meteor-react-apollo-ssr

2 Likes

@nschwarz thanks for your reply. I went almost through all my import if not the most important. There since to be no problem there. But here comes the miracle or I may be lacking some typescript knowledge. Inside my app component, there is a Navigation component:

// ui/components/Navigation/index.tsx
interface NavProps { ... };
const Navigation: FC<NavProps> = (props: NavProps) => (<>SOME LOGIC + render <>);
export defaut Navigation

This is where the problem since to occur. When in do import Navigation from '@/ui/components/Navigation the type of the component is an object. No matter what I do, it remains an object. But when I change the type of the file to ìndex.js(of course with a code refactoring) everything is working fine. The type is now a function. My app was implemented mostly withjsat the biginning. I then decided to switch totypescript` for dynamic type-checking. Could it be that the typescript compiler is somehow the reason for this?

Did you make a config file ? see Build System | Meteor Guide

Also your import 'isomorphic-fetch' is unused, and you should do import fetch from 'isomorphic-fetch'

Yes:

{
  "compilerOptions": {
      "target": "ES2018",
      "module": "ESNext",
      "lib": ["ESNext", "DOM"],
      "allowJs": true,
      "checkJs": false,
      "jsx": "react",
      "incremental": true,
      "sourceMap": true,
      "noEmit": true,

      /* Strict Type-Checking Options */
      "strict": true,
      "noImplicitAny": true,
      "strictNullChecks": true,
      "noImplicitThis": true,
      "strictBindCallApply": true,
      "strictPropertyInitialization": true,
      "strictFunctionTypes":true,

      /* Module Resolution Options */
      "baseUrl": ".",
      "paths": {
          "@/*": ["*"],/* Support absolute /import/ with a leasing '/' */
          "@api/*":["./api/*"],
          "@client/*":["./client/*"],
          "@intl/*":["./intl/*"],
          "@module/*":["./module/*"],
          "@startup/*":["./startup/*"],
          "@public/*":["./public/*"],
          "@private/*":["./private/*"],
          "@ui/*":["./ui/*"],
      },
      "resolveJsonModule": true,
      "preserveSymlinks": true,
      "experimentalDecorators": true,
      "moduleResolution": "Node",
      "esModuleInterop": true,
      // "typeRoots": ["./node_modules/@types", "./modules/typings"],
      "types": [
          "@types/node",
          // "mocha",
          "@types/csvtojson",
          "@types/faker",
          "@types/json2csv",
          "@types/meteor",
          "@types/meteor-roles",
          "@types/pdfmake",
          "@types/react-big-calendar",
          "@types/react-csv",
          "@types/react-helmet",
          "@types/simpl-schema",
          "@types/react-dom",
          "@types/react-router-dom",
          "@types/react-color",
          "@types/graphql-type-json",
          "@types/classnames",
          "@types/commonmark",
          "@types/xml",
          // "modules/typings/@types/gql.d.ts",
          // "modules/typings/meteorDefs.d.ts"
      ],
      /* Checks */
      "noUnusedLocals": true,
      "noUnusedParameters": true,
      "noImplicitReturns": false,
      "noFallthroughCasesInSwitch": false,

      "allowSyntheticDefaultImports": true,
  },
  // "files": ["./modules/typings/@types/gql.d.ts", "./modules/typings/meteorDefs.d.ts"],
  // "include": ["./**/*"],
  "exclude": ["./node_modules/**", "./.meteor/**"]
}

Great boilerplate :+1:

I could ping-pong the error to SSR. The main problem is that I am using meteor/react-meteor-data in my app component to handle subscriptions. Now, subscriptions are not available to the server. So on the server, I just returned {}. This is where the problem is coming from. Also, note that this used to work before when I was using js file. So the props on the Navigation component is empty, causing the error.

const App = (props) => { <Navigation {...props}> ... </Navigation> }
export default withTrackerSSR(() => {
  const app = Meteor.subscribe('app');
  const loading = !app.ready()
  ...
  return {
    loading,
    ...
  }
})(App);


const withTrackerSSR = (container: any) => (Component: any) =>
  withTracker((props) => {
    if (Meteor.isClient) return container(props);
    return {};
  })(Component);

I am still trying to find out how to solve this meteor specific problem.

On the server you should pull the data directly from the db instead of using a subscription.

This is quite complex to be done. First, I use the roles package wish requires a subscription to the client. Second, when using normal .js, it is working. things change only when the file has a .tsx extension.

const root = Meteor.subscribe('root');
const loggingIn = Meteor.loggingIn();
const user = Meteor.user();
const userId = Meteor.userId();
const loading = !app.ready() && !Roles.subscription.ready();

All those are client only. I could still get the user from the server. But what about the rest?

You should read the source code of roles and pull the same data directly.

I could solve that one.
But still don’t get why when importing react-component with .tsx - extension I keep getting:

React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
// ui/components/Navigation/index.tsx
interface INavProps { ... };
const Navigation: FC<NavProps> = (props: NavProps) => (<>SOME LOGIC + render <>);
export defaut Navigation

// ui/layout/index.tsx
import Navigation from '@/ui/components/Navigation';
export interface IAppProps { ... }

const App:FC<IAppProps> = (props: IAppProps) => {
  ...
  return ( <Navigation {...props} />)
}

When I console.log the Navigation-component it’s returning {} of type object. So for some reasons, imports are not returning the right type. This is quite unusual. Everything works fine when using .js-extension.

I finally could solve this. The issue was linked to .less files. In my Navigation component folder there where: ìndex.tsx & index.less so it looks like the imports were resolving to the ìndex.less. Simply changing index.less to navigation.less solves this issue.
I could not imagine just for a single moment that having two files with the same name but different èxtension` could cause such a problem.