Raiden-x
Raiden-x

Reputation: 73

How to update the url of a page using an input field?

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,
                     })
                  }
               })
             }
         }

Upvotes: 3

Views: 3483

Answers (1)

Mickey
Mickey

Reputation: 580

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.

Upvotes: 1

Related Questions