10 ways to use "Ref" in React

Bibek Basyal
5 min read
https://bhoos-cdn.nyc3.digitaloceanspaces.com/blog/2023/07/ref.jpg

Ref can be simply understood as a reference. Refs are used to preserve values across re-renders in React. Unlike state values, the ref can be mutated and the value changes immediately without causing a re-render. With this in mind, let us explore the ways in which refs can be used in React.

Lazy loading a FlatList

One of the ways to improve UI performance is to lazy load things whenever possible. Refs can be used to lazy load FlatList items in react-native.

import { useCallback, useEffect, useRef, useState } from "react";
import {
  ViewToken,
  FlatList,
  TouchableOpacity,
  Image,
  ActivityIndicator,
} from "react-native";

type LazyLoadingFlatListProps<T> = {
  list: T[];
  initialNumToRender: number;
  onPress: (item: T) => void;
};

type ListItem = {
  id: string;
  content: number;
};

export function LazyLoadingFlatList<T extends ListItem>({
  list,
  onPress,
  initialNumToRender,
}: LazyLoadingFlatListProps<T>) {
  const itemsRef = useRef(
    Array.from({ length: list.length }).map(() => ({
      loadItem: () => {},
    }))
  );

  const flatListRef = useRef(null);

  const renderItem = useCallback(
    ({ item, index }: { item: T; index: number }) => {
      const isVisible = index <= initialNumToRender;

      return (
        <TouchableOpacity onPress={() => onPress(item)} activeOpacity={1}>
          <ItemImage
            source={item.content}
            loadItemFnRef={itemsRef.current[index]}
            visible={isVisible}
          />
        </TouchableOpacity>
      );
    },
    []
  );

  const lazyLoadItem = (viewToken: ViewToken[]) => {
    const viewableTokens = viewToken.filter((token) => token.index);
    for (const viewableToken of viewableTokens) {
      if (viewableToken.index === null) continue;

      const loadItemFnRef = itemsRef.current[viewableToken.index];
      loadItemFnRef.loadItem();
    }
  };

  return (
    <FlatList<T>
      ref={flatListRef}
      data={list}
      bounces={false}
      renderItem={renderItem}
      onViewableItemsChanged={(info) => lazyLoadItem(info.changed)}
    />
  );
}

export type ItemImageProps = {
  source: number;
  loadItemFnRef: { loadItem: () => void };
  visible: boolean;
};

function ItemImage({ source, loadItemFnRef, visible }: ItemImageProps) {
  const [show, setShow] = useState(visible);

  useEffect(() => {
    loadItemFnRef.loadItem = () => {
      setShow(true);
    };
  }, []);

  if (!show) return <ActivityIndicator />;
  
  return <Image source={source} />;
}

Access singleton object interface

Refs can also be used to store a singleton object and access its methods.

import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";

type UserProfile = {
  id: string;
  name: string;
  image: string;
};

type SubscriberFn = Dispatch<SetStateAction<UserProfile | null>>;

async function getValueFromStorage(id: string) {
  const valueFromStorage = await readFromStorage(id);
  return valueFromStorage;
}

class ProfileData {
  subscribers = new Set<SubscriberFn>();

  subscribe(fn: SubscriberFn, userId: string) {
    let mounted = true;
    this.subscribers.add(fn);

    getValueFromStorage(userId).then((profile) => {
      if (mounted) fn(profile);
    });

    return () => {
      mounted = false;
      this.subscribers.delete(fn);
    };
  }

  // other methods
}

const profileData = new ProfileData();

function useProfileData(userId: string) {
  const profileObj = useRef(profileData);
  const [userProfile, setUserProfile] = useState<UserProfile | null>(null);

  useEffect(() => {
    return profileObj.current.subscribe(setUserProfile, userId);
  }, [userId]);

  return userProfile;
}

// use case
function ProfileComponent({ userId }: { userId: string }) {
  const userProfile = useProfileData(userId);

  if (!userProfile) return null;

  return (
    <div>
      <img src={userProfile.image} alt={userProfile.name} />
      <p>{userProfile.name}</p>
    </div>
  );
}

Passing ref as a prop to child component

Refs created in the parent component can be passed to a child component. In addition to this, the ref object can be updated inside the child component and the parent component receives the latest value for ref.

import { MutableRefObject, useRef } from "react";

export function ParentComponent() {
  const nameRef = useRef("Hello");

  return (
    <div>
      <ChildComponent nameRef={nameRef} />
    </div>
  );
}

type ChildComponentProps = {
  nameRef: MutableRefObject<string>;
};

function ChildComponent({ nameRef }: ChildComponentProps) {
  // can use nameRef as a regular ref object
  return (
    <div>
      <p>{nameRef.current}</p>
    </div>
  );
}

Using useImperativeHandle hook

Refs are useful to store custom handlers from child to parent components in React. This is possible by using the useImperativeHandle hook.

import { MutableRefObject, useRef, useState, useImperativeHandle } from "react";

export function ModalParentComponent() {
  const modalRef = useRef();

  function handleOpenModal() {
    modalRef.current.openModal();
  }

  return (
    <>
      <p>Parent Component</p>
      <Modal modalRef={modalRef} />
      <button onClick={handleOpenModal}>Open</button>
    </>
  );
}

