I've been going through react-router's tutorial, and I've been following it to the letter as far as I'm aware. I'm having some issues with the url params in loaders segment.
The static contact code looks like this
export default function Contact() {
const contact = {
first: "Your",
last: "Name",
avatar: "https://placekitten.com/g/200/200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
}
And when it loads, it looks like this. That works just fine, however, the tutorial then tells me to change that code so that I use data that's loaded in instead. The code now looks like this
import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts"
export async function loader({ params }) {
const contact = await getContact(params.contactid);
return {contact}
}
export default function Contact() {
const { contact } = useLoaderData();
According to the tutorial, it should just load in an empty contact that looks like this but instead, every time I try to open one of the new contacts, it kicks up an error saying
React Router caught the following error during render TypeError: contact is null
The actual line of code this error points to is in the return segment of the contact component, which looks like this
return (
<div id="contact">
<div>
<img
key={contact.avatar}
src={contact.avatar || null}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a
target="_blank"
href={`https://twitter.com/${contact.twitter}`}
>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
Pretty much anywhere contacts is called gets an error. So, anyone have any idea what I'm doing wrong here? To my knowledge, I've been following their guide to the letter and it seems like it should be able to handle contacts not having any data, but it's not.
These are the pieces of my code that are supposed to be working together to render a contact, or at least the pertinent parts
The router, this is the main file, the only part missing is the part where it's rendered
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root, { loader as rootLoader, action as rootAction } from "./routes/root";
import ErrorPage from "./error-page";
import Contact, { loader as contactLoader } from "./routes/contact"
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactID",
element: <Contact />,
loader: contactLoader
}
]
}
These are the functions in the root file that are called when a new contact is made and when it needs to be displayed
import { Outlet, Link, useLoaderData, Form } from "react-router-dom"
import { getContacts, createContact } from "../contacts"
export async function action() {
const contact = await createContact();
console.log("Contact made")
return {contact}
}
export async function loader(){
const contacts = await getContacts();
return {contacts};
}
This is the createContacts
function that gets called when a contact is created, and this is the getContacts
function
export async function createContact() {
await fakeNetwork();
let id = Math.random().toString(36).substring(2, 9);
let contact = { id, createdAt: Date.now() };
let contacts = await getContacts();
contacts.unshift(contact);
await set(contacts);
return contact;
}
export async function getContact(id) {
await fakeNetwork(`contact:${id}`);
let contacts = await localforage.getItem("contacts");
let contact = contacts.find(contact => contact.id === id);
return contact ?? null;
}
This is the contacts.jsx
file where things are currently going wrong. When a new contact is made, it's going to be empty, which I imagine is the source of the problem, but there are checks here to deal with that, or at least there are supposed to be.
import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts"
export async function loader({ params }) {
const contact = await getContact(params.contactid);
return { contact }
}
export default function Contact() {
const { contact } = useLoaderData();
return (
<div id="contact">
<div>
<img
// these next two lines are where the errors typically start,
// although it seems to extend down to any instance where contact
// gets called.
key={contact.avatar}
src={contact.avatar || null}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a
target="_blank"
href={`https://twitter.com/${contact.twitter}`}
>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
There are some subtle, but detrimental, casing issues in the route path params.
The Contacts
component's route path param is declared as contactID
.
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactID", // <-- "contactID"
element: <Contact />,
loader: contactLoader,
},
],
},
]);
The contact loader is referencing a contactid
path parameter.
export async function loader({ params }) {
const contact = await getContact(params.contactid); // <-- "contactid"
return { contact };
}
As such, the loader function is unable to find a match and returns null
to the Contact
component. An error is thrown in the UI when attempting to access properties of the null reference.
Any valid Javascript identifier will work as the name of the route path parameter, but they should all be in agreement. Casing matters in variable names in Javascript. The common convention in variable names is to use camelCasing, e.g. contactId
.
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
],
},
]);
export async function loader({ params }) {
const contact = await getContact(params.contactId);
return { contact };
}