1

I'm getting a new project going. I am using create-react-app with redux and thunk. The api server is a separate project, and uses node/express and mongo/mongoose for the database. I am trying to redirect to an item list view after either a create or delete item action. If the redirect is in the component itself, the redirect happens before the action is completed, and the refetch of the list happens before the create or delete happens. Therefore I am trying to use thunk to dispatch a redirect action in the .then() part of the action. The redirect successfully changes the url in the browser, but no re-render is triggered. How can I get the redirect to from either the create action or delete action to trigger a re-render of the stockitems list component?

full code is at: https://github.com/jhlindell/BarCode-app/tree/stockitem server is at: https://github.com/jhlindell/BarCode-server/tree/jon

My index.js:

import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import React from 'react';
import ReactDOM from 'react-dom';
import registerServiceWorker from './registerServiceWorker';
import { ConnectedRouter } from 'connected-react-router'

import { createStore, applyMiddleware, compose } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createBrowserHistory } from 'history';
// import { browserHistory } from 'react-router'
import reducers from './reducers';
import thunk from 'redux-thunk';
import { connectRouter, routerMiddleware } from 'connected-react-router'
import { logger } from 'redux-logger';

const history = createBrowserHistory();
const reactRouterMiddleware = routerMiddleware(history); 

const middleWares = [
  thunk,
  logger,
  reactRouterMiddleware
]

const store = createStore(
  connectRouter(history)(reducers), 
  composeWithDevTools(applyMiddleware(...middleWares)));

ReactDOM.render(
  <Provider store = {store}>
    <ConnectedRouter history={history}>
      <App history={history}/>
    </ConnectedRouter>
  </Provider>
, document.getElementById('root'));
registerServiceWorker();

My app.js file:

import './App.css';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Footer from './components/Nav/Footer';
import NavBar from './components/Nav/NavBar';
import React, { Component } from 'react';
import StockItemCreate from './components/StockItems/StockItemCreate';
import StockItemDetail from './components/StockItems/StockItemDetail';
import StockItemList from './components/StockItems/StockItemList';
import HomePage from './components/HomePage';


class App extends Component {
  com

  render() {
    const flexCol = {
      display: 'flex',
      flexDirection: 'column',
    };

    const flex0 = {
      flex: 0
    };

    const flex1 = {
      display: 'flex',
      flex: '1 1 100%',
    };
    return (
      <Router>
        <div className="App" style={flexCol}>
          <div style={flex0}>
            <NavBar />
          </div>
          <div style={flex1} id="mainBlock">
            <Switch>
              <Route exact path='/' component={HomePage} />
              <Route exact path='/stockitems/create' component={StockItemCreate} />
              <Route exact path='/stockitems' component={StockItemList} />
              <Route path='/stockitems/:id' component={StockItemDetail} />
            </Switch>
          </div>
          <div style={flex0}>
            <Footer />
          </div>
        </div>
      </Router>
    );
  }
}

export default App;

the list component:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import React, {Component} from 'react';
import { getStockItemList, clearStockItemList } from '../../actions';

const listStyle = {
    display: 'flex',
    margin: 'auto'
}

class StockItemList extends Component {
  componentDidMount(){
    this.props.getStockItemList();
  }

  componentWillUnmount(){
    this.props.clearStockItemList();
  }

  render(){
    return (
      <div style={listStyle}>
        {this.props.stockItemList ? <ul className="list-group">
          {this.props.stockItemList.map((item) => {
            return <li 
            className="list-group-item" 
            key={item.name}
            onClick={()=> this.props.history.push(`/stockitems/${item._id}`)}
            >{item.name}</li>
          })}
        </ul> : <span>loading...</span>}
      </div>
    );
  }
}

function mapStateToProps(state){
  return { stockItemList: state.stockItemList }
}

