In September of this year, we launched a 5 week epic quest to build a web video call experience using the Zoom Video SDK. It was part of our effort to move our call infrastructure to Zoom and improve A/V quality for Remotion. In the process, we had to seriously level-up our React-fu and make some big brain moves. We’ve got a whole series on useful tricks coming your way soon, but for now, here’s one of the most useful tricks we picked up along the way.
💡 What’s the React equivalent to document.getElementById('banana')?
Answer: Refs! Why would we need this? This is useful in a multitude of cases, but we needed to use React refs to pass DOM elements to an external API. Fear not, it’s much easier than it sounds—let’s get familiar with the Ref concept.
What is a ref?
A ref is just a reference to a value, for when you want to store some information in a component, but you don’t want the component to re-render when that value changes. Here are some common use cases for refs:
Storing timeout IDs returned by setInterval and setTimeout, since they are normally needed for cleanup, but not for rendering UI.
Storing and manipulating DOM elements (ex. <inputs>, <buttons>)
Storing other objects that aren’t necessary to calculate the JSX.
You can store anything inside a ref. When you call useRef, React will create an object with a .current property, and point to the value inside .current.
Unlike objects stored in useState, the ref object is simply a plain javascript object and is mutable, and any changes to the value .current points to will not trigger a re-render.
If you list ref.current as a dependency in a useEffect or useMemo, it won’t ever cause the useEffect or useMemo to re-run. If you have the react-hooks/exhaustive-deps eslint plugin, it will even tell you:
Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component.
Creating and forwarding refs
The typical pattern for modern React apps is to use only functional components (no class components). Since createRef is meant to be used with class components, stick to useRef and forwardRef for functional components.
Here’s how to create and use a ref in Typescript:
Initially, localVideoRef.current will be null. When React creates a DOM node for the <video> element, React will put a reference to this node into localVideoRef.current.
Child component refs
But what if you want to make the video element a child component? The code below will not work because ZoomLocalVideo is a React component and by default React does not let a component access the DOM nodes of other components.
You’ll get the error: "Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"
So let’s useforwardRef! If you wrap forwardRef around the component that you want to reference, you can pass in a ref as an argument in addition to the standard props argument, and the above code will work.
Tada! It works now!
Best practices for refs
Treat refs as an escape hatch: But, if you’re not planning to interface with an external system or API, consider using useState instead. Refs are explicitly meant to update outside of the React render cycle, and if a bulk of application logic relies on refs, it's easy to get into inconsistent states.
Don’t read or write ref.current during rendering: React won’t know when ref.current changes, thus making your component’s output unpredictable. Use useState instead if the information is needed for rendering.
Read more at the official React docs:
https://beta.reactjs.org/learn/referencing-values-with-refs
https://beta.reactjs.org/learn/manipulating-the-dom-with-refs
This is part of an ongoing React series at Remotion. If you're interested in learning more about React, subscribe to our newsletter, or email angela at remotion dot com