type ModalProps = {
  modalRef: MutableRefObject<any>;
};

function Modal({ modalRef }: ModalProps) {
  const [isModalOpen, setModalOpen] = useState<boolean>(false);

  useImperativeHandle(modalRef, () => ({
    openModal: () => setModalOpen(true),
  }), []);

  if (!isModalOpen) return null;

  return (
    <div>
      <p>This is the modal!</p>
      <button onClick={() => setModalOpen(false)}>Close</button>
    </div>
  );
}

Animations

In react-native, animations are done using animated values. These animated values are constantly updated from start to end with precise intervals so as to display the transition as an animation. Refs can be used to store the animated value to change, this way when the component re-renders there is no effect on the animation running as the value of ref is preserved.

import { useEffect, useRef } from "react";
import { Animated, StyleSheet } from "react-native";

const styles = StyleSheet.create({
  circle: {
    height: 32,
    width: 32,
    borderRadius: 16,
    backgroundColor: "red",
  },
});

export function AnimatedCircle() {
  const xPos = useRef(new Animated.Value(-400)).current;

  function animate() {
    Animated.loop(
      Animated.sequence([
        Animated.timing(xPos, {
          toValue: 400,
          duration: 1000,
          useNativeDriver: true,
        }),
        Animated.timing(xPos, {
          toValue: -400,
          duration: 1000,
          useNativeDriver: true,
        }),
      ])
    ).start();
  }

  useEffect(() => {
    animate();
  }, []);

  return (
    <Animated.View
      style={[styles.circle, { transform: [{ translateX: xPos }] }]}
    />
  );
}

Capture the previous state of props

Let us consider a use case for which you want to show the previous value of an input field when its value is changed. This particular use case can be easily implemented using ref. Here we implement a “usePrevious” hook that returns a previous value when the new value is supplied.

import { Fragment, useRef } from "react";

export function usePreviousValue<T>(value: T) {
  const ref = useRef<{ value: T; prev: T | null }>({
    value: value,
    prev: null,
  });

  const current = ref.current.value;

  if (value !== current) {
    ref.current = {
      value: value,
      prev: current,
    };
  }

  return ref.current.prev;
}

export function PersistentValue({ value }: { value: number }) {
  const prevValue = usePreviousValue(value);

  return (
    <Fragment>
      <p>Current Value: {value}</p>
      <p>Previous Value: {prevValue}</p>
    </Fragment>
  );
}

Debounce and Throttling

When we talk about the performance in React, only making things fast does not work so often. On the contrary, slowing things down can actually increase performance. The last thing a developer wants is to crash the server with a series of request triggers. This is where slow down techniques “debouncing” and “throttling” come handy.

import { useCallback, useRef } from "react";

export function useDebounce(cb: Function, delay = 1000) {
  const lastUsed = useRef<number>(0);

  return useCallback(
    (...args) => {
      if (Date.now() - lastUsed.current < delay) return;
      lastUsed.current = Date.now();

      if (cb) cb(...args);
    },
    [cb]
  );
}

export function SoundButton() {
  const debounce = useDebounce(onPress);

  function onPress() {
    console.log("button pressed");
  }

  return (
    <button type="button" onClick={debounce}>
      Press me
    </button>
  );
}

Working with intervals

Refs are helpful to store timer id, since they must be cleared in the cleanup function when the component unmounts. This can be clearly justified with the help of a stopwatch in React.

import { useState, useRef, Fragment } from "react";

export function Stopwatch() {
  const [startTime, setStartTime] = useState<number>();
  const [now, setNow] = useState<number>();
  const intervalRef = useRef<number>();

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    // clear previous interval, if any
    if (intervalRef.current) clearInterval(intervalRef.current);

    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    if (intervalRef.current) clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime !== undefined && now !== undefined) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <Fragment>
      <h3>Time passed: {secondsPassed.toFixed(3)}</h3>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleStop}>Stop</button>
    </Fragment>
  );
}

Preserving values across re-renders

Sometimes we need to preserve values across re-renders and update it, that too without causing re-renders. Refs are just the right tool for the case.

import { useRef, useState } from "react";

export function RerenderCount() {
  const renderCount = useRef<number>(0);
  const [pressCount, setPressCount] = useState<number>(0);

  // increment renderCount everyTime
  renderCount.current++;

  function handlePressCount() {
    setPressCount((value) => value + 1);
  }

  return (
    <div>
      <p>Re-render count: {renderCount.current}</p>
      <p>Press count: {pressCount} </p>
      <button onClick={handlePressCount}>Press Me</button>
    </div>
  );
}

References to callbacks

Another use case of ref can be to store reference to the callback. The callback can be later executed using the current property of the ref.

import { useEffect, useRef } from "react";

type CallbackFn = () => void;

export function useInterval(callback: CallbackFn, delay = 1000) {
  const savedCallback = useRef<CallbackFn>();

  // remember the latest callback
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    const timer = setInterval(() => savedCallback?.current(), delay);

    return () => clearInterval(timer);
  }, [delay]);
}