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!