React Component Not Rendering: 7 Causes and the Fix for Each

A React component not rendering is almost never a React bug. In the great majority of cases it is one of a handful of ordinary mistakes: the tag starts with a lower-case letter, the function forgot its return, the import does not match the export, && put a 0 on the page instead of your component, the component is never mounted, CSS collapsed it to nothing, or something threw and took the tree with it. Each cause below is reproduced in a real browser, so you can match what you see on your screen against a screenshot and a console line, then jump straight to the fix.

Every example here was built with React 19.3.0 in a Vite project and rendered in a real browser; the screenshots and console output are that run. Reference: Your First Component on react.dev.

Start with the symptom, not with the code

The fastest route to the cause is the shape of the failure. Find your row first:

What you seeConsoleUsually meansGo to
A gap where the component should beSilentLower-case tag name, or no returnCause 1 and Cause 2
A bare 0 or NaNSilent&& with a number on the leftCause 4
The whole page is blankElement type is invalidImport / export mismatchCause 3
The whole page is blankA red error from your own codeA throw during renderCause 7
Nothing at all, anywhereSilentThe component is never mountedCause 5
Visible in DevTools, not on screenSilentCSS: zero height, hidden, off-screenCause 6
It appears, but never updatesSilentA state problem, not a render problemthe re-rendering guide

That last row matters. This guide is about a component that never appears at all. If yours appears once and then ignores your updates, the cause is a different family of mistakes, and it is covered in React component not re-rendering after a state update.

Cause 1: the component name starts with a lower-case letter

This is the one that catches the most people, because nothing goes wrong. JSX decides what a tag means from its first character: capitalised means “the variable with this name”, lower-case means “an HTML tag with this name”. So <userCard /> never calls your function. React builds a DOM element called usercard, the browser accepts it as an unknown element, and you get an empty space and a clean console:

import { useEffect } from "react";

function userCard() {                     // lower-case name
  return <p style={{ margin: 0 }}>Ada Lovelace - ada@example.com</p>;
}

function UserCard() {                     // same component, capitalised
  return <p style={{ margin: 0 }}>Grace Hopper - grace@example.com</p>;
}

export default function App() {
  useEffect(() => {
    console.log(document.getElementById("slot").innerHTML);
  }, []);

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <h3 style={{ margin: "0 0 8px" }}>Team</h3>
      <div id="slot" style={{ border: "1px dashed #c8ced6", padding: 8 }}>
        <userCard />                      {/* JSX builds an HTML tag called "userCard" */}
        <UserCard />                      {/* JSX calls the component */}
      </div>
      <p style={{ margin: "8px 0 0", color: "#666" }}>
        One of those two lines rendered. There is no error in the console.
      </p>
    </div>
  );
}

The markup React actually produced:

<usercard></usercard>                      <p style="margin: 0px;">Grace Hopper - grace@example.com</p>
A React page where a lower-case component tag renders nothing while the capitalised version of the same component renders its text
Same component, two tag names: only the capitalised one ran.

The console line is the proof. <usercard></usercard> is a real element sitting in the page with nothing inside it. The fix is to capitalise the function and every tag that uses it. The same rule bites when you choose a component at runtime: const Tag = isLink ? Link : Button then <Tag /> works, while a lower-case const tag = ... used as <tag /> quietly produces an HTML element instead.

Cause 2: the function never returns anything

An arrow function with braces, or a regular function whose return was lost in an edit, builds the JSX and then throws it away. React receives undefined, which it treats as “render nothing” without complaining:

import { useEffect } from "react";

function Greeting({ name }) {
  <p>Hello, {name}!</p>;                  // built, then thrown away: the function returns undefined
}

function GreetingFixed({ name }) {
  return <p style={{ margin: 0 }}>Hello, {name}!</p>;
}

export default function App() {
  useEffect(() => {
    console.log("Greeting returned:", String(Greeting({ name: "Ada" })));
  }, []);

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <div style={{ border: "1px dashed #c8ced6", padding: 8, marginBottom: 8 }}>
        <Greeting name="Ada" />
      </div>
      <div style={{ border: "1px solid #bfe3c8", background: "#f4fff6", padding: 8 }}>
        <GreetingFixed name="Ada" />
      </div>
    </div>
  );
}

What the component handed back:

Greeting returned: undefined
Two React components side by side where the one without a return statement renders an empty box and the one with a return renders its greeting
Top box: no return. Bottom box: the identical component with one.

Two habits prevent it. Use the concise arrow form, const Greeting = ({ name }) => <p>Hello, {name}!</p>;, so there is no body to forget. And never start the JSX on the line below a bare return, because JavaScript inserts a semicolon after it and hands back undefined; put the opening bracket on the same line as return.

Cause 3: the import does not match the export

A file that exports ProfileCard by name has no default export, so a default import binds undefined, and React throws the moment it tries to use that as an element type. This is the error behind most “my whole app went blank” reports:

