Search code examples
reactjsreact-hooksreact-router-dom

Uncaught Runtime Error in React.js application


I've got a simple To-Do list application written in React, and I'm trying to add login to it. I want to have the login page as the landing page and redirect to the Dashboard after login, but I'm still new to React. The code compiles without errors, and I get the following error in the browser after I submit username and password:

tasks is undefined
Dashboard@http://localhost:3000/static/js/bundle.js:215:20
renderWithHooks@http://localhost:3000/static/js/bundle.js:26189:31
mountIndeterminateComponent@http://localhost:3000/static/js/bundle.js:29473:17
beginWork@http://localhost:3000/static/js/bundle.js:30769:20
callCallback@http://localhost:3000/static/js/bundle.js:15785:18
invokeGuardedCallbackDev@http://localhost:3000/static/js/bundle.js:15829:20
invokeGuardedCallback@http://localhost:3000/static/js/bundle.js:15886:35
beginWork$1@http://localhost:3000/static/js/bundle.js:35750:32
performUnitOfWork@http://localhost:3000/static/js/bundle.js:34998:16
workLoopSync@http://localhost:3000/static/js/bundle.js:34921:26
renderRootSync@http://localhost:3000/static/js/bundle.js:34894:11
performSyncWorkOnRoot@http://localhost:3000/static/js/bundle.js:34586:38
flushSyncCallbacks@http://localhost:3000/static/js/bundle.js:22622:26
flushSyncCallbacksOnlyInLegacyMode@http://localhost:3000/static/js/bundle.js:22604:9
scheduleUpdateOnFiber@http://localhost:3000/static/js/bundle.js:34116:11
dispatchSetState@http://localhost:3000/static/js/bundle.js:27217:32
./node_modules/react-router-dom/dist/index.js/BrowserRouter/setState<@http://localhost:3000/static/js/bundle.js:39391:101
push@http://localhost:3000/static/js/bundle.js:1815:15
./node_modules/react-router/dist/index.js/useNavigateUnstable/navigate<@http://localhost:3000/static/js/bundle.js:40519:61
handleSubmit@http://localhost:3000/static/js/bundle.js:841:15

I'm not sure how to go about debugging this error. Could this be a routing problem? Note that there isn't any registration or real verification happening. I'm just interesting in getting the login to redirect at the moment.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';

const DATA = [
  {id: "todo-0", name: "Make bed", completed: true},
  {id: "todo-1", name: "Fold laundry", completed: false},
  {id: "todo-2", name: "Brush teeth", completed: false}
];

ReactDOM.render(
  <BrowserRouter>
    <App tasks={DATA} />
  </BrowserRouter>,
  document.getElementById('root')
);

Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';
import { useNavigate } from 'react-router-dom';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
  const navigate = useNavigate();
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();

  const handleSubmit = async e => {
    e.preventDefault();
    const token = await loginUser({
      username,
      password
    });
    if(token) {
      setToken(token);
      navigate("/dashboard");
    }
  }

  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)} />
        </label>
        <div>
          <button type="login">Login</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

App.js

import Dashboard from './components/Dashboard/Dashboard';
import Login from './components/Login/Login';
import { Route, Routes } from "react-router-dom";
import './App.css';
import useToken from './useToken';

function App() {
  const {token, setToken} = useToken();
  
  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </div>
  );
}

export default App;

Dashboard.js

import React, { useState, useRef, useEffect } from "react";
import Form from "./Form";
import FilterButton from "./FilterButton";
import Todo from "./Todo";
import { nanoid } from "nanoid";

function usePrevious(value) {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
  }
  
  const FILTER_MAP = {
    All: () => true,
    Active: (task) => !task.completed,
    Completed: (task) => task.completed
  };
  
  const FILTER_NAMES = Object.keys(FILTER_MAP);

