Small React Tip – Customisable Filter Panel Component

viscoze

Vlad Oganov

Posted on January 3, 2021

Small React Tip – Customisable Filter Panel Component

We're working on an application that's basically a number of tables. Of course for making life of our customers better we wanted to add an ability to filter data in these tables.

Depending on a kind of data tables could be filtered by the date, price, name, or an id of an item in the system. Different table had different set of column, hence could have different filters.

We wanted to have a reusable and customisable solution, that holds the logic of keeping state locally, and give as an ability of adding a new type of a filter field.

We could go with a straight forward solution like the following:

function FilterPanel(props) {
  ...

  return pug`
    if props.hasDate
      FieldDate(
        value=...
        onChange=...
      )

    if props.hasAmount
      FieldAmount(
        value=...
        onChange=...
      )

    ...
  `
}
Enter fullscreen mode Exit fullscreen mode

And as you can see here we just control presence of fields by flags like hasDate, hasAmount, which is not flexible in a case we want to change the order of the fields. Then we decided to separate fields and the panel.

The first step to find a better solution was to draft its interface to outline the way we want to use it. We came up with the following:

FilterPanel(
  values={}
  onApply=(() => {})
)
  FieldGroup
    FieldDate(
      name="dateMin"
    )    

    FieldDate(
      name="dateMax"
    )

  FieldGroup
    FieldAmount(
      name="amountMin"
    )    

    FieldAmount(
      name="amountMax"
    )
Enter fullscreen mode Exit fullscreen mode

As you can see here we have an ability to configure the panel depending on what table we're going to use it with.

To share the logic between these fields and make it flexible in a case we want to group the fields we used React Context.

If it looks new for you, I highly recommend to read the official docs first.

We create the following folder structure for this component:

FilterPanel/
  Context/
  FieldDate/
  FieldAmount/
  FieldName/
  atoms.common.js <--- common styled components
  atoms.js
  index.js
Enter fullscreen mode Exit fullscreen mode

Let's start with the Context module:

import { createContext, useContext } from 'react'

const Context = createContext({
  getValue: () => null,
  setValue: () => {},
})
Context.displayName = 'FilterPanelContext'

export const Provider = Context.Provider

export function useFilterPanelContext() {
  return useContext(Context)
}
Enter fullscreen mode Exit fullscreen mode

This it our interface to work with the context instance: the Provider component and useFilterPanelContext.

The state holding went to the FilterPanel component:

function FilterPanel(props) {
  const [values, setValues] = useState(props.values)
  const [wasChanged, setWasChanged] = useState(false)

  const isApplied = !_.isEmpty(props.values)

  function getValue(name) {
    return values[name]
  }

  function setValue(name, value) {
    setWasChanged(true)
    setValues({ ...values, [name]: value })
  }

  function clearValues() {
    setWasChanged(false)
    setValues({})
    props.onApply({})
  }

  function submitValues(event) {
    event.preventDefault()
    setWasChanged(false)
    props.onApply(values)
  }

  const formLogic = {
    getValue,
    setValue,
  }

  return pug`
    form(onSubmit=submitValues)
      Provider(value=formLogic)
        Wrapper
          each child in Children.toArray(props.children)
            Box(mr=1.5)
              = child

          Box(mr=1.2)
            if isApplied && !wasChanged
              Button(
                type="button"
                variant="outlined"
                size="medium"
                onClick=clearValues
              ) Clear

            else
              Button(
                type="submit"
                variant="outlined"
                size="medium"
              ) Filter
  `
}
Enter fullscreen mode Exit fullscreen mode

A code is the best documentation. And if there are some places you'd like to know more about, here is some explanations.

Why do we hold state locally? We want not to apply this filters right after they changed — only by click on the "Filter" button.

Why do we track wasChanged? We want to know if user has changed a value of a field, so we show the "Filter" button again instead of the "Clear" one.

How does Provider help us? Data that were passed as the value props are now available in all components that use the useFilterPanelContext hook.

What the purpose of Children.toArray(props.children)? It's a way to render the children and to apply some additional logic. Here we wrap each child into Box — a component that adds margin right.

And the last but not least — a field component. We will take the amount one as an example. Here it is:

function FilterPanelFieldAmount(props) {
  const { getValue, setValue } = useFilterPanelContext() <---- our hook

  const handleChange = event => setValue(event.target.name, event.target.value)
  const handleClear = () => setValue(props.name, '')

  const value = getValue(props.name)

  const Icon = pug`
    if value
      IconButton(
        variant="icon"
        size="small"
        type="button"
        onClick=handleClear
      )
        Icons.TimesCircle

    else
      IconLabel(for=props.name)
        Icons.AmountFilter
  `

  return pug`
    FieldText(
      size="medium"
      id=props.name
      name=props.name
      value=value
      placeholder=props.placeholder
      onChange=handleChange
      endAdornment=Icon
    )
  `
}
Enter fullscreen mode Exit fullscreen mode

And that's it! It's a really nice practice to make something customisable via React Context. I hope it was useful, and let me know if there something I missed.

Cheers!

💖 💪 🙅 🚩
viscoze
Vlad Oganov

Posted on January 3, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related