import { Component } from "react";
import * as CardModule from "./ProfileCard";

// ProfileCard.jsx exports ProfileCard by name only, so the file has no default export.
// This is exactly what `import ProfileCard from "./ProfileCard"` would bind: undefined.
const ProfileCard = CardModule.default;

class Boundary extends Component {
  state = { message: null };
  static getDerivedStateFromError(error) { return { message: error.message }; }
  componentDidCatch(error) { console.log(error.message.split("\n")[0]); }
  render() {
    if (!this.state.message) return this.props.children;
    return (
      <pre style={{ margin: 0, whiteSpace: "pre-wrap", fontSize: 13, color: "#8a1f1f",
                    background: "#fff4f4", border: "1px solid #f3c2c2", padding: 10 }}>
        {this.state.message}
      </pre>
    );
  }
}

export default function App() {
  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <Boundary>
        <ProfileCard name="Ada Lovelace" />
      </Boundary>
    </div>
  );
}

App.displayName = "App";   // the bundler mangles function names; this keeps React's message readable

The first line React logged:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
A React error boundary showing the element type is invalid error caused by a default import of a named export
React names the mistake in the message itself.

Read the second half of that message: it tells you to check the export in the child file, then match it. export default function ProfileCard pairs with import ProfileCard from "./ProfileCard", and export function ProfileCard pairs with import { ProfileCard } from "./ProfileCard". Modern bundlers often catch the mismatch before the browser does and fail the build instead, which is the friendlier version of the same problem.

Cause 4: the && operator printed a 0 instead of your component

&& does not return a boolean. It returns the left operand whenever that operand is falsy, 0 is falsy, and React renders the number 0 quite happily. So an empty list gives you a stray zero where the component should be. An empty string behaves the same way, only invisibly:

import { useState } from "react";

function Cart({ items }) {
  return <ul style={{ margin: "0 0 0 18px" }}>{items.map((i) => <li key={i}>{i}</li>)}</ul>;
}

export default function App() {
  const [items] = useState([]);           // the cart is empty

  return (
    <div style={{ fontFamily: "system-ui", padding: 16, display: "flex", gap: 32 }}>
      <div>
        <code>{"{items.length && <Cart />}"}</code>
        <div style={{ border: "1px solid #f3c2c2", background: "#fff4f4",
                      padding: 10, marginTop: 6, minWidth: 250 }}>
          {items.length && <Cart items={items} />}
        </div>
      </div>
      <div>
        <code>{"{items.length > 0 && <Cart />}"}</code>
        <div style={{ border: "1px solid #bfe3c8", background: "#f4fff6",
                      padding: 10, marginTop: 6, minWidth: 250 }}>
          {items.length > 0 && <Cart items={items} />}
        </div>
      </div>
    </div>
  );
}
Two React panels showing that items.length && Cart renders a zero while items.length greater than 0 && Cart renders nothing
Identical data. The left guard leaks a 0; the right one renders nothing.

Make the left side a real boolean: items.length > 0 &&, Boolean(count) &&, or a ternary when you want a fallback for the empty case. The same care pays off whenever a parent renders whatever it is handed, which is covered in React components with children.

Cause 5: the component is never mounted

It sounds too obvious to check, and it is the second most common cause after the lower-case tag. The component exists, the file is saved, the editor even autocompletes it, and nothing ever renders it. Three variants, in the order worth checking them:

  • The component is defined and exported but never used in any parent’s JSX. Search the project for its name: one hit means the definition is the only hit.
  • It is rendered inside a branch that is never taken, for example under a route you never visit, or behind a flag that is always false.
  • It is mounted into a root that is not on screen. A typo in createRoot(document.getElementById("root")) throws immediately, but a second root rendered into a detached node fails silently.

React DevTools settles it in seconds. Open the Components tab and search for the name: if it is not in the tree it was never mounted, and no amount of editing the component itself will help.

Cause 6: it rendered, you just cannot see it

When DevTools shows the component but the screen does not, the render succeeded and CSS is hiding the result. Measure it rather than guessing:

import { useEffect, useRef } from "react";

function Sidebar() {
  return <p style={{ margin: 0 }}>Filters, sorting and saved views live here.</p>;
}

export default function App() {
  const box = useRef(null);

  useEffect(() => {
    const el = box.current;
    console.log("child in the DOM:", !!el.firstChild,
                "| rendered height:", el.getBoundingClientRect().height);
  }, []);

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <h3 style={{ margin: "0 0 8px" }}>Reports</h3>
      <div ref={box} style={{ height: 0, overflow: "hidden" }}>
        <Sidebar />
      </div>
      <p style={{ margin: "8px 0 0", color: "#666" }}>
        The sidebar is in the page. Its wrapper is zero pixels tall.
      </p>
    </div>
  );
}

Measured after the first paint:

child in the DOM: true | rendered height: 0