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!