React refs and useImperativeHandle hook : A secret sauce for creating complex and reusable custom components.

ยท

3 min read

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.

ย