How to use useSearchParams Hook with React Router v6
Asked Answered
C

1

13

I am trying to implement a search parameter functionality to my React image search app. And, I have learned that I need to (can) use the useSearchParams Hook, but I am not sure how to make these changes.

So, basically I want the URL to be something like localhost:3000/input&page=1, meaning that whatever comes after the slash is going to be the input value and key/value pair for page numbers.

As you can see in the app.js, I have these 3 main Routes and the Home Route (renders Main.js) is the one I am mainly working on. Also, Main.js renders Header.js (renders form and others).

I am thinking that I should create a new Route in the app.js but I am not sure what to do.

import './App.css';
import Home from './components/pages/Home';
import Favorites from './components/pages/Favorites';
import Error from './components/pages/Error';
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { SkeletonTheme } from 'react-loading-skeleton';
import { useDarkMode } from './components/Navbar';


function App() {
  const darkMode = useDarkMode(state => state.darkMode)
  let style
  if (darkMode === 'light') {
    style = 'wrapper'
  } else {
    style = 'wrapper-dark'
  }

  return (
    <div className={style}>
      <SkeletonTheme baseColor="#808080" highlightColor="#b1b1b1">
        <BrowserRouter>
          <Routes>
            <Route path='/' element={<Home />} />
            <Route path='favorites' element={<Favorites />} />
            <Route path='*' element={<Error />} />
          </Routes>
        </BrowserRouter>
      </SkeletonTheme>
    </div>
  );
}

export default App;
import React from 'react'
import Header from './Header'
import Image from './Image'
import { useState, useEffect, useRef } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'
// import InfiniteScroll from 'react-infinite-scroll-component'


function Main() {
  const [input, setInput] = useState('')
  const [allImages, setAllImages] = useState([])
  // const [totalResults, setTotalResults] = useState(null)
  const [isVisible, setIsVisible] = useState(false)
  const [error, setError] = useState(null)
  const [showError, setShowError] = useState(false)
  const [fadeOut, setFadeOut] = useState(false)
  const [page, setPage] = useState(1)
  const paginationRef = useRef(false)

  // get
  useEffect(() => {
    if (localStorage.getItem('input')) {
      setInput(JSON.parse(localStorage.getItem('input')))
    }

    if (localStorage.getItem('allImages')) {
      setAllImages(JSON.parse(localStorage.getItem('allImages')))
      // setTotalResults(JSON.parse(localStorage.getItem('totalResults')))
      setIsVisible(JSON.parse(localStorage.getItem('isVisible')))
      setPage(JSON.parse(localStorage.getItem('page')))
      paginationRef.current = true
    }
  }, [])

  // set
  //* dryer?
  useEffect(() => {
    localStorage.setItem('input', JSON.stringify(input))
  }, [input])

  useEffect(() => {
    localStorage.setItem('allImages', JSON.stringify(allImages))
  }, [allImages])

  // useEffect(() => {
  //   localStorage.setItem('totalResults', JSON.stringify(totalResults))
  // }, [totalResults])

  useEffect(() => {
    localStorage.setItem('isVisible', JSON.stringify(isVisible))
  }, [isVisible])

  function handleChange(event) {
    setInput(event.target.value)
  }

  // display nothing by default
  // display image-list when user press search button

  // function handleSubmit(event) {
  //   event.preventDefault()
  //   // interpolate input state and .env variable to API
  //   fetch(`https://api.unsplash.com/search/photos?query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
  //     .then(res => res.json())
  //     .then(data => setAllImages(data.results))
  // }

  async function fetchImages() {
    try {
      const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
      const data = await res.json()
      if (data.total !== 0) {
        setAllImages(data.results)
        // setTotalResults(data.total)
        setIsVisible(true)
      }
    } catch(error) {
      setError(error)
    }
  }

  const handleSubmit = async (event) => {
    event.preventDefault();
    fetchImages()
    setPage(1)
    paginationRef.current = true
  }

  // error
  useEffect(() => {
    if (error) {
      setShowError(true)
      setTimeout(() => {
        setFadeOut(true)
        setTimeout(() => {
          setShowError(false)
        }, 1000)
      }, 5000)
    }
  }, [error])

  // total results
  // let results
  // if (totalResults >= 10000) {
  //   results = 'Total Results: ' + totalResults + '+'
  // } else if (totalResults > 0) {
  //   results = 'Total Results: ' + totalResults
  // } else if (totalResults === 0) {
  //   results = 'Nothing Found'
  // }

  // pagination
  useEffect(() => {
    if (paginationRef.current) {
      fetchImages()
    }
    localStorage.setItem('page', JSON.stringify(page))
  }, [page])

  function handlePrev() {
    setPage(prevState => prevState - 1)
    fetchImages()
  }
  function handleNext() {
    setPage(prevState => prevState + 1)
    fetchImages()
  }


  return (
    <main>
      <Header
        input={input}
        handleChange={handleChange}
        handleSubmit={handleSubmit}
      />

      {showError && <div className={`network-error ${fadeOut ? 'fade-out' : ''}`}>
        <i><FontAwesomeIcon icon={faTriangleExclamation} /></i>
        <div className='network-error--message'>
          <h5>Network Error</h5>
          <p>Please check your Internet connection and try again</p>
        </div>
      </div>}

      {/* <p className='main--results'>{results}</p> */}
      <div className='main--image-list mt-5 pb-5'>
        {allImages.map(el => (
          <Image
            key={el.id}
            // do need spread operator below for img's src to work in Image.js
            {...el}
            el={el}
          />
        ))}
      </div>

      {isVisible && <div className='main--pagination'>
        <button disabled={page === 1} onClick={handlePrev}>
          Prev
        </button>
        <h5 className='main--pagination--h5'>{page}</h5>
        <button onClick={handleNext}>
          Next
        </button>
      </div>}
    </main>
  )
}

