event.preventDefault does not always prevent what you might expect

Some time ago I was working on a filter feature for a new website we at GRRR were working on. There already was a filter of some sort but this filter could only handle one filter option at a time.

The goal of the filtering changed and therefore the option filter needed to be able to filter on more than just one item at a time. As this article focuses mainly on the preventDefault function I’ll spare you the markup I wrote, but don’t worry if you’re keen on checking out the markup I’ve added links to the examples at the end of this article.

The logics

The logic also needed a makeover ofcourse. As we need to keep track of state somehow let’s start by adding a useState hook for this:

const [filterState, setFilterState] = useState({
  options: [],
  ...
});

Anyone familiar with vanilla js would probably argue the logical first step would be to make a function and prevent the form from its default behaviour by using event.preventDefault() as we want to do something custom.

// Add a function responsible for handling the change event
const handleChange = (event) => {
    // Prevent default form submission
    event.preventDefault();
    // Add variable for clicked target
    const value = event.currentTarget.getAttribute("data-value");
    // Do something with the selected target
    handleOptionChange(value);
};

Let’s also add the handleOptionChange function:

const handleOptionChange = (option) => {
    // The reason an array was chosen in this case,
    // build to make a system that doesn't needs to
    // know it's data is that this solution

    // If the option is not in the filterstate yet add option to the filterState
    if (!filterState.options.includes(option)) {
        setFilterState({
            ...filterState,
            options: [...filterState.options, option],
        });
    }

    // If the option is already in the filterstate remove the option from the filterState
    else (filterState.options.includes(option)) {
        const removeItemOptionFilter = filterState.options.filter(
            (optionItem) => optionItem !== option
        );
        setFilterState({
            ...filterState,
            options: removeItemOptionFilter,
        });
    }
};

This function was in the onChange event of the input:

<input
    className="option-filter__input"
    type="checkbox"
    data-value="just-confirmed"
    onChange={handleChange}
    checked={filterState.options.includes("just-confirmed")}
    name="just-confirmed"
    id="just-confirmed"
/>

The issue

This all seems pretty simple right? However this code did not work the way I thought it would and resulted in the following behaviour:

Checkbox behaviour with preventDefault

By looking at this behaviour it seemed to me that it had to be related to State. But I did not find any solution while falling deeper and deeper in a rabbit hole of despair.

The solution

After a major scavenger hunt for a possible solution I stumbled upon this solution on StackOverflow by Dave van Fleet. What this post basically suggests is my code would work fine when event.preventDefault would be removed and voilà:

Checkbox behaviour without preventDefault

But huh?! Why does this work?

I was pretty suprised to see event.preventDefault() breaking our code here. The post explains that MDN explains the following about the checked attribute:

The checked attribute is a Boolean attribute indicating whether or not this checkbox is checked by default (when the page loads). It does not indicate whether this checkbox is currently checked: if the checkbox’s state is changed, this content attribute does not reflect the change.

Dave van Fleet goes on and tells us that this means there’s some kind of disconnect between the checked attribute and, whether or not the state of the input is checked or not.

When it comes to React, on each rerender, the checked attribute will reflect the current state, but it’s still only a default value in the sense that the input has been newly rendered, and not manipulated since the state last changed.

Dave goes on to tell us that the actual native event for checkboxes is actually the click event and not the change event:

Also, without getting too off track, the event that natively changes the state on a checkbox input is actually the click event, not the change event. If you were to mess with the listeners and the values of a checkbox input in your browser’s js console, you’d see you could manipulate it in a way that the checkbox isn’t checked, but the values of the node say otherwise.

Conclusion

This basically means we don’t want to prevent the default behaviour. As the actual event is the click event and not the change event, clicking in combination with the preventDefault() will prevent the default click behaviour which we don’t want. Preventing the default behaviour causes the issues described here. As a matter of fact the examples in the React documentation don’t use event.preventDefault() on components 😅.

Sources