Sync height between elements in React
Tags: javascript, react, webdev, beginners
Published: 2022-08-14 09:00:11

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):
```tsx
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>
  );
};
```

When you run this app, you will get this result:
![Default result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oain9fxhpvc1gb1z1i6j.png)

The desired result would be something like this:
![React elements have same height](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mw2sypdska60h6lriudl.png)

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`:
```typescript
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);
};
```

By using this hook we can change a bit ItemCard element:
```tsx
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>
  );
};
```

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