Reputation: 697
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
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
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