React and The Browser

Matthew MacFarquhar
5 min readJul 9, 2024

--

Introduction

I have been reading this book on React 18 and its best practices. As I go through this book, I will distill the core pieces of the most interesting tid-bits in this series of articles on React. This article will talk about some of the integrations React has with some common things we would want in a webpage. We will start with two specific cases of how we can handle forms and respond to browser events (like clicks and keyboard presses) and then we will talk about Refs, which allows us to bridge the gap between React and any DOM object to invoke their native functionality.

Controlled vs. Uncontrolled Inputs

In many web applications, a user will need to fill out some form or enter some data at some point in the application lifecycle. There are two patterns we can take to manage forms in react.

Uncontrolled

Uncontrolled inputs keep all state strictly in the DOM and we use refs to reach into the <input/> tag’s value to extract the data when we need it.

const Uncontrolled: FC = () => {
const firstNameInputRef = useRef<HTMLInputElement | null>(null);
const lastNameInputRef = useRef<HTMLInputElement | null>(null);

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()

console.log(`${firstNameInputRef.current!.value} ${lastNameInputRef.current!.value}`)
}

return (
<form onSubmit={handleSubmit}>
<input type="text" name="firstName" ref={firstNameInputRef} />
<input type="text" name="lastName" ref={lastNameInputRef} />
<button>Submit</button>
</form>
)
}

As you can see, the only point of React-ness we have is when we use the useRef to reach into the DOM input field to get its value. This is an anti-pattern, in general we should try to use React logic first to accomplish something browser based and only use a Ref to the DOM nodes if we really have no other option.

Controlled

Controlled inputs maintain state using React-y logic, keeping the values of the input in a React state and adding onChange callbacks to alter that state.

const Controlled = () => {
const [values, setValues] = useState({firstName: "Matthew", lastName: "MacFarquhar"})

const handleChange = ({target: {name, value}}: {target: {name: string, value: string}}) => {
setValues({
...values,
[name]: value
})
}

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()

console.log(`${values.firstName} ${values.lastName}`)
}

return (
<form onSubmit={handleSubmit}>
<input type="text" name="firstName" value={values.firstName} onChange={handleChange}/>
<input type="text" name="lastName" value={values.lastName} onChange={handleChange}/>
<button>Submit</button>
</form>
)
}

This approach does have more code than the uncontrolled solution, but it is often considered a better practices and allows us to maintain the input state in React. Keeping the state in React land will enable us to leverage more of React’s functionality (e.x. use a useEffect to turn text green when it is a valid input and red when it is not).

Handling Events

React wraps native DOM events in a SyntheticEvent this exposes the functionalities of the native events and also makes sure to fix some browser inconsistencies so our event handlers work the way we expect them to in all browsers.

const EventSwitching = () => {

const handleClick = (event: SyntheticEvent) => {
switch(event.type) {
case 'click':
console.log("CLICKED")
break
case 'dblclick':
console.log("DOUBLE CLICKED")
break
default:
console.log('unhandled', event.type)
}
}

return (
<button
onClick={handleClick}
onDoubleClick={handleClick}
>
Click Me!
</button>
)
}

The pattern above — called Event Switching — is a very common pattern in React, which has a single handle function and switches on the type of event received to determine the functionality that should be performed. This way we get one larger handler for all events instead of a bunch of specific handlers with the same boilerplate which clutters up the event loop subscriptions and our codebase.

Refs

In the context of working with the browser, Refs are used to allow react to interact directly with DOM objects and invoke their native functionality. In general, if you can do something without explicitly invoking some DOM functionality by using a ref, it is better not to use one, as this breaks out of the React philosophy of being declarative over imperative.

const Focus = () => {
const inputRef = useRef<HTMLInputElement>(null)

const handleClick = () => {
inputRef.current?.focus()
}

return (
<>
<input type="text" ref={inputRef}/>
<button onClick={handleClick}>Set Focus</button>
</>
)
}

Above is a basic example of useRef, where the ref is established on our input and then the same component has a handler which invokes a DOM native function — in this case it focuses on the input. This is a good use case for useRef, since there isn’t really a great way to trigger the focus logic of an input field other than by directly calling it.

Below is another example of a useRef, in this case we are using a forward ref. The DOM object we are interested in controlling lives in a child component, so we pass a ref object down as a prop to our child which will then link the ref up for us to use.

const TextInputWithRef = forwardRef((props, ref: ForwardedRef<HTMLInputElement>) => {
return <input ref={ref} type="text" {...props}/>
})

const ForwardRefComponent = () => {
const inputRef = useRef<HTMLInputElement | null>(null)

const handleClick = () => {
inputRef.current?.focus()
}

return (
<div>
<TextInputWithRef ref={inputRef}/>
<button onClick={handleClick}>Focus On Input</button>
</div>
)
}

This allows us to interact with DOM elements that may appear further down in our component tree from the parent, which may be useful if we need to extract data or invoke functionality on DOM objects that live down separate child branches.

Conclusion

In this article, we explored a couple of patterns to allow React to work well with native browser and DOM functionality. We injected React state into html forms, handled events in a concise way thanks to React’s synthetic event wrapper and discovered how we can access native browser and DOM functionality by using a useRef.

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet