javascriptreactjsreact-router

React Router does not update component if url parameter changes


I just implemented a global search in my website and I started having issues with React-Router. It is not updating the view if the url changes parameters.

For example, navigating from /users/454545 to /teams/555555 works as expected. However, navigating from /teams/111111 to teams/222222 changes the url but the component is still /teams/111111.

Here is my code fo the Search Input field.

const SearchResult = ({ id, url, selectResult, text, type }) => (
    <Row key={id} onClick={() => selectResult(url)} width='100%' padding='5px 15px 5px 15px' style={{cursor: 'pointer'}}>
        <Column  alignItems='flex-start' style={{width: '100%'}}>
            <Label textAlign='left' color='#ffffff'>{text}</Label>
        </Column>
        <Column style={{width: '100%'}}>
            <Label textAlign='right' color='#ffffff'>{type}</Label>
        </Column>
    </Row>
)

const SearchInput = (props) => {
    const { isSearching, name, onChange, onClear, results } = props;

    return (
        <Section width='100%' style={{display: 'flex', position: 'relative'}}>
            <Wrapper height={props.height} margin={props.margin}>
                <i className="fas fa-search" style={{color: 'white'}} />
                <input id='search_input' placeholder={'Search for a team, circuit, or user'} name={name} onChange={onChange} style={{outline: 'none', backgroundColor: 'transparent', borderColor: 'transparent', color: '#ffffff', width: '100%'}} />
                {onClear && !isSearching && <i onClick={onClear} className="fas fa-times-circle" style={{color: '#50E3C2'}} />}
                {isSearching && 
                <Spinner viewBox="0 0 50 50" style={{marginBottom: '0px', height: '50px', width: '50px'}}>
                    <circle
                    className="path"
                    cx="25"
                    cy="25"
                    r="10"
                    fill="none"
                    strokeWidth="4"
                     />
                </Spinner>
                }
            </Wrapper>
            {results && <Section backgroundColor='#00121A' border='1px solid #004464' style={{maxHeight: '400px', position: 'absolute', top: '100%', left: '0px', width: '97%', overflowY: 'scroll'}}>
                <Section backgroundColor='#00121A' style={{display: 'flex', flexDirection: 'column', padding: '15px 0px 0px 0px', justifyContent: 'center', alignItems: 'center', width: '100%'}}>
                    {results.length === 0 && <Text padding='0px 0px 15px 0px' color='#ffffff' fontSize='16px'>We didn't find anything...</Text>}
                    {results.length !== 0 && results.map(r => <SearchResult selectResult={props.selectResult} id={r._id} url={r.url} text={r.text} type={r.type} />)}
                </Section>
            </Section>}
        </Section>
    )
}

export default SearchInput;

The parent component is a nav bar which looks something like this. I've slimmed it down for readability.

import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';

import SearchInput from '../shared/inputs/SearchInput';

const TopNav = (props) => {
    const [search, setSearch] = useState(null);
    const [searchResults, setSearchResults] = useState(null);
    const debouncedSearchTerm = useDebounce(search, 300);
    const [isSearching, setIsSearching] = useState(false);

    function clearSearch() {
        document.getElementById('search_input').value = '';
        setSearchResults(null);
    }

    function searchChange(e) {
        if (!e.target.value) return setSearchResults(null);
        setSearch(e.target.value);
        setIsSearching(true);
    }

    async function updateQuery(query) {
        const data = {
            search: query
        }
        
        const results = await api.search.query(data);

        setSearchResults(results);
        setIsSearching(false);
    }

    function selectResult(url) {
        props.history.push(url);
        setSearchResults(null);
    }

    function useDebounce(value, delay) {
        // State and setters for debounced value
        const [debouncedValue, setDebouncedValue] = useState(value);
      
        useEffect(
          () => {
            // Update debounced value after delay
            const handler = setTimeout(() => {
              setDebouncedValue(value);
            }, delay);
      
            // Cancel the timeout if value changes (also on delay change or unmount)
            // This is how we prevent debounced value from updating if value is changed ...
            // .. within the delay period. Timeout gets cleared and restarted.
            return () => {
              clearTimeout(handler);
            };
          },
          [value, delay] // Only re-call effect if value or delay changes
        );
      
        return debouncedValue;
      }

    useEffect(() => {

        if (debouncedSearchTerm) {
            
            updateQuery(debouncedSearchTerm);
          } else {
            setSearchResults(null);
          }
    }, [user, debouncedSearchTerm])

    return (
        <ContentContainer style={{boxShadow: '0 0px 0px 0 #000000', position: 'fixed', zIndex: 1000}}  backgroundColor='#00121A' borderRadius='0px' width='100%'>
            <Section style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50px'}} width='1200px'>
                <SearchInput height={'30px'} margin='0px 20px 0px 0px' isSearching={isSearching} selectResult={selectResult} onChange={searchChange} onClear={clearSearch} results={searchResults} />
            </Section>
        </ContentContainer>
    )
}

function mapStateToProps(state) {
    return {
        user: state.user.data,
        notifs: state.notifs
    }
}

export default connect(mapStateToProps, { logout, fetchNotifs, updateNotifs })(TopNav);

Tl;DR

Using react-router for site navigation. Doesn't update component if navigating from /teams/111111 to /teams/222222 but does update if navigating from /users/111111 to /teams/222222.

Any and all help appreciated!


Solution

  • When a URL's path changes, the current Component is unmounted and the new component pointed by the new URL is mounted. However, when a URL's param changes, since the old and new URL path points to the same component, no unmount-remount takes place; only the already mounted component receives new props. One can make use of these new props to fetch new data and render updated UI.

    Suppose your param id is parameter.

    1. With hooks:

      useEffect(() => {
          // ... write code to get new data using new prop, also update your state
      }, [props.match.params.parameter]);
      
    2. With class components:

      componentDidUpdate(prevProps){
          if(this.props.match.params.parameter!== prevProps.match.params.parameter){
              // ... write code to get new data using new prop, also update your state
          }
      }
      
    3. Use KEY:

      Another approach could be to use the unique key prop. Passing a new key will force a component to remount.

      <Route path="/teams/:parameter" render={(props) => (
          <Team key={props.match.params.parameter} {...props} />
      )} />