swenedo
swenedo

Reputation: 3114

React Native onLayout with React Hooks

I want to measure the size of a React Native View every time it renders, and save it to state. If element layout didn't change the effect should not run.

It's easy to do with a class based component, where onLayout can be used. But what do I do in a functional component where I use React Hooks?

I've read about useLayoutEffect. If that's the way to go, do you have an example of how to use it?

I made this custom hook called useDimensions. This is how far I've got:

const useDimensions = () => {
  const ref = useRef(null);
  const [dimensions, setDimensions] = useState({});
  useLayoutEffect(
    () => {
      setDimensions(/* Get size of element here? How? */);
    },
    [ref.current],
  );
  return [ref, dimensions];
};

And I use the hook and add the ref to the view that I want to measure the dimensions of.

const [ref, dimensions] = useDimensions();

return (
  <View ref={ref}>
    ...
  </View>
);

I've tried to debug ref.current but didn't find anything useful there. I've also tried measure() inside the effect hook:

ref.current.measure((size) => {
  setDimensions(size); // size is always 0
});

Upvotes: 25

Views: 31099

Answers (3)

Jim Riordan
Jim Riordan

Reputation: 1428

As a refinement to matto1990's answer, and to answer Kerkness's question - here's an example custom hook that supplies the x, y position as well as the layout size:

const useComponentLayout = () => {
  const [layout, setLayout] = React.useState(null);

  const onLayout = React.useCallback(event => {
    const layout = event.nativeEvent.layout;
    setLayout(layout);
  }, [])

  return [layout, onLayout]
}

const Component = () => {
  const [{ height, width, x, y }, onLayout] = useComponentLayout();
  return <View onLayout={onLayout} />;
};

Upvotes: 6

matt-oakes
matt-oakes

Reputation: 3856

If you could like a more self-contained version of this here is a custom hook version for React Native:

const useComponentSize = () => {
  const [size, setSize] = useState(null);

  const onLayout = useCallback(event => {
    const { width, height } = event.nativeEvent.layout;
    setSize({ width, height });
  }, []);

  return [size, onLayout];
};

const Component = () => {
  const [size, onLayout] = useComponentSize();
  return <View onLayout={onLayout} />;
};

Upvotes: 60

Will Jenkins
Will Jenkins

Reputation: 9787

You had the right idea, it just needed a couple of tweaks... mainly, handing in the element ref and using elementRef (not elementRef.current) in the useEffect dependency array.

(Regarding useEffect vs useLayoutEffect, as you're only measuring rather than mutating the DOM then I believe useEffect is the way to go, but you can swap it out like-for-like if you need to)

const useDimensions = elementRef => {
   const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
   useEffect(() => {
      const el = elementRef.current;
      setDimensions({ width: el.clientWidth, height: el.clientHeight });
    }, [elementRef]);
    return [dimensions];
};

Use it like this:

function App() {
  const divRef = useRef(null);
  const [dimensions] = useDimensions(divRef);
  return (
    <div ref={divRef} className="App">
      <div>
        width: {dimensions.width}, height: {dimensions.height}
      </div>
    </div>
  );
}

Working codesandbox here

Edited to Add React Native version:

For React Native you can use useState with onLayout like this:

const App=()=>{
  const [dimensions, setDimensions] = useState({width:0, height:0})
    return (
      <View onLayout={(event) => {
                const {x, y, width, height} = event.nativeEvent.layout;
                setDimensions({width:width, height:height});
        }}>
        <Text}>
          height: {dimensions.height} width: {dimensions.width}
        </Text>
      </View>
    );

}

Upvotes: 6

Related Questions