16

I have a simple react hooks application - a list of Todos - with react router v4

On the List of Todos, when a Todo is clicked I need to:

  1. Dispatch the current todo in context
  2. Redirect to another route (from /todos to /todos/:id)

In the previous React Class based implementation I could use this.context.history.push to redirect to another route.

How would I handle that using React Hooks in combination of React Router v4 (in code below see my comment in function editRow())?

Code below:

=====index.js=====

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter} from "react-router-dom"

import App from './App';

ReactDOM.render(
    <BrowserRouter>
        <App />
    </BrowserRouter>, document.getElementById('root'));

=====main.js=====

import React from 'react'
import { Switch, Route } from 'react-router-dom'
import TodosList from './todoslist'
import TodosEdit from './todosedit'

const Main = () => (
  <main>
    <Switch>
      <Route exact path="/todos" component={TodosList}/>
      <Route exact path="/todos/:id" component={TodosEdit} />
    </Switch>
  </main>
)

export default Main

=====app.js=====

import React, {useContext, useReducer} from 'react';
import Main from './main'
import TodosContext from './context'
import todosReducer from './reducer'

const App = () => {
  const initialState = useContext(TodosContext);
  const [state, dispatch] = useReducer(todosReducer, initialState);
  return (
    <div>
      <TodosContext.Provider value={{state, dispatch}}>
        <Main/>
      </TodosContext.Provider>
    </div>
  )
}
export default App;

=====TodosContext.js=====

import React from 'react'

const TodosContext = React.createContext({
    todos: [
        {id:1, text:'Get Grocery', complete:false},
        {id:2, text:'Excercise', complete:false},
        {id:3, text:'Drink Water', complete:true},
    ],
    currentTodo: {}
})

export default TodosContext

=====reducer.js=====

import React from 'react'

export default function reducer(state, action){
    switch(action.type){
        case "GET_TODOS":
            return {
                ...state,
                todos: action.payload
            }
        case "SET_CURRENT_TODO":
                   return {
                       ...state,
                       currentTodo: action.payload
            }
        default: 
            return state
    }
}

=====Todos.js=====

import React, {useState, useContext, useEffect} from 'react';
import TodosContext from './context'

function Todos(){   
    const [todo, setTodo] = useState("")
    const {state, dispatch} = useContext(TodosContext)
    useEffect(()=>{
        if(state.currentTodo.text){
            setTodo(state.currentTodo.text)
        } else {
            setTodo("")
        }
        dispatch({
            type: "GET_TODOS",
            payload: state.todos
        })
    }, [state.currentTodo.id])

    const editRow = event =>{
        let destUrlEdit = `/todos/${event.id}`

        let obj = {}
        obj.id = event.id
        obj.text = event.text

        dispatch({type:"SET_CURRENT_TODO", payload: obj})

        //after dispatch I would like to redirect to another route to do the actual edit
        //destUrlEdit
    }
    return(
        <div>
            <h1>List of ToDos</h1>
            <h4>{title}</h4>
            <ul>
                {state.todos.map(todo => (
                    <li key={todo.id}>{todo.text} &nbsp;
                        <button onClick={()=>{
                            editRow(todo)}}>
                        </button>
                    </li>
                ))}
            </ul>
        </div>
    )
}

export default Todos;
WD1
  • 163
  • 1
  • 1
  • 4

3 Answers3

20

It's actually a lot simpler than the other answers, React Router v5.1 provides a useHistory hook.

import React from 'react'
import { useHistory } from 'react-router-dom'

const MyComponent = () => {
  const history = useHistory()
  const handleButtonClick = (event) => {
    history.push(event.target.value)
  }
  return (
    <button
      type="button"
      value="/my/path"
      onClick={handleButtonClick}
    >
      Navigate Me!
    </button>
  )
}
Lynden Noye
  • 621
  • 5
  • 6
  • 1
    I think it's actually v5.1 that introduces this feature: https://reacttraining.com/blog/react-router-v5-1/ – jmargolisvt Feb 22 '20 at 20:38
  • Yeah you're right! I've updated the version in my answer. – Lynden Noye Feb 23 '20 at 03:04
  • 1
    history.push('/') will still send the user to an old iteration of the page, though, you will still need to either put it inside of a useEffect and provide the useEffect with a variable to check for change, or provide props to your history via props.history.push('/'), where props are handed into the functional component. – to240 Jul 16 '20 at 16:04
13

Your problem is related to Programmatically navigating using react-router-v4 instead of with hooks,

In react-router-v4, you would get history from props if the Todos component is rendered as a child or Route or from an ancestor that is render form Route and it passed the Router props to it. However it is not receiving Router props, you can use withRouter HOC from react-router to get the router props and call props.history.push(destUrlEdit)

import React, {useState, useContext, useEffect} from 'react';
import TodosContext from './context'
import { withRouter } from 'react-router-dom';

function Todos(props){   
    const [todo, setTodo] = useState("")
    const {state, dispatch} = useContext(TodosContext)
    useEffect(()=>{
        if(state.currentTodo.text){
            setTodo(state.currentTodo.text)
        } else {
            setTodo("")
        }
        dispatch({
            type: "GET_TODOS",
            payload: state.todos
        })
    }, [state.currentTodo.id])

    const editRow = event =>{
        let destUrlEdit = `/todos/${event.id}`

        let obj = {}
        obj.id = event.id
        obj.text = event.text

        dispatch({type:"SET_CURRENT_TODO", payload: obj})

        //after dispatch I would like to redirect to another route to do the actual edit
        //destUrlEdit
        props.history.push(destUrlEdit);
    }
    return(
        <div>
            <h1>List of ToDos</h1>
            <h4>{title}</h4>
            <ul>
                {state.todos.map(todo => (
                    <li key={todo.id}>{todo.text} &nbsp;
                        <button onClick={()=>{
                            editRow(todo)}}>
                        </button>
                    </li>
                ))}
            </ul>
        </div>
    )
}

export default withRouter(Todos);
Shubham Khatri
  • 211,155
  • 45
  • 305
  • 318
  • 1
    thank you for your prompt reply!! This pointed me in the right direction to explore accessing React Router props in React Class components vs React Functional components. One thing to add to your answer is since inherently gives the {history, match, location} props you do not need the HOC withRouter if the route is already defined and is at the main level. The props are accessible as is. But if the component is deeply nested then the HOC withRouter becomes helpful. – WD1 Feb 08 '19 at 19:37
  • 3
    I wrote an article on this: https://dev.to/httpjunkie/programmatically-redirect-in-react-with-react-router-and-hooks-3hej and a sandbox example here: https://6wo1673y2r.codesandbox.io/ May help! – Eric Bishard May 07 '19 at 01:48
  • The React logs give the sense that a recursive state update is happening, which causes component to be unmount and then mounted again, but in the end the history unmount the component before the state is persisted, that's so strange. –  Jan 19 '20 at 05:07
  • @EricBishard I have been looking for hours something as simple as you suggest in your article. I think you should post it as an answer here. – kroiz Sep 12 '20 at 07:15
0

Using react-redux and connected-react-router...

import {useDispatch } from 'react-redux';
import { push } from 'connected-react-router';

export default () => {
 const dispatch = useDispatch();

 return (
   <Button onClick={() => dispatch(push('/login'))}>
     Login
   </Button>    
  );
};
Sten Muchow
  • 6,373
  • 4
  • 33
  • 46
  • I get the error `Uncaught Could not find router reducer in state tree, it must be mounted under "router"`, even though everything is set up correctly from before I started using hooks. Old methods using component classes worked. Once I switched to this method I get that error. – mjwrazor Jan 15 '21 at 15:40