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=...
)
...
`
}
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"
)
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
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)
}
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
`
}
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
)
`
}
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!
Top comments (0)