useLoaderData undefined error with Sanity query in Remix loader

16 replies
Last updated: Oct 11, 2022
Not sure what's going on here, because the same code is working on another page but I am currently getting this error:

TypeError: Cannot destructure property 'posts' of '(0 , import_react27.useLoaderData)(...)' as it is undefined.
For this code:


import { useLoaderData } from "@remix-run/react";
import { Link } from "@remix-run/react";
import moment from "moment";
import { getClient } from "~/lib/sanity/getClient";
import { urlFor } from "~/lib/sanity/sanity";

// loader() must be async!
export async function loader() {
  const posts = await getClient().fetch(
    `*[_type == "post"]{ 
      _id, 
      title, 
      slug, 
      author,
      "authorName": author->name,
      "authorImage": author->image,
      mainImage, 
      publishedAt, 
      categories,
      excerpt, 
      myTags, 
      featuredPost,
      "categoryName": *[_type == 'category' && _id in ^.categories[]._ref]{title, slug},
    } | order(publishedAt desc) `
  );

  return { posts };
}

export default function LatestBlogPosts() {
  let { posts } = useLoaderData();

return (
    <section className="">
      <div className="relative pt-16 pb-20 px-4 sm:px-6 lg:pt-24 lg:pb-28 lg:px-8">
        <div className="absolute inset-0">
          <div className="h-1/3 sm:h-2/3" />
        </div>
        <div className="relative mx-auto">
          <div className="text-left">
            <h2 className="text-7xl tracking-tight font-black text-richBlack">
              ALL POSTS
            </h2>
          </div>
          <div className="mt-12 grid gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 lg:max-w-none">
            {posts?.length > 1
              ? posts.slice(0, 12).map((post: any) => (
Any help?
AI Update

This is a classic Remix loader issue where useLoaderData() returns undefined. Since you mentioned the same code works on another page, here are the most likely causes:

1. Loader Not Being Called (Most Common)

The most common reason is that your route file isn't properly exporting the loader or the file naming doesn't match Remix's routing conventions. Check:

  • Is your file in the app/routes/ directory?
  • Is the loader export actually being recognized? Try adding a console.log at the very top of your loader to verify it's running:
export async function loader() {
  console.log("LOADER EXECUTING"); // Check server logs
  const posts = await getClient().fetch(/* ... */);
  console.log("Posts count:", posts?.length);
  return { posts };
}

If you don't see "LOADER EXECUTING" in your server console when you visit the page, the loader isn't running at all.

2. Client-Side Navigation Issue

If you're navigating to this page from another page, Remix might be trying to call the loader but getting an error that's swallowed. Add error handling:

export async function loader() {
  try {
    const posts = await getClient().fetch(
      `*[_type == "post"]{ 
        _id, 
        title, 
        slug, 
        author,
        "authorName": author->name,
        "authorImage": author->image,
        mainImage, 
        publishedAt, 
        categories,
        excerpt, 
        myTags, 
        featuredPost,
        "categoryName": *[_type == 'category' && _id in ^.categories[]._ref]{title, slug},
      } | order(publishedAt desc)`
    );
    
    return { posts: posts || [] };
  } catch (error) {
    console.error("Sanity fetch error:", error);
    throw new Response("Failed to load posts", { status: 500 });
  }
}

3. Sanity Client Configuration

Since it works elsewhere, this is less likely, but verify your getClient() function returns a properly configured client:

import { createClient } from "@sanity/client";

export function getClient() {
  return createClient({
    projectId: process.env.SANITY_PROJECT_ID, // or your hardcoded ID
    dataset: process.env.SANITY_DATASET || "production",
    apiVersion: "2024-01-01",
    useCdn: false, // Important for loaders
  });
}

4. Defensive Component Code

As a quick fix while debugging, make your component more defensive:

export default function LatestBlogPosts() {
  const data = useLoaderData();
  const posts = data?.posts || [];

  if (!posts || posts.length === 0) {
    return <div>No posts found</div>;
  }

  return (
    <section className="">
      {/* your existing JSX */}
    </section>
  );
}

5. Check for TypeScript Errors

If you're using TypeScript, make sure there are no type errors preventing the loader from compiling. Run npm run typecheck to verify.

Debugging Steps

  1. Check your server console logs (not browser console) when you load the page
  2. Verify the route file is in the correct location and named correctly
  3. Try hard-refreshing the page (Ctrl+Shift+R) instead of navigating to it
  4. Check if you have an ErrorBoundary export in the same file that might be catching errors silently

The fact that identical code works on another page strongly suggests either a route configuration issue or the loader isn't being executed at all on this specific route. The console.log approach will quickly tell you which it is.

Show original thread
16 replies
The strange thing about this is that I'm copying it from another part of the website where it works just fine, only changing a few things here...
This is another error it's showing me regarding the same issue

TypeError: Cannot destructure property 'posts' of '(0 , import_react27.useLoaderData)(...)' as it is undefined.
It looks like everything should work. Do you want to post the code from the other file that’s working?
Sure! Here you go:
import { useLoaderData } from "@remix-run/react";
import { Link } from "@remix-run/react";
import moment from "moment";
import NavbarIndex from "~/components/navigation-index";
import { CgArrowLongLeft, CgArrowLongRight } from "react-icons/cg";

import { getClient } from "~/lib/sanity/getClient";

import { urlFor } from "~/lib/sanity/sanity";
import { ReactChild, ReactFragment, ReactPortal } from "react";

// loader() must be async!
export async function loader() {
  const posts = await getClient().fetch(
    `*[_type == "post"]{ 
      _id, 
      title, 
      slug, 
      author,
      "authorName": author->name,
      "authorImage": author->image,
      mainImage, 
      publishedAt, 
      categories,
      excerpt, 
      myTags, 
      featuredPost,
      "categoryName": *[_type == 'category' && _id in ^.categories[]._ref]{title, slug},
    } | order(publishedAt desc) `
  );

  return { posts };
}

export default function BlogIndex() {
  let { posts } = useLoaderData();
  const dateFormatter = new Intl.DateTimeFormat("en-GB");

  return (
    <main className="">
      <NavbarIndex />
      <div className="relative pt-16 pb-20 px-4 sm:px-6 lg:pt-24 lg:pb-28 lg:px-8">
        <div className="absolute inset-0">
          <div className="h-1/3 sm:h-2/3" />
        </div>
        <div className="relative mx-auto">
          <div className="text-left">
            <h2 className="text-7xl tracking-tight font-black text-richBlack">
              ALL POSTS
            </h2>
          </div>
          <div className="mt-12 grid gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 lg:max-w-none">
            {posts?.length > 1
              ? posts.slice(0, 12).map((post: any) => (
Could you
console.log(posts)
in your loader function before the return to make sure the fetch is working?
);
 console.log(posts)
  return { posts };
}
here?
Yes. Then when you visit that page, it should log something to your terminal (where you ran
npm run dev
or its equivalent).
Is the working file in
routes
and the other in
components
?
Yes!
Is it fixed once you move it to routes?
No, same error
Should I just start this component over from scratch? I feel like it should absolutely be working
The one that is working is basically the page to show ALL the blog posts, this component is to show only 3 on the index page
Proof that it's returning data on the page that's working:
I had to export the loader on the index route
this works now

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?