React refs and useImperativeHandle hook : A secret sauce for creating complex and reusable custom components.
Refs in react are objects that allow us to store values (of any type) that do not trigger re-renders when updated.
Native elements have props ref
which we can set to a ref we created before, to get a direct reference to that element. This allows us to access properties and functionalities attached to it.
import { useRef } from "react";
function MyComponent(props: any) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
// We can access methods and properties available
// in the HTMLInputElement.
ref.current?.focus();
}, [])
return (
<input ref={ref} />
);
}
When this works for native elements it does not for custom elements. For that purpose, we use forwardRef
function that allows us to accept a ref as props in a custom components. Then, to expose functionalities and/or properties from our component to the parent component we use the useImperativeHandle
hook.
import {
createRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from "react";
// Define the properties and methods we expose to the outside.
interface MyComponentRef {
exposedFunction: (args: any) => void;
myComponentValue: any;
}
interface MyComponentProps {}
const MyComponent = forwardRef<MyComponentRef, MyComponentProps>(
(_, forwardedRef) => {
const [myComponentValue, setMyComponentValue] = useState<any>(null);
function exposedFunction(args: any) {
//perform some work with args.
console.log(args);
setMyComponentValue(args);
}
// Attach the properties and methods you want to expose to the `forwardedRef`
useImperativeHandle(forwardedRef, () => ({
exposedFunction,
myComponentValue,
}));
return <div>{/* JSX */}</div>;
}
);
function App() {
const ref = useRef<MyComponentRef>(null);
useEffect(() => {
const args = "new value";
// We can access all values available in `MyComponentRef`
ref.current?.exposedFunction(args);
console.log(ref.current?.myComponentValue);
}, [ref]);
return <MyComponent ref={ref} />;
}
Real-world applications
This pattern is used in many react libraries, here are a few of them :
React native rnmapbox
https://github.com/rnmapbox/maps
import { Camera } from '@rnmapbox/maps';
const camera = useRef<Camera>(null);
useEffect(() => {
camera.current?.setCamera({
centerCoordinate: [lon, lat],
});
}, []);
return (
<Camera ref={camera} />
);
React Native Bottom Sheet Modal
https://gorhom.github.io/react-native-bottom-sheet/modal
import React, { useCallback, useMemo, useRef } from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import {
BottomSheetModal,
BottomSheetModalProvider,
} from '@gorhom/bottom-sheet';
const App = () => {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
// variables
const snapPoints = useMemo(() => ['25%', '50%'], []);
const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present();
}, []);
const handleSheetChanges = useCallback((index: number) => {
console.log('handleSheetChanges', index);
}, []);
return (
<BottomSheetModalProvider>
<View style={styles.container}>
<Button
onPress={handlePresentModalPress}
title="Present Modal"
color="black"
/>
<BottomSheetModal
ref={bottomSheetModalRef}
index={1}
snapPoints={snapPoints}
onChange={handleSheetChanges}
>
<View style={styles.contentContainer}>
<Text>Awesome ๐</Text>
</View>
</BottomSheetModal>
</View>
</BottomSheetModalProvider>
);
};
const styles = StyleSheet.create({
/** Style definition */
});
export default App;
Conclusion
By using forwardRef
and useImperativeHandle
, we can create custom components that expose some functions to their parent through a ref props. This allows us to hide the implementation details of our components while exposing useful functions and/or properties to the parent.
This pattern is so practical that it's used in many react libraries and now you know it's implemented under the hoods.