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.
Subscribe to my newsletter
Read articles from Yeled THEOPHANE directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by