Home > Software engineering >  React Router: props aren't passed down when directly accessing the URL of child component. Will
React Router: props aren't passed down when directly accessing the URL of child component. Will

Time:02-16

I'm pretty new to React and React Router so apologies if this is a silly question or answered elsewhere - I did a search but couldn't find a similar question.

I'm using React Router v6 and React Hooks. My very basic full-stack app is focused on keeping track of musical artists and their released projects (albums, mixtapes, EPs, etc.). My App component will render the ProjectList component at the '/' route, and the ArtistList component at the '/artists' route.

Relevant portions of App.js:

import React, { useState, useEffect } from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import axios from 'axios';

import ProjectList from './ProjectList';
import ArtistList from './ArtistList';

import { Button } from './styles.js';

const App = () => {
  const [ projects, setProjects ] = useState([]);

  const getProjects = () => axios('projects').then(({ data }) => setProjects(data));
  useEffect(getProjects, []);

  return (<>
    <Link to="/">
      <Button>Projects</Button> // Button is using styled-components
    </Link>
    <Link to="/artists">
      <Button>Artists</Button>
    </Link>
    <Routes>
      <Route path="/" element={<ProjectList projects={projects} />} />
      <Route path="/artists" element={<ArtistList projects={projects} />} />
    </Routes>
  </>);
};

This all works as expected when I click the Projects and Artists buttons. However, when I attempt to access the '/artists' route directly through the URL bar of my browser, nothing renders and I get an error about how the projects prop is undefined. My assumption is that because I'm trying to access the ArtistList component before rendering the App component itself, getProjects doesn't run and so no projects props are passed down to ArtistList.

I need the list of projects to render my ArtistList component. Each element of projects is an object that includes an artist property that I use to create my list of artist names and a dateAdded property that I use to sort the artist list by recency. I also keep track of how many projects each artist has.

Relevant portions of ArtistList.jsx (the return block contains some styled-components - Options, Header, TextWrapper, and Artist):

const ArtistList = ({ projects }) => {
  const [ artists, setArtists ] = useState([]);
  const [ sortBy, setSortBy ] = useState('name');

  const getArtists = () => {
    const artists = [];
    let curArtist = projects[0].artist;
    let projectCount = 0;
    const now = new Date();
    let firstAdded = now;

    for (const { artist, dateAdded } of projects) {
      if (artist !== curArtist) {
        artists.push({ name: curArtist, projectCount, firstAdded });
        curArtist = artist;
        projectCount = 0;
        firstAdded = now;
      }
      projectCount  ;
      const projectDate = new Date(dateAdded);
      if (projectDate < firstAdded)
        firstAdded = projectDate;
    }
    setArtists(artists);
  };
  useEffect(getArtists, [ projects, sortBy ]);

  if (sortBy === 'number')
    artists.sort((a, b) => b.projectCount - a.projectCount);
  else if (sortBy === 'recency')
    artists.sort((a, b) => b.firstAdded - a.firstAdded);

  return (<>
    <Options>
      <label htmlFor="sortBy">Sort by: </label>
      <select id="sortBy" value={sortBy} onChange={e => setSortBy(e.target.value)}>
        <option value="artist">Name</option>
        <option value="number"># of Projects</option>
        <option value="recency">Recently Added</option>
      </select>
    </Options>
    <Header>
      <TextWrapper>Name</TextWrapper>
      <TextWrapper># of Projects</TextWrapper>
    </Header>
    {artists.map(({ name, projectCount }, idx) => (
      <Artist key={idx}>
        <TextWrapper>{name}</TextWrapper>
        <TextWrapper>{projectCount}</TextWrapper>
      </Artist>
    ))}
  </>);
};

I currently see 2 solutions to this issue (I'm sure I'm missing more):

  1. Create a projects state and a getProjects in the ArtistList component, pretty much exactly as they are in the parent App component. Whenever I render ArtistList, I will make a request to my back end to retrieve a list of projects and then invoke setProjects. I'd then derive the artists state from projects, as before. Of course, I'd no longer need to pass the projects props from App to ArtistList. However, I would then have the exact same state values/functionality in both parent and child components, and this feels to me like a violation of fundamental React principles.

  2. Put a collection of Artists into my Mongo database, in addition to the Projects collection I currently have. I would have to implement something similar to the getArtists function, but in the back end this time, then put the list of artist data into my database. Then, every time I try to render ArtistList, I will make a request to my back end to retrieve the list of artists and go from there.

The third option is doing nothing, but then I'm unable to access the '/artists' page directly through the URL.

What would be best practice and the best approach in this instance? Any help is appreciated!

CodePudding user response:

The projects state is actually defined on the initial render cycle:

const [ projects, setProjects ] = useState([]);

So it's a defined prop when passed to ArtistList:

<Route path="/artists" element={<ArtistList projects={projects} />} />

The issue starts when in ArtistList in the useEffect hook calling getArtists where it is incorrectly attempting to access a property of the first element of the projects array which is currently an undefined object.

const getArtists = () => {
  const artists = [];
  let curArtist = projects[0].artist; // <-- error, access artist of undefined!
  let projectCount = 0;
  const now = new Date();
  let firstAdded = now;

  for (const { artist, dateAdded } of projects) {
    if (artist !== curArtist) {
      artists.push({ name: curArtist, projectCount, firstAdded });
      curArtist = artist;
      projectCount = 0;
      firstAdded = now;
    }
    projectCount  ;
    const projectDate = new Date(dateAdded);
    if (projectDate < firstAdded)
      firstAdded = projectDate;
  }
  setArtists(artists);
};

useEffect(getArtists, [projects, sortBy]);

What you can do is to only run the logic in getArtists is the array is populated.

const getArtists = () => {
  const artists = [];
  let curArtist = projects[0].artist;
  let projectCount = 0;
  const now = new Date();
  let firstAdded = now;

  for (const { artist, dateAdded } of projects) {
    if (artist !== curArtist) {
      artists.push({ name: curArtist, projectCount, firstAdded });
      curArtist = artist;
      projectCount = 0;
      firstAdded = now;
    }
    projectCount  ;
    const projectDate = new Date(dateAdded);
    if (projectDate < firstAdded)
      firstAdded = projectDate;
  }
  setArtists(artists);
};

useEffect(() => {
  if (projects.length) {
    getArtists();
  }
}, [projects, sortBy]);

And to ensure that projects is at least always a defined value, provide an initial value when destructuring the prop.

const ArtistList = ({ projects = [] }) => { ... }
  • Related