export default function Dashboard(props) {
    const [tasks, setTasks] = useState(props.tasks);
    const [filter, setFilter] = useState('All');

    function addTask(name) {
        const newTask = { id: `todo-${nanoid()}`, name, completed: false };
        setTasks([...tasks, newTask]);
      }
    
      function toggleTaskCompleted(id) {
        const updatedTasks = tasks.map((task) => {
          // if this task has the same ID as the edited task
          if (id === task.id) {
            // use object spread to make a new object
            // whose `completed` prop has been inverted
            return {...task, completed: !task.completed}
          }
          return task;
        });
        setTasks(updatedTasks);
      }
    
      function deleteTask(id) {
        const remainingTasks = tasks.filter((task) => id !== task.id);
        setTasks(remainingTasks);
      }
    
      function editTask(id, newName) {
        const editedTaskList = tasks.map((task) => {
        // if this task has the same ID as the edited task
          if (id === task.id) {
            //
            return {...task, name: newName}
          }
          return task;
        });
        setTasks(editedTaskList);
      }  
    
      const taskList = tasks.filter(FILTER_MAP[filter]).map((task) => (
        <Todo
          id={task.id}
          name={task.name}
          completed={task.completed}
          key={task.id}
          toggleTaskCompleted={toggleTaskCompleted}
          deleteTask={deleteTask}
          editTask={editTask}/>
      ));
      
      
      const filterList = FILTER_NAMES.map((name) => (
        <FilterButton
          key={name}
          name={name}
          isPressed={name === filter}
          setFilter={setFilter}
        />
      ));
    
      const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task';
      const headingText = `${taskList.length} ${tasksNoun}`;
      const listHeadingRef = useRef(null);
      const prevTaskLength = usePrevious(tasks.length);
    
      useEffect(() => {
        if (tasks.length - prevTaskLength === -1) {
          listHeadingRef.current.focus();
        }
      }, [tasks.length, prevTaskLength]);

    return(
        <div className="todoapp stack-large">
        <h1>Trevor's ToDo List</h1>
            <Form addTask={addTask} />
            <div className="filters btn-group stack-exception">
                {filterList}
            </div>
            <h2 id="list-heading" tabIndex="-1" ref={listHeadingRef}>
                {headingText}
            </h2>
            <ul className="todo-list stack-large stack-exception" aria-labelledby="list-heading" >
                {taskList}
            </ul>
        </div>
    );
}

Solution

  • Issue

    The error is "tasks is undefined" somewhere in "Dashboard@http://localhost:3000/static/js/bundle.js:215:20". Based on the code it looks like the tasks state in Dashboard is undefined because props.tasks is undefined because no tasks prop was passed to Dashboard.

    <Routes>
      <Route path="/login" element={<Login />} />
      <Route
        path="/dashboard"
        element={<Dashboard />} // <-- no tasks prop passed!
      />
    
    </Routes>
    
    export default function Dashboard(props) {
      const [tasks, setTasks] = useState(props.tasks); // <-- undefined
      ...
    

    Anywhere in Dashboard where tasks is referenced, e.g. like tasks.map() or tasks.filter(), will throw an error because tasks is undefined.

    Solution

    Pass the tasks prop passed to App through to children components.

    index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter } from 'react-router-dom';
    import './index.css';
    import App from './App';
    
    const DATA = [
      {id: "todo-0", name: "Make bed", completed: true},
      {id: "todo-1", name: "Fold laundry", completed: false},
      {id: "todo-2", name: "Brush teeth", completed: false}
    ];
    
    ReactDOM.render(
      <BrowserRouter>
        <App tasks={DATA} /> // <-- tasks passed here
      </BrowserRouter>,
      document.getElementById('root')
    );
    

    App

    function App({ tasks }) { // <-- access tasks prop
      const { token, setToken } = useToken();
      
      if (!token) {
        return <Login setToken={setToken} />
      }
    
      return (
        <div>
          <Routes>
            <Route path="/login" element={<Login setToken={setToken} />} />
            <Route
              path="/dashboard"
              element={<Dashboard tasks={tasks} />} // <-- pass tasks prop
            />
          </Routes>
        </div>
      );
    }
    

    Dashboard

    export default function Dashboard(props) {
      const [tasks, setTasks] = useState(props.tasks); // <-- defined 🙂
      ...