How to get data from store before render in React-redux?

313 views Asked by At

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?

1

There are 1 answers

0
Drew Reese On BEST ANSWER

Issue

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>
  );
};

Solution

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>
  );
};