Search code examples
javascriptreactjsreact-router-dom

How to properly persist props between functional components in React?


I have one functional component named 'Shop', which decides which apidatabase to use depending on the type of shop (e.g shopAPI_1, shopAPI_2, etc) as follows:

  useEffect(() => {
    if (shopname === 'shopName_1') {
      setSelectedAPI('shopApiName_1');
    } else if (shopname === 'shopName_2') {
      setSelectedAPI('shopApiName_2');
    } else if (shopname === 'shopName_3') {
      setSelectedAPI('shopApiName_3');
    } else {
      setSelectedAPI('null');
    }
  }, [shopname]);

Then, through react-router-dom I send this selectedAPIto another functional component as follows:

  <div>
      <Link to='/'>BACK</Link>
      <div className={styles.wrapper}>
        <div className={styles.title}>Search your product at {shopname}</div>
        <div className={styles.searchBar}>
          <SearchBar apidatabase={selectedAPI} />
        </div>
      </div>
    </div>

This SearchBar component renders a set products, which can be clicked upon and guide you to aother page, which shows data related to the product as follows:

        productData
          .filter((item) => !selectedCategory || item.Category === selectedCategory)
          .map((item) => (
            <Link
                className={styles.searchBarLink}
                to={`/metrics/${item.id}`}
                state={{ itemData: item }}
                key={item.id}
              >
              <ProductBox
                name={item.Name}
                price={item.Price}
              />
            </Link>

Issue: At first try, this works as expected, but when I access the product's metrics page and then return back to the product listing page, the selectedAPI sent as a prop is lost and set to null.

Although I am pretty sure that there are better implementations to this approach, I would like to know how to persist the selected API when I return from the metrics page into the product listing page.

The only workaround for this bug that I found, is to go back to the shop selection menu and search the product again.

Notes to understand the issue that I am facing

  • For the backend I am using Supabase and doing SQL queries to the database
  • Every shop has its own database, thus the need for different 'databaseapi'.

EDIT (Added a minimal reproducible example):

The Shop function takes from the URL: http://IP:3000/shop/shopName_1 the shopName_1 which is then evaluated and used to select the correct API to use.

function Shop() {
  // Access the shopname parameter using useParams
  const { shopname } = useParams();
  const [selectedAPI, setSelectedAPI] = useState('');

  useEffect(() => {
    if (shopname === 'shopName_1') {
      setSelectedAPI('shopApiName_1');
    } else if (shopname === 'shopName_2') {
      setSelectedAPI('shopApiName_2');
    } else if (shopname === 'shopName_3') {
      setSelectedAPI('shopApiName_3');
    } else {
      setSelectedAPI('null');
    }
  }, [shopname]);

  return (
    <div>
      <Link to='/'>BACK</Link>
      <div className={styles.wrapper}>
        <div className={styles.title}>Search your product at {shopname}</div>
        <div className={styles.searchBar}>
          <SearchBar apidatabase={selectedAPI} />
        </div>
      </div>
    </div>
  );
}

As you may observe, I am using passing this information down to another component named SearchBar as props with the name apidatabase. As you may observe below, this apidatabase value is used for fetching data from a backendservice (this works correctly) and the output from this backend service is then shown to the user in a ProductBox which is wrapped by a Link that routes the user to a metrics page of that specific product.

export default function SearchBar( {apidatabase} ) {
  const [searchInput, setSearchInput] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('');
  const [productData, setProductData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [categories, setCategories] = useState([]);

  const error = false;

  useEffect(() => {

    async function fetchCategories() {
      try {
        const productCategories = await backendService.getCategories();
        setCategories(productCategories)
      } catch (error) {
        //TODO: Add better error handling
      }
    }

    fetchCategories();
  }, [])

  const handleChange = async (e) => {
    const inputValue = e.target.value;
    
    
    setSearchInput(inputValue);
    setLoading(true);
      
      try {
        const output = await backendService.findProductByName(inputValue, apidatabase);
        setProductData(output);
        setLoading(false);
      } catch (error) {
        //setError(error);
        setLoading(false);
      }
    };
        

   const handleSelectedCategory = (e) => {
    if (e.Category === selectedCategory) {
      setSelectedCategory(''); // Set to an empty string to reset the filterx
    } else {
      setSelectedCategory(e.Category); // Update the selected category
    }    
  };

  return (
    <div>
      <div className={styles.searchInput}>
        {searchInput}<br/>
        {/*selectedAPI*/}
        <input
          type="text"
          placeholder="Name of the product"
          onChange={handleChange}
          value={searchInput}
        />
        <div className={styles.filterTitle}>
            Filter results by: {selectedCategory}
        </div>
        <div className={styles.searchFilter}>
          <div className={styles.buttonWrapper}>
             {categories.map((item, index) => (
                <button 
                  id={`categoryButton${index}`}
                  key={item.id}
                  onClick={() => handleSelectedCategory(item)}
                  className={styles.button}>{item.Category}</button>
              ))}
          </div>
        </div>
      </div>
      <div className={styles.productWrapper}>
      {loading ? (
        <p>Loading...</p>
      ) : error ? (
        <p>Error: {error}</p>
      ) : (
        productData
          .filter((item) => !selectedCategory || item.Category === selectedCategory)
          .map((item) => (
            <Link
                className={styles.searchBarLink}
                to={`/metrics/${item.id}`}
                state={{ itemData: item }}
                key={item.id}
              >
              <ProductBox
                name={item.Name}
                price={item.Price}
              />
            </Link>
        ))
      )}
      </div>
    </div>
  );
  
}

How to reproduce this issue?

Inside the SearchBar function, route to another page, such as /metrics, and then go back to the previous functional component. You may observe that the apidatabase value is lost.

In order to go back to the previous page (functional component) I use a Link from react-router-dom pointing to the SearchBar function.

EDIT 2: I added the routing section of the code if it helps throubleshoot this issue further

function App() {
  return (
    <div className="App">
      <Router>
        <div>
          <Routes>
            <Route path='/' element={<Home />} />
            <Route path='/shop/:shopname' element={<Shop />} />
            <Route path='/metrics/:id' element={<ProductMetrics />} />
          </Routes>
        </div>
      </Router>
      {/*<Footer/>*/}
    </div>
  );
}

Solution

  • Since the SearchBar component that renders the links to the product metrics pages doesn't share any routing paths with the ProductMetrics component my suggestion would be to forward the current shop page as a referrer location that the "BACK" link in ProductMetrics can navigate back to. Pass the current location in link state to the "/metrics/:id" route.

    SearchBar

    Access and forward the current location in route state.

    import { Link, useLocation } from 'react-router-dom';
    
    export default function SearchBar({ apidatabase }) {
      const location = useLocation(); // <-- location
    
      ...
    
      return (
        <div>
          ...
          <div className={styles.productWrapper}>
            <Link
              className={styles.searchBarLink}
              to={`/metrics/${42}`}
              state={{
                itemData: item,
                from: location // <-- pass location in state
              }}
              key={42}
            >
              <ProductBox name={"prodName"} price={42} />
            </Link>
          </div>
        </div>
      );
    }
    

    ProductMetrics

    Access any passed route state and attempt to navigate back to the previous location, e.g. state.from, otherwise navigate to the home page.

    import { Link, useLocation } from 'react-router-dom';
    
    export default function ProductMetrics() {
      const { state } = useLocation(); // <-- access route state
      
      ...
    
      return (
        <div align="center">
          ...
          <div>
            <Link to={state?.from ?? "/"}> // <-- navigate to previous page or home
              BACK
            </Link>
          </div>
        </div>
      );
    }