DEV Community

SeongKuk Han
SeongKuk Han

Posted on • Updated on

React TS: Use Query String Instead of useState

Use Query String Instead of useState

Although I titled this post as "Use Query String Instead of useState", you don't need to use query string in all cases. I provided an example where you may find the usage of query string useful.


Example

main page

category selection

search items

I will implement the example using both useState and query string.

To focus on the differences between useState and query string later, I will first provide a general explanation of the example code.


Project File Structure

Project File Structure

There are two folders in src. components and hooks.


hooks

In the hooks folder, there are two files.

useAttractions.ts

This API hook is used to fetch data from the server using react-query library. For the server, json-server is utilized.

import { useQuery } from 'react-query';

export type COUNTRY_CODE = 'de' | 'kr';
export type COUNTRY_CODE_FILTER = 'all' | COUNTRY_CODE;

const useAttractions = ({
  country,
  keyword,
}: {
  country: COUNTRY_CODE_FILTER;
  keyword: string;
}) => {
  return useQuery<
    {
      id: number;
      country: string;
      title: string;
      url: string;
    }[]
  >({
    queryKey: ['useAttractions', country, keyword],
    queryFn: async () => {
      const params: {
        title_like: string;
        country?: COUNTRY_CODE;
      } = {
        title_like: keyword,
      };
      if (country !== 'all') params.country = country;

      const res = await fetch(
        `http://localhost:3000/attractions?${new URLSearchParams(params)}`
      );
      return res.json();
    },
    useErrorBoundary: true,
  });
};

export default useAttractions;
Enter fullscreen mode Exit fullscreen mode

useSearchParam.ts

This custom hook is used to extract a specific data from the query string. This is my own implementation, and, there are many good libraries available for manipulating query string that you can use instead.

import { useSearchParams } from 'react-router-dom';

const useSearchParam = <T extends string>({
  key,
  defaultValue,
}: {
  key: string;
  defaultValue?: T;
}): [T, (value: T) => void] => {
  const [searchParams, setSearchParams] = useSearchParams();

  const setter = (value: T) => {
    const newParams = new URLSearchParams(searchParams.toString());
    newParams.set(key, value as string);
    setSearchParams(newParams);
  };

  return [(searchParams.get(key) || defaultValue || '') as T, setter];
};

export default useSearchParam;
Enter fullscreen mode Exit fullscreen mode

query string is essentially a string, so the generic type extends string. In this code, version 6 of react-router-dom is being used, and the useSearchParams hook provided by the library is utilized.


components

In the components folder, let's see the two files SearchPage.tsx and AttractionItem.tsx.

AttractionItem.tsx

interface AttractionProps {
  url: string;
  country: string;
  title: string;
}

const AttractionItem = ({ url, country, title }: AttractionProps) => {
  return (
    <div className="search-page-result-card">
      <img src={url} alt="attraction image" />
      <div>
        <h5>{country}</h5>
        <span>{title}</span>
      </div>
    </div>
  );
};

export default AttractionItem;
Enter fullscreen mode Exit fullscreen mode

It takes three parameters, url, country, and title, and renders the item.

SearchPage.tsx

import { useRef, useState } from 'react';
import useAttractions, { COUNTRY_CODE_FILTER } from '../hooks/useAttractions';
import AttractionItem from './AttractionItem';
import './SearchPage.css';

const SearchPage = () => {
  const [country, setCountry] = useState<COUNTRY_CODE_FILTER>('all');
  const [keyword, setKeyword] = useState<string>('');

  const [countryOptions] = useState<
    {
      label: string;
      country: COUNTRY_CODE_FILTER;
    }[]
  >([
    { label: 'ALL', country: 'all' },
    { label: 'South Korea', country: 'kr' },
    { label: 'Germany', country: 'de' },
  ]);
  const inputRef = useRef<HTMLInputElement>(null);

  const { data } = useAttractions({
    country,
    keyword,
  });

  const search = () => {
    if (!inputRef.current) {
      console.error('inputRef.current is not defined', inputRef);
      return;
    }

    setKeyword(inputRef.current.value);
  };

  const handleCountryChange = (country: COUNTRY_CODE_FILTER) => () => {
    setCountry(country);
  };

  const handleEnter =
    (callback: VoidFunction): React.KeyboardEventHandler<HTMLInputElement> =>
    (e) => {
      if (e.code === 'Enter') callback();
    };

  return (
    <div className="search-page">
      <h1>Attractions</h1>
      <div className="search-page-tool">
        <input
          type="text"
          ref={inputRef}
          onKeyUp={handleEnter(search)}
          defaultValue={keyword}
        />
        <button type="button" onClick={search}>
          Search
        </button>
      </div>
      <ul className="search-page-filters">
        {countryOptions.map((option) => (
          <li
            key={option.country}
            className={country === option.country ? 'active' : ''}
            onClick={handleCountryChange(option.country)}
          >
            {option.label}
          </li>
        ))}
      </ul>
      <div className="search-page-result">
        {data?.map((d) => (
          <AttractionItem key={d.id} {...d} />
        ))}
      </div>
    </div>
  );
};

