React-router URL changes but page is still unchanged

Viewed 1526

I am new to react and react-router, so please go easy on me.

I am trying to implement router in my Todo List project, where path="/" takes me to my todo list and path="/id" takes me to a test page (later will show the description of the task).

When I click the link that takes me to "/id", the URL in the browser changes but the page/content doesn't. However, when I refresh my browser, the test page loads.

I have put the Switch in App.js shown below.

import React, { Component } from "react";
import "./App.css";
import TodoList from "./components/TodoList";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Test from "./components/Test";

class App extends Component {
  render() {
    return (
      <Router>
        <div className="todo-app">
          <p>
            <Link to="/">Home</Link>
          </p>
          <Switch>
            <Route exact path="/" component={TodoList} />
            <Route path={`/id`} component={Test} />
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;

And I have put the Link to "/id" as shown below in a child component of component which is called here in App.js.

<div key={todo.id}>
    <Link className="todo-text" to={`/id/${todo.id}`}>
      {todo.text}
    </Link>
</div>

Am I missing something which is causing my component to not load when I click the link?

Edit: Here's a link to my project. https://stackblitz.com/edit/react-7cpjp9?file=src/index.js

3 Answers

Issue

Ok, the issue is exactly as I had suspected. You are rendering multiple routers in your app. The first is a BrowserRouter in your index.js file, the second, another BrowserRouter in App.js, and at least a third BrowserRouter in Todo.js. You need only one router to provide a routing context for the entire app.

The issue here is that the router in Todo component is the closest router context to the links to specific todo details. When a link in Todo is clicked, this closest router handles the navigation request and updates the URL in the address bar. The blocks, or "masks", the router in App component or index.js that is rendering the routes from "seeing" that a navigation action occurred. In other words, the URL in the address bar is updated by the inner router, but the outer router doesn't know to render a different route.

Solution

Keep the BrowserRouter wrapping App in index.js and remove all other routers used in your app.

App - Remove the Router component. Also, reorder the routes/paths from most specific to least specific so you don't need to specify the exact prop on every route. Allows more specific paths to be matched and rendered before less specific paths by the Switch component.

class App extends Component {
  render() {
    return (
      <div className="todo-app">
        <p>
          <Link to="/">Home</Link>
        </p>
        <Switch>
          <Route path="/id/:todoId" component={Test} />
          <Route path="/" component={TodoList} />
        </Switch>
      </div>
    );
  }
}

Todo - Remove the Router component. Move the key={todo.id} up to the outer-most element so when todos array is updated React can reconcile updates.

class Todo extends Component {
  constructor(props) {
    super(props);
    this.state = {
      id: null,
      value: "",
      details: "",
    };
    this.submitUpdate = this.submitUpdate.bind(this);
  }

  submitUpdate(value) {
    const { updateTodo } = this.props;
    updateTodo(this.state.id, value);
    this.setState({
      id: null,
      value: "",
    });
  }

  render() {
    const { todos, completeTodo, removeTodo } = this.props;
    if (this.state.id) {
      return <TodoForm edit={this.state} onSubmit={this.submitUpdate} />;
    }

    return todos.map((todo, index) => (
      <div
        className={todo.isComplete ? "todo-row complete" : "todo-row"}
        key={todo.id}
      >
        <div>
          <Link className="todo-text" to={`/id/${todo.id}`}>
            {todo.text}
          </Link>
        </div>
        <div className="icons">
          <RiCloseCircleLine
            onClick={() => removeTodo(todo.id)}
            className="delete-icon"
          />
          <TiEdit
            onClick={() => this.setState({ id: todo.id, value: todo.text })}
            className="edit-icon"
          />
          <RiCheckboxCircleLine
            onClick={() => completeTodo(todo.id)}
            className="delete-icon"
          />
        </div>
      </div>
    ));
  }
}

First of all the approach, you are taking for dynamic routing is wrong. It should be like this you will have to add the exact keyword on the dynamic route.

<Route exact path="/id/:todoId" component={Test} />

And

<div key={todo.id}>
<Link className="todo-text" to={`/id/${todo.id}`}>
  {todo.text}
</Link>
import React, { Component } from "react";
import "./App.css";
import TodoList from "./components/TodoList";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Test from "./components/Test";

class App extends Component {
  render() {
    return (
      <Router>
        <div className="todo-app">
          <p>
            <Link to="/">Home</Link>
          </p>
          <Switch>
            <Route exact path="/" component={TodoList} />
            **<Route exact path={`/id`} component={Test} />**
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;
Related