function mapDispatchToProps(dispatch){
  return bindActionCreators({ getStockItemList, clearStockItemList }, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(StockItemList);

the create component:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import React, {Component} from 'react';
import { createStockItem } from '../../actions'

const cardStyle = {
  display: 'flex',
  margin: 'auto',
};

const formStyle = {
  display: 'flex',
  flexDirection: 'column',
  width: '80%',
  margin: 'auto'
};

class StockItemCreate extends Component{
  constructor(props){
    super(props);
    this.state = {
      name: '',
      description: ''
    }
  }
  handleFormSubmit = (event) => {
    event.preventDefault();
    this.props.createStockItem(this.state);
    this.clearForm();
    //this.props.history.push('/stockitems');
  }

  handleInputChange = (event) => {
    const target = event.target;
    const value = target.value;
    const name = target.name;
    this.setState({[name]: value});
  }

  clearForm = () => {
    this.setState({ name: '', description: ''});
  }

  render(){
    return(

        <form className="card" onSubmit={this.handleFormSubmit} style={cardStyle}>
            <div className="card-header">
              <h3>Add new ingredient</h3>
            </div>
            <div className="card-block mt-2">
              <div style={formStyle}> 
                <label>Name</label>
                <input name="name" type="text"
                  onChange={(e) => {this.handleInputChange(e)}}
                  placeholder="Name"
                  value={this.state.name}/>             
              </div>
              <div className="mt-2" style={formStyle}> 
                <label>Description</label>
                <input name="description" type="text"
                  onChange={(e) => {this.handleInputChange(e)}}
                  placeholder="Description"
                  value={this.state.description}/>             
              </div>
            </div>
            <div className="btn-group mb-2 mt-2" style={{padding: '0', margin: 'auto'}}>
              <button className="btn btn-primary" type="submit">
                Submit
              </button>
              <button className="btn btn-secondary" type="button" onClick={()=>this.clearForm()}>
                Cancel
              </button>
            </div>
        </form>

    )
  }
}

function mapDispatchToProps(dispatch){
  return bindActionCreators({ createStockItem }, dispatch);
}

export default connect(null, mapDispatchToProps)(StockItemCreate);

and finally, the action file where the redirect is called:

import axios from 'axios';
import { push, replace } from 'connected-react-router'


const URL = 'http://localhost:8000';

export function getStockItemList(){
  return function(dispatch){
    axios.get(`${URL}/api/stock_items/`)
      .then((response) => {
        dispatch({ type: 'STOCK_ITEM_LIST', payload: response.data });
      })
      .catch((error) => {
        console.log('error getting stock items');
      });
  }
}

export function clearStockItemList(){
  return { type: 'CLEAR_STOCK_ITEM_LIST' };
}

export function getStockItemById(id){
  return function(dispatch){
    axios.get(`${URL}/api/stock_items/${id}`)
      .then((response) => {
        dispatch({ type: 'SINGLE_STOCK_ITEM', payload: response.data });
      })
      .catch((error) => {
        console.log('error getting stock item by id');
        dispatch(push('/stockitems'));
      });
  }
}

export function clearSingleStockItem(){
  return { type: 'CLEAR_SINGLE_STOCK_ITEM' };
}

export function createStockItem(item){
  return function(dispatch){
    axios.post(`${URL}/api/stock_items/`, item)
      .then((response)=> {
        console.log("response", response);
        dispatch(push('/stockitems'));
      })
      .catch((error) => {
        //create error container to post error to
        console.log('error creating stock item', error);
      });
  }
}

export function deleteStockItem(id){
  return function(dispatch){
    axios.delete(`${URL}/api/stock_items/${id}`)
      .then((response)=> {
        console.log("delete response: ", response);
        dispatch(push('/stockitems'));
      })
      .catch((error) => {
        //create error container to post error to
        console.log('error deleting stock item', error);
      });
  }
}
skyboyer
  • 15,149
  • 4
  • 41
  • 56
Jon Lindell
  • 11
  • 1
  • 1
  • 3

2 Answers2

3

Consider using push from connected-react-router as an "action creator" instead of this.props.history.push.

Trevor Reid
  • 2,533
  • 3
  • 23
  • 33
0

As per this SO answer:

Changing from this:

    <Switch>
      <Route path="/">
        <Welcome />
      </Route>
      <Redirect to="/" />
    </Switch>

To this:

    <Switch>
      <>
        <Route path="/">
          <Welcome />
        </Route>
        <Redirect to="/" />
      </>
    </Switch>

... worked for me.

I was trying to implement a basic fallback redirect for any paths not explicitly specified, so random paths like http://localhost:3000/askfjasdf would redirect to http://localhost:3000. For some reason adding the fragment as a top level child of <Switch> did the trick.

Adam Caron
  • 61
  • 5
  • This isn't the correct way to do it. You should use `exact` - https://stackoverflow.com/questions/49162311/react-difference-between-route-exact-path-and-route-path – Anubhav Das Aug 28 '20 at 16:48