I need to render component by route "courses/:courseId"
, but I got an error "Uncaught TypeError: Cannot read properties of undefined". It's because courses
is empty just after render.
import React, { useEffect, useState } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { pipeDuration } from '../../helpers/pipeDuration';
import { transformDate } from '../../helpers/dateGenerator';
import { selectCourses, selectAuthors } from './selectors';
import { BACK_TO_COURSES } from '../../constants';
import classes from './CourseInfo.module.css';
import { getCourses } from '../../store/courses/thunk';
import { getAuthors } from '../../store/authors/thunk';
const CourseInfo = () => {
useEffect(() => {
dispatch(getAuthors());
dispatch(getCourses());
}, []);
const dispatch = useDispatch();
const { courseId } = useParams();
const courses = useSelector(selectCourses);
const authors = useSelector(selectAuthors);
const course = courses.find((course) => course.id === courseId);
const createdDate = transformDate(course.creationDate);
const duration = pipeDuration(course.duration);
const courseAuthors = course.authors.map((authorId) => {
return authors.find(({ id }) => id === authorId);
});
const courseAuthorsList = courseAuthors.map((author, index, array) => {
return index + 1 === array.length ? author.name : author.name + ', ';
});
return (
<div className='container'>
<div className={classes.wrapper}>
<Link to='/courses'>{BACK_TO_COURSES}</Link>
<h2 className={classes.title}>{course.title}</h2>
<div className={classes.info}>
<p className={classes.description}>{course.description}</p>
<div className={classes.details}>
<div><strong>ID: </strong>{courseId}</div>
<div><strong>Duration: </strong>{duration} hours</div>
<div><strong>Created: </strong>{createdDate}</div>
<div><strong>Authors: </strong>
<div className={classes.authorsWrapper}>
{courseAuthorsList}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default CourseInfo;
How can I initialize courses
const before render?
The issue here is that the CourseInfo
component isn't being very defensive about protecting itself against potentially undefined values. Assuming the selected courses
state is an array you should keep in mind that Array.prototype.find
returns undefined
when no array element is matched back the predicate callback function.
Return value
The first element in the array that satisfies the provided testing function. Otherwise,
undefined
is returned.
The useEffect
hook runs after the component is rendered to the DOM during the "commit phase".
Your current code:
const CourseInfo = () => {
useEffect(() => {
dispatch(getAuthors());
dispatch(getCourses());
}, []);
const dispatch = useDispatch();
const { courseId } = useParams();
const courses = useSelector(selectCourses);
const authors = useSelector(selectAuthors);
const course = courses.find((course) => course.id === courseId); // <-- potentially undefined
const createdDate = transformDate(course.creationDate); // <-- potential undefined access
const duration = pipeDuration(course.duration); // <-- potential undefined access
const courseAuthors = course.authors.map((authorId) => { // <-- potential undefined access
return authors.find(({ id }) => id === authorId);
});
const courseAuthorsList = courseAuthors.map((author, index, array) => {
return index + 1 === array.length ? author.name : author.name + ', ';
});
return (
<div className='container'>
<div className={classes.wrapper}>
<Link to='/courses'>{BACK_TO_COURSES}</Link>
<h2 className={classes.title}>
{course.title} // <-- potential undefined access
</h2>
<div className={classes.info}>
<p className={classes.description}>
{course.description} // <-- potential undefined access
</p>
<div className={classes.details}>
<div><strong>ID: </strong>{courseId}</div>
<div><strong>Duration: </strong>{duration} hours</div>
<div><strong>Created: </strong>{createdDate}</div>
<div><strong>Authors: </strong>
<div className={classes.authorsWrapper}>
{courseAuthorsList}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
A simple solution would be to check for a defined courses
state value prior to attempting to access into the object.
const CourseInfo = () => {
useEffect(() => {
dispatch(getAuthors());
dispatch(getCourses());
}, []);
const dispatch = useDispatch();
const { courseId } = useParams();
const courses = useSelector(selectCourses);
const authors = useSelector(selectAuthors);
const course = courses.find((course) => course.id === courseId);
// Check for falsey course value
if (!course) {
return <div>No courses found.</div>;
}
const createdDate = transformDate(course.creationDate);
const duration = pipeDuration(course.duration);
// Guard against undefined course.authors and author arrays
const courseAuthors = (course.authors || [])
// Map found author objects
.map((authorId) => (authors || []).find(({ id }) => id === authorId))
// Filter any undefined "holes" for unknown authors
.filter(Boolean)
// Map to author's name property
.map((author) => author.name)
// Join into comma-separated list string
.join(", ");
return (
<div className='container'>
<div className={classes.wrapper}>
<Link to='/courses'>{BACK_TO_COURSES}</Link>
<h2 className={classes.title}>
{course.title}
</h2>
<div className={classes.info}>
<p className={classes.description}>
{course.description}
</p>
<div className={classes.details}>
<div><strong>ID: </strong>{courseId}</div>
<div><strong>Duration: </strong>{duration} hours</div>
<div><strong>Created: </strong>{createdDate}</div>
<div><strong>Authors: </strong>
<div className={classes.authorsWrapper}>
{courseAuthors}
</div>
</div>
</div>
</div>
</div>
</div>
);
};