Using query params as state management tool in React

Ruslan Elishaev
By
Ruslan Elishaev
,
Cover Image for Using query params as state management tool in React

Summary

  • Use query params as state manager in react app
  • Implement useQueryParams hook as state manager for e-commerce products filters
  • Share the stateful app link

useQueryParams is not a replacement for redux, recoil or context

Query params are an excellent addition to existing state management tools. With useQueryParams we are not only managing the state, but we can share the stateful link with our friends.

Shareable link

Let's say we have an e-commerce app. That app has a complex filter and uses those inputs to filter and sort out the products we want. Next, we want to share those filtered products with somebody else. With the usual state management tools, it's not possible. The good old query params provide us the possibility to save some values into a URL and share that URL.

useQueryParams hook to the rescue

With the help of useQueryParams hook, we can leverage query params to force re-render in react app.

  1. Change some filter input
  2. URL is changed instantly with the help of History API
  3. The app is listening to changes in the URL and re-renders accordingly to the modification

Getting started with use-query-params

install with yarn:

yarn add use-query-params

install with npm:

npm install use-query-params

Wrap the app with QueryParamProvider

// index.tsx:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import reportWebVitals from './reportWebVitals'
import { ChakraProvider } from '@chakra-ui/react'
import { QueryParamProvider } from 'use-query-params'
import { createBrowserHistory } from 'history'

export const history = createBrowserHistory()

ReactDOM.render(
  <React.StrictMode>
    <ChakraProvider>
      <QueryParamProvider history={history as any}>
        <App />
      </QueryParamProvider>
    </ChakraProvider>
  </React.StrictMode>,
  document.getElementById('root')
)

Note that in this example we are not going to use a routing library. If your app is wrapped with a router, check the docs here:
https://github.com/pbeshai/use-query-params/tree/master/packages/use-query-params#readme


Usage in App.tsx

  • Listen for changes to the URL and force the app to re-render
  • Fetch products from external API and set returned data to local state
  • Consume data in <ItemsList />

// App.tsx
import { useEffect, useState, useReducer } from 'react'
import { Box } from '@chakra-ui/react'
import ItemsList from './components/items-list'
import Filters from './components/filters'
import { history } from './index'

const API_URL = 'https://fakestoreapi.com/products'

export interface ItemProps {
  title: string
  id: number
  image: string
  category: string
  price: string
}

function App() {
  const [rawProducts, setRawProducts] = useState<ItemProps[]>([])
  const [categories] = useState([
    `men's clothing`,
    'jewelery',
    'electronics',
    `women's clothing`,
  ])

  const [, forceUpdate] = useReducer((x) => x + 1, 0)
  useEffect(() => {
    history.listen(() => {
      forceUpdate()
    })
  }, [])

  const getProducts = async () => {
    const res = await fetch(API_URL)
    const json = await res.json()
    setRawProducts(json)
  }

  useEffect(() => {
    getProducts()
  }, [])

  return (
    <Box className="App" maxWidth="800px" mx="auto" px={4}>
      <Box as="header" textAlign="center" py={7}>
        <Box as="h1" fontSize="1.5rem">
          Query params state management
        </Box>
      </Box>

      <Box>
        <Box fontSize="2rem" textAlign="center">
          Products List
        </Box>
        <Filters categories={categories} />
        <ItemsList rawProducts={rawProducts} categories={categories} />
      </Box>
    </Box>
  )
}

export default App


Create <ItemsList /> component

// components/items-list.tsx
import { useEffect, useState } from 'react'
import { Badge, Box, Grid, Image } from '@chakra-ui/react'
import { ItemProps } from '../App'
import { StringParam, useQueryParam } from 'use-query-params'

interface Props {
  rawProducts: ItemProps[]
  categories: string[]
}
const ItemsList = ({ rawProducts, categories }: Props) => {
  const [categoryParam] = useQueryParam('filterByCategory', StringParam)

  const [filteredProducts, setFilteredProducts] = useState(rawProducts)

  const isFiltered = categoryParam && categories.includes(categoryParam)

  const filterByCategory = (p: ItemProps[], cat: string) => {
    return p.filter((item) => item.category === cat)
  }

  useEffect(() => {
    if (isFiltered) {
      setFilteredProducts(filterByCategory(rawProducts, categoryParam))
    } else {
      setFilteredProducts(rawProducts)
    }
  }, [rawProducts, categoryParam, isFiltered])

  return (
    <Box>
      <Grid templateColumns="repeat(3, 1fr)" gap={10}>
        {filteredProducts.length &&
          filteredProducts.map((item) => (
            <Box
              key={item.id}
              boxShadow="lg"
              borderRadius={5}
              overflow="hidden"
              position="relative"
              padding={5}
            >
              <Image
                src={item.image}
                boxSize="250px"
                objectFit="cover"
                mb={4}
              />
              <Box fontSize="sm">{item.title}</Box>
              <Badge>{item.category}</Badge>
            </Box>
          ))}
      </Grid>
    </Box>
  )
}

export default ItemsList


Create <Filters /> component

// components/filters.tsx
import React from 'react'
import { Box, Flex, Select } from '@chakra-ui/react'
import { StringParam, useQueryParam } from 'use-query-params'

interface Props {
  categories: string[]
}
const Filters = ({ categories }: Props) => {
  const [categoryParam, setCategoryParam] = useQueryParam(
    'filterByCategory',
    StringParam
  )

  return (
    <Flex my={4} alignItems="center">
      Filter by category{' '}
      <Box mx={2}>
        <Select
          placeholder="Select option"
          onChange={(e) => setCategoryParam(e.target.value)}
          defaultValue={categoryParam ? categoryParam : ''}
        >
          {categories.map((cat) => (
            <option key={cat} value={cat}>
              {cat}
            </option>
          ))}
        </Select>
      </Box>
    </Flex>
  )
}

export default Filters

The outcome

Now we are able to change the values in the select box and see that the products filtered out, and the URL params getting the values of the filter.

  • Change the category in select box to "electronics".
  • The URL should change to: https://localhost:3000/?filterByCategory=electronics
  • Refresh the page.
  • The products should be filtered by "electronics" category

Codesandbox example

https://codesandbox.io/s/new-darkness-gwlwf?file=/src/App.tsx

Resources

https://github.com/pbeshai/use-query-params

More Posts To Read



HomeAboutMy ProjectsFavorite Tools
© creotip.io