export default Main
import React from 'react'
import Navbar from './Navbar'


function Header(props) {
  return (
    <div className='header'>
      <Navbar />
      <h2 className='header--heading text-center text-light'>Find Images</h2>
      <div className='header--form'>
        <form onSubmit={props.handleSubmit}>
          <input
            className='header--form--input'
            autoComplete='off'
            type='text'
            placeholder='Search'
            onChange={props.handleChange}
            name='input'
            value={props.input}
          />
        </form>
      </div>
    </div>
  )
}

export default Header
Circassia answered 23/1, 2023 at 20:42 Comment(1)
useSearchParams is all query after ?. what you want is possible useParams for defining a variable in the RouteVerity
B
11

If you are just wanting to initialize the page state to the page queryParam the the following could work. If uses the useSearchParams to access the queryString and return a constructed URLSearchParams object which can then access individual query params. Pass the "page" query param as the initial page state value.

const [searchParams] = useSearchParams();

const [page, setPage] = useState(Number(searchParams.get("page")) || 1);

In all likelihood though you'll not want competing "sources of truth" for what the current page is. If you want the URL queryString to be the source of truth then remove the page state and just read/update the "page` query parameter directly.

Example:

function Main() {
  const [searchParams, setSearchParams] = useSearchParams();

  ...
 
  const page = Number(searchParams.get("page"));

  // get
  useEffect(() => {
    ...

    if (localStorage.getItem('allImages')) {
      ...
      setSearchParams(params => {
        params.set("page", JSON.parse(localStorage.getItem('page')) || 1);
        return params;
      });
      ...
    }
  }, []);

  ...

  const handleSubmit = async (event) => {
    event.preventDefault();
    ...
    setSearchParams(params => {
      params.set("page", 1);
      return params;
    });
    ...
  }

  ...

  // pagination
  useEffect(() => {
    if (paginationRef.current) {
      fetchImages();
    }
    localStorage.setItem('page', JSON.stringify(page));
  }, [page])

  function handlePrev() {
    setSearchParams(params => {
      params.set("page", Math.max(1, page - 1));
      return params;
    });
    ...
  }

  function handleNext() {
    setSearchParams(params => {
      params.set("page", page + 1);
      return params;
    });
    ...
  }

  return (
    ...
  );
}
Birthwort answered 23/1, 2023 at 20:51 Comment(1)
I figured that I had to add another search parameter for 'query' along with this page parameter, but thank you so much!Circassia

© 2022 - 2024 — McMap. All rights reserved.