Refactor checkbox filters with React Custom Hook
See how you can extract logic to React Hook to make the code more generic. And manage the state of checkbox filters with ease!

Search for a command to run...
See how you can extract logic to React Hook to make the code more generic. And manage the state of checkbox filters with ease!

No comments yet. Be the first to comment.
I already covered implementing Linear Regression model in Elixir from scratch. This time we'll move to implementing something fancy - Deep Learning model using a neural network under the hood! I'm going to use the same dataset so you'll have a nice 1...

In the previous post, I described how to create a Linear Regression Machine Learning model in Elixir from scratch. This time we'll discuss so similar yet different beast - Logistic Regression. Logistic Regression First things first - what is this fun...

In this article, I'll focus on getting as much value as possible from training data, particularly features. We'll continue our journey started in Linear Regression model using Elixir and Nx. Let's jump into the practice and learn how to squeeze lemon...

Learn how you can predict data using Linear Regression with Elixir

Find out what Machine Learning is about and what it looks in the Elixir's world

On one of my projects, I had multiple sets of checkboxes, used for filtering vacation offers. There're filters for departure cities, destination places, board options, etc. Let's look together, at how we can achieve this using React ๐
Let's assume we store the state for each filter set as an array. So when a checkbox is checked, its value is added to the array. It looks like this:
board = []
// checking "All Inclusive" and "Breakfast"...
board = ['allInvlusive', 'breakfast']
// unchecking "Breakfast"
board = ['allInclusive']
# etc.
Additionally, we wanted to push the array with the state after each change to Redux, using onChange callback. I hope it's quite clear, isn't it? Let's go to code and 1st iteration!
Let's start simple and implement storing state using useState hook and calling onChange callback under useEffect. selectedBoard is the array I mentioned earlier. It contains board options (IDs). Updating the state is performed by setSelectedBoard with the jsxhelper function toggleItem.
const [selectedBoard, setSelectedBoard] = useState<string[]>([])
useEffect(
() => {
onChange(selectedBoard)
}, [selectedBoard, onChange]
)
<input
type="checkbox"
name={ board.name }
value={ board.id }
checked={ isItemChecked(selectedBoard, board.id) }
onChange={ e => toggleItem(setSelectedBoard, e.target.value) }
/>
Nothing fancy. It just works ๐ The problem is that we'll have unnecessary code repetition in the future: with this approach, we have to copy useState and useEffect block for each filter component...
Fortunately, we can easily fix this, by extracting the hook logic to a more generic custom hook. What we'll do in the 2nd step.
The second step is all about extracting hook-related code to a separate file, wrapping it under useCheckboxes function (naming is up to you, by convention it starts with use) and return an object with functions related to the state.
Actually, useCheckboxes could return anything, like the array, instead of the object. In this case, it makes sense to use an object, since I expect useChecbkoxes to be growing, so it's much easier to import its functionalities via object keys.
As you can see, it uses the same useState and useEffect logic with the generic name Items.
export const useCheckboxes = ({ onChange, defaultItems }: Props) => {
const [selectedItems, setSelectedItems] = useState<string[]>(defaultItems)
const isItemChecked = (itemId: string): boolean => {
return selectedItems.includes(itemId)
}
const toggleItem = (itemId: string): void => {
setSelectedItems((prevState: string[]) => {
if (prevState.includes(itemId)) {
return prevState.filter(item => item !== itemId)
}
else {
return [...prevState, itemId]
}
})
useEffect(
() => {
onChange(selectedItems)
}, [selectedItems, onChange]
)
return { isItemChecked, toggleItem }
To use useCheckboxes for the Board component, we just need to import the functions and make some minor updates.
const { isItemChecked, toggleItem } = useCheckboxes({ onChange, defaultItems: [] })
...
<input
type="checkbox"
name={ board.name }
value={ board.id }
checked={ isItemChecked(board.id) }
onChange={ e => toggleItem(e.target.value) }
/>
And that's it! Now we have a generic useCheckboxes which can be easily used anywhere, without code repetition! Now, the code is much easier to maintain.
Also, it brings us a great bonus - now the checkbox logic can be easily tested.
import { renderHook, act } from '@testing-library/react-hooks'
import { useCheckboxes } from '../useCheckboxes'
const onChange = jest.fn()
it('#isItemCheck returns correct value', () => {
const { result } = renderHook(() => useCheckboxes({ onChange, defaultItems: ['XYZ'] }))
expect(result.current.isItemChecked('XYZ')).toBeTruthy()
expect(result.current.isItemChecked('ABC')).toBeFalsy()
})
it('#toggleItem toggles the given value and calls onChange', () => {
const { result } = renderHook(() => useCheckboxes({ onChange, defaultItems: ['XYZ'] }))
expect(onChange).toHaveBeenCalledTimes(1)
expect(result.current.isItemChecked('XYZ')).toBeTruthy()
expect(result.current.isItemChecked('ABC')).toBeFalsy()
act(() => result.current.toggleItem('ABC'))
expect(onChange).toHaveBeenCalledTimes(2)
expect(result.current.isItemChecked('ABC')).toBeTruthy()
expect(result.current.isItemChecked('XYZ')).toBeTruthy()
})
That's pretty nice! But, that's not the end. Let's suppose that we need to add some more features, like clearing an individual/all items (also without calling onChange in some cases), pulling state from Redux, etc.
Handling state became more complex, so we can do one more refactor - use React useReducer. And we'll do it!
useReducer hook manages state very similarly to Redux. It dispatches actions, which are handled by the reducer, which returns a new state.
The biggest advantage of this approach is we have a finite set of actions and we can get rid of useEffect, which by its asynchronous nature sometimes leads to unpredictable behavior (especially when managing a complex state).
As I mentioned, the reducer is a Redux-like function taking state and action as arguments. For clarity, I listed just part of the code.
export enum ActionType {
TOGGLE_ITEM = 'TOGGLE_ITEM',
CLEAR_ITEM = 'CLEAR_ITEM',
...
}
export const useCheckboxesReducer = (state: string[], action: useCheckboxesReducerAction) => {
switch (action.type) {
case ActionType.TOGGLE_ITEM: {
const { itemId } = action
const newState = state.includes(itemId) ? state.filter(item => item !== itemId) : [...state, itemId]
action.onChange(newState)
return newState
}
case ActionType.CLEAR_ITEM: {
const newState = state.filter(item => item !== action.itemId)
action.onChange(newState)
return newState
}
...
BTW, I like using enum for declaring action types. It's easy to export/import etc. Bear in mind, it's a TypeScript feature.
Now writing separate reducers pays off - useChecboxes becomes a very lean function ๐. Its main goal is dispatching actions with demanded payload.
export const useCheckboxes = ({ onChange, defaultItems }: Props): HookReturn => {
const [checkedItems, dispatch] = useReducer(useCheckboxesReducer, defaultItems)
const isItemChecked = (itemId: string): boolean => checkedItems.includes(itemId)
const toggleItem = (itemId: string): void => dispatch({ type: ActionType.TOGGLE_ITEM, itemId, onChange })
const clearItem = (itemId: string): void => dispatch({ type: ActionType.CLEAR_ITEM, itemId, onChange })
...
return { isItemChecked, toggleItem, clearItem, ... }
}
We went on a journey from simple state management with useState and useEffect, through extracting it to custom hook, to more complex code with useReducer.
We DRY-up the code and made it more maintainable and easy to test. Is this a good approach? I think so. But, only when you consider this as a path. Custom React Hook using useReducer definitely is not a silver bullet and you should end up with that solution, only when it makes sense. Wondering when? ๐ค
My thoughts on this:
Managing a simple state, related to just one component? Basic useState and useEffect can make it.
The state logic is relatively simple, but you use this in many components? Extract it to a custom hook!
When managing the state is like bull riding, rewrite the code with useReducer.
Just be pragmatic ๐