React Router does not update component if url parameter changes
Asked Answered
C

3

12

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!

Cheese answered 10/7, 2020 at 14:23 Comment(2)
Does your state depends on the props, i.e, in the component for do you use the search id like 111111 or 2222 as a state?Lourielouse
@RohanAgarwal the state for the user/team pages depends on props, yes. The id of the entity is pulled in via componentdidmount / useEffect and then all of the other data is populated. Does that help?Cheese
L
31

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} />
    )} />
    
Lourielouse answered 13/7, 2020 at 16:48 Comment(1)
The key approach was perfect for my use case - thanks! :DFootling
L
6

Re-render does not cause component to re-mount so use useEffect hook to call initializing logic in your component whenever props changes and update your state in the callback.

useEffect(() => {
  //Re initialize your component with new url parameter
}, [props]);
Lorola answered 11/7, 2020 at 21:8 Comment(1)
Ok, I'll try that!Cheese
S
0

In 2024, the render API does not seem to be there anymore, but there is a simple fix that can re-render the component every time the path changes. To do this, I used the useLocation() hook, which contains information about the path, serach parameters and a unique key for a location (which changes every time a route is loaded, even for the same path).

These components can be used to build a key referenced by the <Outlet /> to re-render the component (the <Outlet />, and everything within it) whenever the path changes, without having to monitor the path in each route for updates.

For example, the following will reload the <Outlet /> every time the path changes (including the path parameters, but not the query string parameters):

import React from 'react';
import { Outlet, useLocation } from 'react-router-dom';

export default function MyLayout() {
   const location = useLocation();

   return (
      <div className="myLayout">
         {
            // Your layout code...
         }
         <Outlet key={location.pathname} />
      </div>
   )

}

With the above, the <MyLayout /> component will then render every child route it matches within the , for example:

// ...
import React from 'react';
import MyLayout from './MyLayout';

export default function App() {
   return (
      <BrowserRouter>
         <Routes>
            <Route path="/" element={<MyLayout />}>
               <Route index element={<h1>Index page</h1>} />
               <Route path="teams/*">
                  <Route path=":teamId" element={<h1>Team page that reloads when teamId changes.</h1>} />
               </Route>
            </Route>
         </Routes>
      </BrowserRouter>
   );
}

Hope this helps whoever stumbles upon this after v5/v6 has been released!

Sputnik answered 15/8, 2024 at 13:45 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.