5

I am trying to change the height of a container, when in mobile landscape mode only. I am playing around in the developer tools to swap the orientation of a mobile device but it only works on the first render. I am new to react hooks so not sure if I am implementing it right.

The idea is that I am testing that once in landscape, if it's on mobile the height should be less than 450px (which is the check I am doing for the if statement)

Could someone point me in the right direction, please?

Thanks!

const bikeImageHeight = () => {
    const windowViewportHeight = window.innerHeight;
    const isLandscape = window.orientation === 90 || window.orientation === -90;
    let bikeImgHeight = 0;

    if (windowViewportHeight <= 450 && isLandscape) {
      bikeImgHeight = windowViewportHeight - 50;
    }

    return bikeImgHeight;
  };

  useEffect(() => {
    bikeImageHeight();

    window.addEventListener("resize", bikeImageHeight);

    return () => {
      window.removeEventListener("resize", bikeImageHeight);
    };
  }, []);
mark
  • 235
  • 3
  • 11

3 Answers3

6

The useEffect hook is not expected to fire on orientation change. It defines a callback that will fire when the component re-renders. The question then is how to trigger a re-render when the screen orientation changes. A re-render occurs when there are changes to a components props or state.

Lets make use of another related stackoverflow answer to build a useWindowDimensions hook. This allows us to hook into the windows size as component state so any changes will cause a re-render.

useWindowDimensions.js

import { useState, useEffect } from 'react';

function getWindowDimensions() {
  const { innerWidth: width, innerHeight: height } = window;
  return {
    width,
    height
  };
}

export default function useWindowDimensions() {
  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());

  useEffect(() => {
    function handleResize() {
      setWindowDimensions(getWindowDimensions());
    }

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return windowDimensions;
}

You can then use that hook in your component. Something like:

BikeImage.js

import React from 'react'
import useWindowDimensions from './useWindowDimensions'

export default () => {
  const windowDimensions = useWindowDimensions();

  // Define these helper functions as you like
  const width = getImageWidth(windowDimensions.width)
  const height = getImageHeight(windowDimensions.height)

  // AppImage is a component defined elsewhere which takes 
  // at least width, height and src props
  return <AppImage  width={width} height={height} src="..." .../>
}
peter554
  • 1,248
  • 1
  • 12
  • 24
3

Here is a custom hook that fires on orientation change,

import React, {useState, useEffect} from 'react';

// Example usage
export default () => {
  const orientation = useScreenOrientation();
  return <p>{orientation}</p>;
}

function useScreenOrientation() {
  const [orientation, setOrientation] = useState(window.screen.orientation.type);

  useEffect(() => {
    const handleOrientationChange= () => setOrientation(window.screen.orientation.type);
    window.addEventListener('orientationchange', handleOrientationChange);
    return () => window.removeEventListener('orientationchange', handleOrientationChange);
  }, []);

  return orientation;
}

Hope this takes you to the right direction.

arpl
  • 3,505
  • 3
  • 18
  • 16
0

You need to trigger a re-render, which can be done by setting state inside of your bikeImageHeight.

  const [viewSize, setViewSize] = useState(0)


    const bikeImageHeight = () => {
        const windowViewportHeight = window.innerHeight;
        const isLandscape = window.orientation === 90 || window.orientation === -90;
        let bikeImgHeight = 0;

        if (windowViewportHeight <= 450 && isLandscape) {
          bikeImgHeight = windowViewportHeight - 50;
        }
        setViewSize(bikeImgHeight)
        return bikeImgHeight;
  };

And per the comments conversation, here's how you'd use debounce:

function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};


