Small React Tip – Customisable Filter Panel Component
Tags: react
Published: 2021-01-03 13:53:01



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:
```jsx
function FilterPanel(props) {
  ...

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

    ...
  `
}
```

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:
```jsx
FilterPanel(
  values={}
  onApply=(() => {})
)
  FieldGroup
    FieldDate(
      name="dateMin"
    )    

    FieldDate(
      name="dateMax"
    )

  FieldGroup
    FieldAmount(
      name="amountMin"
    )    

    FieldAmount(
      name="amountMax"
    )
```

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](https://reactjs.org/docs/context.html) first.

We create the following folder structure for this component:
```jsx
FilterPanel/
  Context/
  FieldDate/
  FieldAmount/
  FieldName/
  atoms.common.js <--- common styled components
  atoms.js
  index.js
```

Let's start with the Context module:
```jsx
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)
}
```

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

The state holding went to the FilterPanel component:
```jsx
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
  `
}
```

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:
```jsx
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
    )
  `
}
```

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!