Search code examples
reactjsreact-router-dom

Why location.state gets undefined when the page reloads?


I'm working on a project with react and i'm doing a simple history.push() just to navigate to another component. Beside navigating to the other component i'm passion some data by using history.push({state: []}).

To access the data to the other component I use useLocation() and access it with location.state. I switch the language on the app (the language logic is set with setLocalStorage()) and suddenly the location.state gets undefined. I read in some articles that location.state gets undefined on re render. I didn't find something clear that specify that, not even on the react-router-dom documentation. Also when I reload the page the location.state doesn't become undefined.

My question is why does it happen when the page is re render.? Is it the best practice to pass the data like this? If not which is the best practice to pass it to another component so these errors can be avoid?

LanguageDropdown

import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Dropdown } from 'react-bootstrap';
import classNames from 'classnames';

import enFlag from './flags/uk.jpg';
import italyFlag from './flags/italy.jpg';
import { useTranslation } from 'react-i18next';

// get the languages
const Languages = [
  {
    name: 'English',
    flag: enFlag,
    short_name: 'en'
  },
  {
    name: 'Italian',
    flag: italyFlag,
    short_name: 'it'
  }
];

const LanguageDropdown = () => {
  const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
  const [selectedLanguage, setSelectedLanguage] = useState(
    localStorage.getItem('defaultLang') !== null
      ? Languages.find(function (item: { name: string; flag: string; short_name: string }) {
          return item.short_name == localStorage.getItem('defaultLang');
        })
      : Languages[0]
  );

  const [t, i18n] = useTranslation('common');
  /*
   * toggle language-dropdown
   */
  const toggleDropdown = () => {
    setDropdownOpen(!dropdownOpen);
  };

  const changeLanguage = async (lang: any) => {
    i18n.changeLanguage(lang.short_name);
    setSelectedLanguage(lang);
    localStorage.setItem('defaultLang', lang.short_name);
  };

  return (
    <Dropdown show={dropdownOpen} onToggle={toggleDropdown}>
      <Dropdown.Toggle
        id='dropdown-languages'
        as='a'
        onClick={toggleDropdown}
        className={classNames('nav-link waves-effect waves-light', {
          show: dropdownOpen
        })}>
         <img src={selectedLanguage.flag} alt={selectedLanguage.name} height='16' />
      </Dropdown.Toggle>
      <Dropdown.Menu className='dropdown-menu dropdown-menu-end'>
        <div onClick={toggleDropdown}>
          {(Languages || []).map((lang, i) => {
            return (
              <Link
                to='#'
                onClick={() => changeLanguage(lang)}
                className='dropdown-item notify-item'
                key={i + '-lang'}>
                <img src={lang.flag} alt={lang.name} className='me-1' height='12' />
                <span className='align-middle'>{lang.name}</span>
              </Link>
            );
          })}
        </div>
      </Dropdown.Menu>
    </Dropdown>
  );
};

export default LanguageDropdown;


Solution

  • Issue

    The issue appears to be the link targets used in the language selection dropdowns. They issue a navigation action to "/#" sans any existing state. This is the route state being wiped out.

    Solution

    Instead of navigating to "/#" you may just navigate to the current location

    const LanguageDropdown = () => {
      const location = useLocation(); // <-- current location, including any state
    
      ...
    
      return (
        <Dropdown show={dropdownOpen} onToggle={toggleDropdown}>
          ...
          <Dropdown.Menu className='dropdown-menu dropdown-menu-end'>
            <div onClick={toggleDropdown}>
              {(Languages || []).map((lang, i) => {
                return (
                  <Link
                    to={location} // <-- current location, including any state
                    onClick={() => changeLanguage(lang)}
                    className='dropdown-item notify-item'
                    key={i + '-lang'}
                  >
                    ...
                  </Link>
                );
              })}
            </div>
          </Dropdown.Menu>
        </Dropdown>
      );
    };
    
    export default LanguageDropdown;
    

    If you are effectively staying on the current page though, you could also just omit the to prop and cancel the navigation action.

    const LanguageDropdown = () => {
      ...
    
      return (
        <Dropdown show={dropdownOpen} onToggle={toggleDropdown}>
          ...
          <Dropdown.Menu className='dropdown-menu dropdown-menu-end'>
            <div onClick={toggleDropdown}>
              {(Languages || []).map((lang, i) => {
                return (
                  <Link
                    onClick={(e) => {
                      e.preventDefault(); // <-- prevent navigation action
                      changeLanguage(lang);
                    }}
                    className='dropdown-item notify-item'
                    key={i + '-lang'}
                  >
                    ...
                  </Link>
                );
              })}
            </div>
          </Dropdown.Menu>
        </Dropdown>
      );
    };
    
    export default LanguageDropdown;
    

    Demo

    Edit why-location-state-gets-undefined-when-the-page-reloads