DEV Community

Oleksandr Demian
Oleksandr Demian

Posted on

8 1

Sync height between elements in React

A simple problem: make sure that the different elements in the app are the same height, as if they were in a table.

Let's start with a sample react app that renders 3 cards with different items (styles are omitted, but at the end they are all flex boxes):

const ItemCard = ({
  title,
  items,
  footerItems,
}: {
  title: string;
  items: string[];
  footerItems: string[];
}) => {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="separator" />
      <div className="items">
        {items.map((item) => (
          <p>{item}</p>
        ))}
      </div>
      <div className="separator" />
      <div className="footer">
        {footerItems.map((footerItem) => (
          <p>{footerItem}</p>
        ))}
      </div>
    </div>
  );
};

export const App = () => {
  return (
    <div>
      <ItemCard title="Card one" items={['One', 'Two']} footerItems={['One']} />
      <ItemCard
        title="Card two"
        items={['One', 'Two', 'Three', 'Four']}
        footerItems={['One', 'Two', 'Three']}
      />
      <ItemCard title="Card three" items={['One']} footerItems={['One']} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

When you run this app, you will get this result:
Default result

The desired result would be something like this:
React elements have same height

In order to synchronize the height I came with the following idea: a custom hook that stores the references to all different elements that have to be matched in a {[key: string]: value: array of elements} object, and when there is a change in dependencies, the height of elements gets recalcualted in useLayoutEffect:

import { MutableRefObject, useLayoutEffect } from 'react';

type Target = MutableRefObject<HTMLElement | null>;

// Store all elements per key, so it is easy to retrieve them
const store: Record<string, Target[]> = {};

// Triggered when useLayoutEffect is executed on any of the components that use useSyncRefHeight hook
const handleResize = (key: string) => {
  // get all elements with the same key
  const elements = store[key];
  if (elements) {
    let max = 0;
    // find the element with highest clientHeight value
    elements.forEach((element) => {
      if (element.current && element.current.clientHeight > max) {
        max = element.current.clientHeight;
      }
    });
    // update height of all 'joined' elements
    elements.forEach((element) => {
      if (element.current) {
        element.current.style.minHeight = `${max}px`;
      }
    });
  }
};

// Add element to the store when component is mounted and return cleanup function
const add = (key: string, element: Target) => {
  // create store if missing
  if (!store[key]) {
    store[key] = [];
  }

  store[key].push(element);

  // cleanup function
  return () => {
    const index = store[key].indexOf(element);
    if (index > -1) {
      store[key].splice(index, 1);
    }
  };
};

// Receives multiple elements ([key, element] pairs). This way one hook can be used to handle multiple elements
export type UseSyncRefHeightProps = Array<[string, Target]>;
export const useSyncRefHeight = (refs: UseSyncRefHeightProps, deps?: any[]) => {
  useLayoutEffect(() => {
    // store cleanup functions for each entry
    const cleanups: (() => void)[] = [];
    refs.forEach(([key, element]) => {
      // add element ref to store
      cleanups.push(add(key, element));
    });
    return () => {
      // cleanup when component is destroyed
      cleanups.forEach((cleanup) => cleanup());
    };
  }, []);

  useLayoutEffect(() => {
    // when any of the dependencies changes, update all elements heights
    refs.forEach(([key]) => {
      handleResize(key);
    });
  }, deps);
};
Enter fullscreen mode Exit fullscreen mode

By using this hook we can change a bit ItemCard element:

const ItemCard = ({
  title,
  items,
  footerItems,
}: {
  title: string;
  items: string[];
  footerItems: string[];
}) => {
  // create ref to the parent container, to only target its children instead of running query on the entire document
  const itemsRef = useRef(null);
  const footerRef = useRef(null);

  // align elements with class items
  // deps is an empty array, so it will only be aligned when the component is mounted.
  // You can add your dependencies, or remove it to make sure the hook runs at every render
  useSyncRefHeight(
    [
      ['items', itemsRef],
      ['footer', footerRef],
    ],
    // trigger hook when items of footerItems changes, since it may change height
    [items, footerItems],
  );
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="separator" />
      <div className="items" ref={itemsRef}>
        {items.map((item) => (
          <p>{item}</p>
        ))}
      </div>
      <div className="separator" />
      <div className="footer" ref={footerRef}>
        {footerItems.map((footerItem) => (
          <p>{footerItem}</p>
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now, items and footer elements height will be matched across all cards.

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (3)

Collapse
 
daveybrown profile image
daveybrown

This is cool. Thank you Oleksandr

...
let max = 0;
// reset height of all elements
elements.forEach((element) => {
  if (element.current) {
    element.current.style.minHeight = '0px';
  }
});
...
Enter fullscreen mode Exit fullscreen mode

If you add a reset like this, and trigger the hook on window width, then it works responsively.

Collapse
 
kryamk profile image
Kryamk

How trigger the hook on window width?

Collapse
 
daveybrown profile image
daveybrown

Use something like: usehooks.com/usewindowsize
And then pass width into useSyncRefHeight as the 2nd param, like [items, footerItems, size.width],

The best way to debug slow web pages cover image

The best way to debug slow web pages

Tools like Page Speed Insights and Google Lighthouse are great for providing advice for front end performance issues. But what these tools can’t do, is evaluate performance across your entire stack of distributed services and applications.

Watch video