export default SearchPage;
Enter fullscreen mode Exit fullscreen mode

This is the main page component. You can search a keyword by pressing Enter.


useState

...
  const [country, setCountry] = useState<COUNTRY_CODE_FILTER>('all');
  const [keyword, setKeyword] = useState<string>('');
...
Enter fullscreen mode Exit fullscreen mode

example using useState, it works well

As you cab see, it works well. However, if you refresh the page, the data will disappear as shown below.

after refreshing, all data has gone

Every time, you enter this page, it will be empty.

Suppose you want to share the page with your friends. If you use query string, you can share the page URL with them.


query string

import { useRef, useState } from 'react';
import useAttractions, { COUNTRY_CODE_FILTER } from '../hooks/useAttractions';
import useSearchParam from '../hooks/useSearchParam';
import AttractionItem from './AttractionItem';
import './SearchPage.css';

const SearchPage = () => {
  const [country, setCountry] = useSearchParam<COUNTRY_CODE_FILTER>({
    key: 'country',
    defaultValue: 'all',
  });
  const [keyword, setKeyword] = useSearchParam<string>({
    key: 'keyword',
    defaultValue: '',
  });

  const [countryOptions] = useState<
    {
      label: string;
      country: COUNTRY_CODE_FILTER;
    }[]
  >([
    { label: 'ALL', country: 'all' },
    { label: 'South Korea', country: 'kr' },
    { label: 'Germany', country: 'de' },
  ]);
  const inputRef = useRef<HTMLInputElement>(null);

  const { data } = useAttractions({
    country,
    keyword,
  });

  const search = () => {
    if (!inputRef.current) {
      console.error('inputRef.current is not defined', inputRef);
      return;
    }

    setKeyword(inputRef.current.value);
  };

  const handleCountryChange = (country: COUNTRY_CODE_FILTER) => () => {
    setCountry(country);
  };

  const handleEnter =
    (callback: VoidFunction): React.KeyboardEventHandler<HTMLInputElement> =>
    (e) => {
      if (e.code === 'Enter') callback();
    };

  return (
    <div className="search-page">
      <h1>Attractions</h1>
      <div className="search-page-tool">
        <input
          type="text"
          ref={inputRef}
          onKeyUp={handleEnter(search)}
          defaultValue={keyword}
        />
        <button type="button" onClick={search}>
          Search
        </button>
      </div>
      <ul className="search-page-filters">
        {countryOptions.map((option) => (
          <li
            key={option.country}
            className={country === option.country ? 'active' : ''}
            onClick={handleCountryChange(option.country)}
          >
            {option.label}
          </li>
        ))}
      </ul>
      <div className="search-page-result">
        {data?.map((d) => (
          <AttractionItem key={d.id} {...d} />
        ))}
      </div>
    </div>
  );
};

export default SearchPage;
Enter fullscreen mode Exit fullscreen mode

I have switched from useState to useSearchParam, which is a custom hook I made. You can use your own way, but in this example, we will focus on the fact we are using query string for the data keyword and category values.

If you search for a keyword, the url will be updated.

keyword is changed by searching

When you click a category, the url will be updated.

category selected

After refreshing the page, it will remain the same.

The looking of the page after refreshing

If you share this URL with your friends and they open it, they will see the same page. Additionally, we can modify the URL as we want.

In the URL http://localhost:5173/?keyword=Ber&country=de, let's change the country from de to kr and the keyword from Ber to han.

http://localhost:5173/?keyword=han&country=kr

Let's open the link. (Make sure to start the dev server on your own on port 5173)

You will see the following page. (Oh, I found a typo, it should be 'Hongdae' instead of 'Hangdae', but it's not important, let's move on.)

opened link


Conclusion

By using query string, you can get following benefits:

  • Provide different pages by URL.
  • If your pages are prepared for SEO, it can provide better SEO.

However, you should avoid using query string for

  • Sensitive information
  • Long data

Long data can make your URL much long, which can't look good from users. you can use local/session storages and cookies to store some data temporarily or permanently. Use query string for the data you want to expose via URL.

That's it! I hope you find it useful.

Happy Coding!


Github Source

Latest comments (0)