3

I try to integrate a search page in my application with react router v5.

How could I update my url's query parameter using my search box?

When I refresh my application, I lose my search results and the value of my search field.

I use redux to manage the state of the value of my search fields and my search results, I think that going through the parameters of the url would be a better solution but I do not know how to do that.

I tried a solution (see my code), but the query parameter of the url is not synchronized with the value of my text field

Edit:

My component Routes.js

 const Routes = (props) => {
    return (
       <div>
          <Route exact path="/" component={Home} />
          <Route
             exact
             path='/search'
             component={() => <Search query={props.text} />}
          />
          <Route path="/film/:id" component={MovieDetail} />  
          <Route path="/FavorisList" component={WatchList} />
          <Route path="/search/:search" component={Search} />
          <Route path="*" component={NotFound} />  
       </div>

  )}

My component SearchBar.js (Embedded in the navigation bar, the Search route displays the search results)

EDIT:

I wish to realize the method used by Netflix for its research of series.

I want to be able to search no matter what page I am in, if there is an entry in the input field, I navigate to the search page withthis.props.history.push (`` search / ), if the input field is empty, I navigate to the page with this.props.history.goBack ().

The state inputChange is a flag that prevents me from pushing to the search page each time I enter a character.

To know more, I had opened = a post here => How to change the route according to the value of a field

  class SearchBar extends React.Component {
      constructor(props) {
         super(props);

         this.state = {
            inputValue:'',
         };

       }

      setParams({ query }) {
         const searchParams = new URLSearchParams();
         searchParams.set("query", query || "");
         return searchParams.toString();
      }

      handleChange = (event) => {

          const query = event.target.value
          this.setState({ inputValue: event.target.value})

          if (event.target.value === '') {
             this.props.history.goBack()
             this.setState({ initialChange: true }) 
             return;
          } 

          if(event.target.value.length && this.state.initialChange){
               this.setState({
                  initialChange:false
               }, ()=> {

              const url = this.setParams({ query: query });
              this.props.history.push(`/search?${url}`)
              this.search(query)
           })
         }
       }

       search = (query) => {
           //search results retrieved with redux 
           this.props.applyInitialResult(query, 1)
       }

       render() {
          return (
            <div>
               <input
                   type="text"
                   value={this.state.inputValue}
                   placeholder="Search movie..."
                   className={style.field}
                   onChange={this.handleChange.bind(this)}
               />  
            </div>
          );
        }


       export default SearchBar;

Component App.js

       class App extends React.Component {
          render(){
              return (
                 <div>
                    <BrowserRouter>
                       <NavBar />
                       <Routes/>
                    </BrowserRouter>
                 </div>
              );
           }
        }

        export default App;

Query for search results (Managed with Redux)

      export function applyInitialResult(query, page){
          return function(dispatch){
              getFilmsFromApiWithSearchedText(query, page).then(data => {
                 if(page === 1){
                     dispatch({
                        type:AT_SEARCH_MOVIE.SETRESULT,
                        query:query,
                        payload:data,
                     })
                  }
               })
             }
         }
Raiden-x
  • 73
  • 1
  • 5
  • if you want to change the url with react-router than you can use the `replace` method on `history` – Eric Hasselbring Aug 13 '19 at 18:57
  • if you are trying to persist a redux store, you should look into libs like https://github.com/rt2zz/redux-persist – Eric Hasselbring Aug 13 '19 at 18:58
  • Thanks for your reply, indeed I think about Redux persist. Currently I'm using Redux for my search component, but maybe I can manage from my component with my URL parameter instead of redux. I am not sure – Raiden-x Aug 13 '19 at 19:09
  • I just tried to replace and it works but I need to use it once, because I use the event onChange, to change page, suddenly I would like to know how to adapt to my case – Raiden-x Aug 13 '19 at 19:34
  • 1
    @EricHasselbring `replace` will destroy the previous entries in the history, which is rarely the intended behavior. It should be `push` while ensuring you don't have an `/` in the front of the string – Andrew Aug 14 '19 at 00:04
  • if you are updating for a query, seems logical, no one wants to push the back button for queries – Eric Hasselbring Aug 14 '19 at 14:45

1 Answers1

1

Instead of splitting up the routes, you could just use an optional param and handle the query or lack thereof in the component by changing <Route path="/search/:search" component={Search} /> to <Route path="/search/:search?" component={Search} /> and removing <Route exact path='/search' component={() => <Search query={props.text} />} /> entirely.

With that change, you can then get the current query by looking at the value of props.match.params.search in this component. Since you're updating the URL each time the user changes the input, you then don't need to worry about managing it in the component state. The biggest issue with this solution is you'll probably want to delay the search for a little bit after render, otherwise you'll be triggering a call on every keystroke.

EDITED IN RESPONSE TO QUESTION UPDATE

You're right, if applyInitialResult is just an action creator, it won't be async or thenable. You still have options, though.

For example, you could update your action creator so it accepts callbacks to handle the results of the data fetch. I haven't tested this, so treat it as pseudocode, but the idea could be implemented like this:

Action creator

   export function applyInitialResult(
      query, 
      page,
      // additional params
      signal,
      onSuccess,
      onFailure
      // alternatively, you could just use an onFinished callback to handle both success and failure cases
   ){
      return function(dispatch){
          getFilmsFromApiWithSearchedText(query, page, signal) // pass signal so you can still abort ongoing fetches if input changes
             .then(data => {
                onSuccess(data); // pass data back to component here
                if(page === 1){
                    dispatch({
                       type:AT_SEARCH_MOVIE.SETRESULT,
                       query:query,
                       payload:data,
                    })
                 }
              })
              .catch(err => {
                  onFailure(data); // alert component to errors
                  dispatch({
                     type:AT_SEARCH_MOVIE.FETCH_FAILED, // tell store to handle failure
                     query:query,
                     payload:data,
                     err
                  })
              })
          }
      }

searchMovie Reducer:

// save in store results of search, whether successful or not
export function searchMovieReducer(state = {}, action) => {
   switch (action.type){
      case AT_SEARCH_MOVIE.SETRESULT:
         const {query, payload} = action;
         state[query] = payload;
         break;
      case AT_SEARCH_MOVIE.FETCH_FAILED:
         const {query, err} = action;
         state[query] = err;
         break;
   }
}

Then you could still have the results/errors directly available in the component that triggered the fetch action. While you'll still be getting the results through the store, you could use these sort of triggers to let you manage initialChange in the component state to avoid redundant action dispatches or the sort of infinite loops that can pop up in these situations.

In this case, your Searchbar component could look like:

class SearchBar extends React.Component {
    constructor(props){

       this.controller = new AbortController();
       this.signal = this.controller.signal;

       this.state = {
           fetched: false,
           results: props.results // <== probably disposable based on your feedback
       }
    }

    componentDidMount(){
        // If search is not undefined, get results
        if(this.props.match.params.search){
            this.search(this.props.match.params.search);
        }
    }

    componentDidUpdate(prevProps){
        // If search is not undefined and different from prev query, search again
        if(this.props.match.params.search
          && prevProps.match.params.search !== this.props.match.params.search
        ){
            this.search(this.props.match.params.search);
        }
    }

    setParams({ query }) {
       const searchParams = new URLSearchParams();
       searchParams.set("query", query || "");
       return searchParams.toString();
    }

    handleChange = (event) => {
       const query = event.target.value
       const url = this.setParams({ query: query });
       this.props.history.replace(`/search/${url}`);
    }

    search = (query) => {
        if(!query) return; // do nothing if empty string passed somehow
        // If some search occurred already, let component know there's a new query that hasn't yet been fetched
        this.state.fetched === true && this.setState({fetched: false;})

        // If some fetch is queued already, cancel it
        if(this.willFetch){
            clearInterval(this.willFetch)
        }

        // If currently fetching, cancel call
        if(this.fetching){
            this.controller.abort();
        }

        // Finally queue new search
        this.willFetch = setTimeout(() => {
            this.fetching = this.props.applyInitialResult(
                query,
                1,
                this.signal,
                handleSuccess,
                handleFailure
            )
        },  500 /* wait half second before making async call */);
    }

    handleSuccess(data){
       // do something directly with data
       // or update component to reflect async action is over
    }

    handleFailure(err){
       // handle errors
       // or trigger fetch again to retry search
    }

    render() {
       return (
         <div>
            <input
                type="text"
                defaultValue={this.props.match.params.search} // <== make input uncontrolled
                placeholder="Search movie..."
                className={style.field}
                onChange={this.handleChange.bind(this)}
            />  
         <div>
       );
    }
}

const mapStateToProps = (state, ownProps) => ({
   // get results from Redux store based on url/route params
   results: ownProps.match.params.search
       ? state.searchMovie[ownProps.match.params.search]
       : []
});

const mapDispatchToProps = dispatch => ({
   applyInitialResult: // however you're defining it
})

export default connect(
   mapStateToProps,
   mapDispatchToProps
)(SearchBar)

EDIT 2

Thanks for the clarification about what you're imagining.

The reason this.props.match.params is always blank is because that's only available to the Search component - the Searchbar is entirely outside the routing setup. It also renders whether or not the current path is /search/:search, which is why withRouter wasn't working.

The other issue is that your Search route is looking for that match param, but you're redirecting to /search?query=foo, not /search/foo, so match params will be empty on Search too.

I also think the way you were managing the initialChange state was what caused your search value to remain unchanged. You handler gets called on every change event for the input, but it shuts itself off after the first keystroke and doesn't turn on again until the input is cleared. See:

      if (event.target.value === '') {
         this.props.history.goBack()
         this.setState({ initialChange: true }) // <== only reset in class
         return;
      } 
      ...
      if(event.target.value.length && this.state.initialChange){
           this.setState({
              initialChange:false
           }, ()=> {
           // etc...
       })
     }

This is what the pattern I was suggesting accomplishes - instead of immediately turning off your handler, set a delay for the dispatch it and keep listening for changes, searching only once user's done typing.

Rather than copying another block of code here, I instead made a working example on codesandbox here addressing these issues. It could still use some optimization, but the concept's there if you want to check out how I handled the Search page, SearchBar, and action creators.

The Searchbar also has a toggle to switch between the two different url formats (query like /search?query=foo vs match.param like /search/foo) so you can see how to reconcile each one into the code.

Mickey
  • 570
  • 3
  • 11
  • I'm trying the solution you propose. I see that you use the local state to store the search results, without going through redux, how I could update my search component that contains the search results, you should know that my search bar is integrated in my bar navigation. – Raiden-x Aug 13 '19 at 21:14
  • I try to adapt your solution, I have two errors. Looks like `this.props.match.params.search` is still undefined. Then I have a `then` error, I think my function is not asynchronous. `this.props.applyInitialResult (query, 1, this.signal)` is a function that executes the redux action, I updated my message with the code of this function. – Raiden-x Aug 13 '19 at 21:55
  • I updated my answer in response to the additional info you shared. I'm not sure why `this.props.match.params.search` would be undefined, however. Can you confirm you consolidated the search routes to only ``? Also, is the Search component in that route the one exported from Searchbar.js? If `Searchbar` is a component within a parent `Search` component, then you'll need to pass it the history and match objects as props. – Mickey Aug 13 '19 at 23:23
  • Ha no I forget to prescise, compose it SearchBar.js is embed in the navigation bar (The component NavBar.js). The Search component is used to display the results and manage the pagination, that's why I can not pass a local state and I prefer to manage a global state. If you have a better solution, I'm interested. – Raiden-x Aug 13 '19 at 23:29
  • Well, as far as I can tell, this solution should still be workable. You can still access those props in the SearchBar using [withRouter](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md). If this still doesn't do what you expect, however, I'd suggest you clarify your answer further to explain the exact cases you're imagining and what behavior you want (i.e. the purpose of `this.props.history.goBack()` and the `initialChange` state value) – Mickey Aug 14 '19 at 00:29
  • I had already added "with router" to the component, when I would display my accessories, I see come the corresponding accessories, but it is empty, by cons I have the clean location that met up to date. Yes I will update my post – Raiden-x Aug 14 '19 at 03:20
  • Alright, I updated my answer in response to your new info. – Mickey Aug 14 '19 at 19:47