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):
Create a
projects
state and agetProjects
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 invokesetProjects
. I'd then derive theartists
state fromprojects
, as before. Of course, I'd no longer need to pass theprojects
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.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 = [] }) => { ... }