Niko
Niko

Reputation: 697

Dispatching Redux actions on location change with react-router-dom

I am using React and Redux for a search application. Using react-router-dom, I'm routing /search/:term? to a Search component:

<Router>
  <Switch>
    <Route exact path="/search/:term?" component={Search} />
    <Redirect to="/search" />
  </Switch>

const Search = (props) => {
  const { term } = props.match.params;
  return (
    <div>
      <SearchForm term={term}/>
      <SearchResults />
    </div>
  )
};

When a user submits a search in the SearchForm component, I'm dispatching an action to submit the search query. I'm also initiating a search in the constructor if a term is given, initially:

class SearchForm extends Component {
  constructor(props) {
    super(props);

    const term = props.term ? props.term : '';
    this.state = {
      term: term,
    }

    if (term) {
      this.props.submitSearch(term);
    }
  }

  handleSubmit = (e) => {
    e.preventDefault();
    if (this.state.term) {
      this.props.submitSearch(this.state.term);
    }
  }

  render = () => {
    <form
      onSubmit={this.handleSubmit.bind(this)}>
      ...
    </form>
  }
}

I'm using withRouter from react-router-dom, so the URL updates when the search is submitted.

The problem happens when the user navigates Back in their browser. The URL navigates back, the props update (i.e. props.match.params.term), but the search does not resubmit. This is because the submitSearch action only gets dispatched in SearchForm.constructor (search on initial loading if a term is in the URL) and SearchForm.handleSubmit.

What is the best way to listen for a state change to term when the URL changes, then dispatch the search action?

Upvotes: 0

Views: 2626

Answers (2)

Eld0w
Eld0w

Reputation: 904

I would retrieve the route parameter in componentDidMount since you are pushing a new route and therefore reloading the view.

Inside your SearchForm it would look like this.

state = {
  term: '';
}

onChange = (term) => this.setState({ term })

onSubmit = () => this.props.history.push(`/search/${this.state.term}`);

And in your SearchResult :

componentDidMount() {
  this.props.fetchResults(this.props.term)
}

A nice thing to do would be to keep the SearchResult component dry. There are several ways to achieve that, here is one using higher order components aka HOC :

export default FetchResultsHoc(Component) => {

  @connect(state => ({ results: state.searchResults }))
  class FetchResults extends React.Component {
    componentDidMount(){
      dispatch(fetchResults(this.props.match.params.term))
    }

    render(){
      <Component {...this.props} />
    }
  }

  return FetchResultsHoc;
}

That you would then call on your SearchResult component using a decorator.

import { fetchResults } from './FetchResultsHoc';

@fetchResults
export default class SearchResult extends React.PureComponent { ... }
// You have now access to this.props.results inside your class

Upvotes: 1

Niko
Niko

Reputation: 697

My current solution is to dispatch submitSearch in the componentWillRecieveProps lifecycle method if the new props don't match the current props:

componentWillReceiveProps(nextProps) {
  if (this.props.term !== nextProps.term) {
    this.setState({
      term: nextProps.term,
    });
    this.props.submitSearch(nextProps.term);
  }
}

Then, instead of dispatching an action on form submission, I push a new location onto the history and componentWillReceiveProps does the dispatching:

handleSubmit = (e) => {
  e.preventDefault();
  if (this.state.term) {
    this.props.history.push('/search/'+this.state.term);
  }
}

This solution feels a little wrong, but it works. (Other's would seem to agree: Evil things you do with redux — dispatch in updating lifecycle methods)

Am I violating a React or Redux principle by doing this? Can I do better?

Upvotes: 0

Related Questions