javascriptreactjsreact-routerpreactpreact-router

Preact error: "objects are not valid as a child. Encountered an object with the keys {}" when using async await in root component


I'm using Preact for the first time.

I simply created a new project with preact-cli and this default template: https://github.com/preactjs-templates/default.

In app.js I'm trying to use this code:

import { Router } from 'preact-router';

import Header from './header';
import Home from '../routes/home';
import Profile from '../routes/profile';

// I added this function
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const App = async () => { // I added "async" and the "{" in this line
  await sleep(3000) // I added this line

  return ( // I added this line
    <div id="app">
      <Header />
      <Router>
        <Home path="/" />
        <Profile path="/profile/" user="me" />
        <Profile path="/profile/:user" />
      </Router>
    </div>
  )
} // I added this line

export default App;

But unfortunately browser's gives me error:

Uncaught Error: Objects are not valid as a child. Encountered an object with the keys {}.

Why?

It works if I do not use async/await.


Solution

  • Disclaimer: I work on Preact.

    Our debug addon (preact/debug) will print this error whenever an invalid object is passed as a child that doesn't match the expected return type of h/createElement, usually called vnode:

    const invalidVNode = { foo: 123 };
    <div>{invalidVNode}</div>
    

    In your case your component function returns a Promise which is an object in JavaScript. When Preact renders that component the render function will NOT return a vnode, but a Promise instead. That's why the error occurs.

    Which poses the question:

    How to do async initialization?

    Once triggered, the render process in Preact is always synchronous. A component that returns a Promise breaks that contract. The reason it is that way is because you usually want to show at least something, like a spinner, to the user, while the asynchronous initialization is happening. A real world scenario for that would be fetching data via the network for example.

    import { useEffect } from "preact/hooks";
    
    const App = () => {
      // useEffect Hook is perfect for any sort of initialization code.
      // The second parameter is for checking when the effect should re-run.
      // We only want to initialize once when the component is created so we
      // pass an empty array so that nothing will be dirty checked.
      useEffect(() => {
        doSometThingAsyncHere()
      }, []);
    
      return (
        <div id="app">
          <Header />
          <Router>
            <Home path="/" />
            <Profile path="/profile/" user="me" />
            <Profile path="/profile/:user" />
          </Router>
        </div>
      )
    }