const YourComponent = () => {

  const bikeImageHeight = () => {
    const windowViewportHeight = window.innerHeight;
    const isLandscape = window.orientation === 90 || window.orientation === -90;
    let bikeImgHeight = 0;

    if (windowViewportHeight <= 450 && isLandscape) {
      bikeImgHeight = windowViewportHeight - 50;
    }
    setViewSize(bikeImgHeight)
    return bikeImgHeight;
  };


  const debouncedBikeHeight = debounce(bikeImageHeight, 200)

  useEffect(() => {
    bikeImageHeight();

    window.addEventListener("resize", debouncedBikeHeight);

    return () => {
      window.removeEventListener("resize", debouncedBikeHeight);
    };
  }, []);

  return <div>some jsx</div>

}

Example debounce taken from here: https://davidwalsh.name/javascript-debounce-function

Jay Kariesch
  • 1,392
  • 7
  • 13
  • You'll need to throttle or debounce your event. https://codeburst.io/throttling-and-debouncing-in-javascript-b01cad5c8edf – Jay Kariesch Oct 15 '19 at 15:41
  • I was accidentally calling the function directly when passing it down into a child component which I have removed, however bikeImageHeight still doesn't log out it's value when switching from portrait to landscape and vice-versa? – mark Oct 15 '19 at 15:43
  • Where is your console log? Is it inside of bikeImageHeight ? – Jay Kariesch Oct 15 '19 at 15:45
  • I am currently logging within useEffect(); If I log outside of useEffect() I get the looping error again :( I need to pass the value of bikeImageHeight into a component but I need the value to change on orientation change – mark Oct 15 '19 at 15:47
  • Ahh, gotcha, Yeah, useEffect isn't going to fire more than once. If it's not 'watching' a variable in the second argument array, it'll behave just like the classic 'componentDidMount' lifecycle method. I'll update my post to demonstate how to trigger useEffect to fire when your viewport size changes. – Jay Kariesch Oct 15 '19 at 15:51
  • Thank you so much! – mark Oct 15 '19 at 15:52
  • Actually, on second thought, you likely don't want useEffect to trigger over and over. You should be fine just setting a state value inside of bikeImageHeight. Setting state will trigger a re-render, and you could use the value in your JSX. All that said, you'll inevitably have to use throttle or debounce, Here's a good article around why you should use it, which mentions Twitter's issue with scroll events: https://css-tricks.com/debouncing-throttling-explained-examples/ – Jay Kariesch Oct 15 '19 at 15:56
  • Updated my original post with debounce example. – Jay Kariesch Oct 15 '19 at 16:04
  • That's a typo, haha. My bad! – Jay Kariesch Oct 15 '19 at 16:10
  • After implemeting your changes, I'm still not getting the value back (ugh!!) when I pass bikeImageHeight to my component, it is still not recieving the updated values :( – mark Oct 15 '19 at 16:13
  • I have added a jsFiddle with my component in there (it won't actually work because I am using styled components, but you can see that I am passing the bikeImageHeight into a in my render... https://jsfiddle.net/tms0u4kn/ – mark Oct 15 '19 at 16:17
  • When I log out bikeImageHeight I can see it's just giving me the function (i.e. needs to be called) but when I call it when passing it into that is when I get the looping error :/ – mark Oct 15 '19 at 16:24
  • It works here: https://codepen.io/sodapop/pen/RwwamgL I set it to setViewSize to window.innerHeight so you can see that it changes when you pull the codepen iframe view up and down. – Jay Kariesch Oct 15 '19 at 16:43
  • We might have to see what's going on inside of "Wrap" in order to see why you're getting this error. – Jay Kariesch Oct 15 '19 at 16:44
  • Wrap is just a styled component (div) which I am passing the bikeImageHeight down to. max-height: ${bikeImageHeight ? bikeImageHeight + "px" : ""}; – mark Oct 15 '19 at 16:54
  • You need to pass viewSize to your wrapped component, like this: – Jay Kariesch Oct 15 '19 at 17:00
  • Sorry my laptop has just died and have forgotten to bring my charger, I will have to try this change tomorrow but just wanted to say thanks a lot for your help so far! – mark Oct 15 '19 at 17:16