JavaScript default exports aren’t always fun, or how I wasted 4 hours

by
Tags: , ,
Category:

This is one of those blog posts I write to save others from the pain I suffer…

Recap: named exports

In ECMAScript JavaScript module syntax (import / export), exported elements can either be named, or you can define a single default export.

Named exports are just what they sound like – you export them by name, and can have any number of exports in the same file:

// my-stuff.js
export const One = { a: 1, b: 2 };
export const Two = "hi there";

// using my-stuff in another file:
import { One, Two } from './my-stuff';
// One = { a: 1, b: 2}
// Two = "hi there";

Default exports: React loves these

Now, we can also have default exports. These are similar to the old node-style module.exports syntax, essentially in that you can only export one thing. That thing can be a complex object or just a simple primitive. But you get only ONE.

// demo-component.js
export default function FooBar (props) {
  return (
    <>I am Foobar!</>
  );
}

// using demo-component.js
import FooBar from './demo-component';

// later in rendering:
return (
  <>
    <FooBar />
  </>
);

React developers typically (but not always) export components as the default… Keep that in mind. Also Next.js does this for data fetching – exporting the component as default, and the data fetching API methods as non-default exports.

Fun fact #1: The import of a default export can have any arbitrary name

Herein lies the beginning of my pain. A default export can be imported with any variable name:

// you can also do this:
import BarFoo from './demo-component';

// later in rendering:
return (
  <>
    <BarFoo />
  </>
);

So, it’s easy to make a mistake and misname the exported component something different. Technically this is not a bug, it is just sloppy coding.

Fun fact #2: There is no export default const

While I have a soapbox, might I complain about having to do this everywhere I want to export a default that isn’t a straightforward function?

const MyComponent = ({ propA, propB }) => {
  return >p<I am a component</>;
};

export default MyComponent;

n.b. – the way to do it if you can is to just use a plain function…

export default function MyComponent({ propA, propB }) {
  return >p<I am a component</>;
}

JavaScript is so picky and odd. But I digress…

Factoid #2: React can’t render plain objects

What if you accidentally tried to import what you thought was a named export but left off the braces? In preparing a demo for an upcoming talk, I made this mistake using the Vercel SWR library.

import SWRConfig from 'swr';

export default function MyTemplateComponent ({ Component, pageProps }) {
  return (
    <>
      <SWRConfig value={{ configuration settings here}}>
      <Component ...pageProps />
    </>
  );
}

Did you spot the bug? Come on, Ken, SWRConfig is NOT the default export of swr.

Next.js gave me a rather confusing stack trace about this:

react-dom.development.js?3c4a:13231 Uncaught Error: 
  Objects are not valid as a React child (found: 
  object with keys {mutate, data, error, isValidating}). 
  If you meant to render a collection of children, use an array instead.
  (stack trace with no actual identifiable lines of code in it, just framework APIs)

It’s not too confusing now that I figured out what I was doing wrong, but I had thought I imported the React component SWRConfig when in fact I imported the whole library as SWRConfig.

Chasing my tail

I spent the next four hours trying to debug this component, which used the properties referenced in the API (mutate, data, error, isValidating):

// cut out the stuff that's irrelevant to the issue
import useSWR from 'swr';
import { fetcher } from '../../utils';
import PodcastEpisode from '../podcast-episode/podcast-episode';
import FeedItem from '@powerplayer/shared/lib/domains/feed_item';

const PodcastEpisodeList = () => {
    // SWR hook to consume RSS feed from Node server
    const {data, error, isValidating} = useSWR(
      `http://localhost:3010/podcasts/feed?...`);
    const episodeComponents = (data) ? data
      .map((e:FeedItem) => <PodcastEpisode key = {e.guid} episode = {e} />) : null;

    return (
      <>
          // lots of other stuff
          <div className={styles.container}>
            {!isValidating && episodeComponents}
            {isValidating && <Spinner />}
          </div>
      </>
    );
}

export default PodcastEpisodeList;

Mainly I focused on the useSWR hook, because it used those three variables that were referenced in the stack. I removed the entire API call, and still I got the same error. But useSWR IS the default export of the swr module, so that code was fine.

In the end, the problem was simply my mis-import of the SWRConfig in my _app.js Next.JS page layout template component.

// the fix - use a destructuring import 
import { SWRConfig } from 'swr';

export default function MyTemplateComponent ({ Component, pageProps }) {
  return (
    <>
      <SWRConfig value={{ configuration settings here}}>
        <Component ...pageProps />
      </SWRConfig>
    </>
  );
}

Normally React components are default exports, and other things such as PropTypes and functions / hooks are exported as named exports. But in this library, the hook was the default component and the component was a non-default export.

Lessons learned

I learned the following lessons:

  • Unlike many other React libraries, SWR exports a hook as a default export (useSWR) and a component as a named export. This is a tad confusing.
  • I will run into this again. I did it a few times in the past and it caused me pain
  • The problem was not surfaced by a linter check or a compiler, because JS doesn’t care what you call your import with default objects
  • As a library creator, I will try to make sure to stick with the components-as-default-exports mantra wherever possible. The SWR team didn’t do that in this case.
  • I was using Next.js – it’s got a page templating system, and this error lived there. So every page died because I was trying to use a higher-order component to configure SWR, but in fact, it was just the whole exported module…

So tuck this away in your head whenever you get a strange “Objects are not valid as a React child” to make sure you’ve actually imported a true React component, and not the wrong object.

Happy coding!