Brendan McKenzie

Issues with direct DOM manipulation in React

Thursday, 15 April 2021

Working on projects with others I have come across a couple of patterns where there is a direct manipulation of the DOM in React applications.

CSS state management

1const el = document.querySelector(".some-class");
3const handleToggle = () => {
4  el.classList.toggle("is-active");
7<span className="some-class">hello world.</span>

This simply toggles a class on the selected element in the callback function.

The problem with this is that you have added another level of state management. To determine whether the element is "active" you need to check the classList of the element.

The alternative is to use React's state management.

1const [active, setActive] = React.useState(false);
3const handleToggle = () => setActive(prevState => !prevState);
5<span className={active ? "is-active" : undefined}>hello world</span>

I tend to use the classnames package for managing complex CSS class situations, I tend to alias it to cx(). So the above would become -

1<span className={cx({ "is-active": active })}>hello world</span>

Element interaction

Another example is interacting with elements.

1const el = document.querySelector(".some-class");
3const handleFocus = () => el.focus();
5<input className="some-class" />

This one may lean more towards personal preference, but this can be contained within React using refs.

1const inputRef = React.useRef<HTMLInputElement>()
3const handleFocus = () => inputRef.current?.focus();
5<input ref={inputRef} />

The issue using querySelector() here is that you are relying on the DOM to keep track of your element, circumstances might change, another React component might be created that uses the same reference used in the query selector which could interfere with your logic. Keeping track of the element reference with React means that you can explicitly target your intended element.