'React app throws TypeError: Cannot read properties of undefined (reading 'name') when compiling

I keep running into an issue with my CRUD application when I go to edit a name from my list, it throws an error "TypeError: Cannot read properties of undefined (reading 'name')".

It seems to stem from the "value={selectedUser.name}" in my input field, but when I remove it the app compiles but I cannot alter the field at all - nothing happens when you type. Any help is appreciated!

import React, { useState, useContext, useEffect } from 'react'
import { GlobalContext } from '../context/GlobalState'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { Form, FormGroup, Label, Input, Button } from 'reactstrap'

const EditUser = (props) => {
const [selectedUser, setSelectedUser] = useState({
  id: '',
  name: ''
});
const { users, editUser } = useContext(GlobalContext);
const navigate = useNavigate();
const currentUserId = useParams(props);

useEffect(() => {
  const userId = currentUserId;
  const selectedUser = users.find(user => user.id === userId)
  setSelectedUser(selectedUser)
}, [currentUserId, users])

const onSubmit = (e) => {
  e.preventDefault()
  editUser(selectedUser)
  navigate('/');
}

const onChange = (e) => {
  setSelectedUser({...selectedUser, [e.target.name]: e.target.value})
}

  return (
    <div>
        <Form onSubmit={onSubmit}>
            <FormGroup>
                <Label>Edit Name</Label>
                <Input type='text' name='name' value={selectedUser.name} onChange={onChange} placeholder='Enter Name'></Input>
            </FormGroup>
        <Button type='submit'>Edit Name</Button>
        <Link to='/' className='btn btn-danger m-2'>Back</Link>
        </Form>
    </div>
  )
}

export default EditUser


Solution 1:[1]

Issue

Array.prototype.find returns the first matching element, or undefined if no such match is found.

const selectedUser = users.find(user => user.id === userId); // potentially undefined

Additionally, the returned value from the useParams hook is an object of the key-value pairs of any defined route path parameters. The code appears to incorrectly assume the returned value is the currentUserId parameter from a path, and uses that to search the users array.

const currentUserId = useParams(props);

useEffect(() => {
  const userId = currentUserId;
  const selectedUser = users.find(user => user.id === userId);
  setSelectedUser(selectedUser);
}, [currentUserId, users]);

I doubt there could ever be a found match comparing user.id against the returned params object.

Solution

I suspect that the actual path param is really userId. Either destructure it immediately from the useParams hook.

const { userId } = useParams();

or rename currentUserId to params and destructure userId in the hook.

const params = useParams(props);

useEffect(() => {
  const { userId } = params;
  const selectedUser = users.find(user => user.id === userId);
  setSelectedUser(selectedUser);
}, [params.userId, users]);

One last point about route params and id properties. The route params will always be a string type. Sometimes the id is actually a number type, so you'll want to use a type-safe equality check. Convert ids to string types, which covers both string ids and will convert numbers to number-like strings.

useEffect(() => {
  const { userId } = params;
  const selectedUser = users.find(user => String(user.id) === userId);
  setSelectedUser(selectedUser);
}, [params.userId, users]);

Guarding against null/undefined values.

  1. First, try to maintain a stable selectedUser state invariant. In other words, don't update it an undefined value unless you have a specific need to. Check the result of users.find to see if a match was found.

    const { userId } = useParams(props);
    
    useEffect(() => {
      const selectedUser = users.find(user => String(user.id) === userId);
      if (selectedUser) {
        setSelectedUser(selectedUser);
      }
    }, [userId, users]);
    
  2. Second, defensively guard against potentially null/undefined accesses in your render function, just in case. You can use the Optional Chaining and Nullish Coalescing Operators to guard null access and provide a valid defined value for the controlled input.

    <Input
      type='text'
      name='name'
      value={selectedUser?.name ?? ""}
      onChange={onChange}
      placeholder='Enter Name'
    />
    

    or if you prefer older-school null-checks/guard-clauses

    <Input
      type='text'
      name='name'
      value={(selectedUser && selectedUser.name) ?? ""}
      onChange={onChange}
      placeholder='Enter Name'
    />
    

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Drew Reese