Render Component Based on Media Query

Hi all,
I’m trying to do what I feel like should be simple. I’ve looked at a few threads but nothing seems to work nicely. I have a

<DesktopNav/> and a <MobileNav/> component

I want to render when screen size is < 500px and render when screen size is greater than say 768px. The numbers are kind of irrelevant. I was using the useEffect hook and window.matchMedia to do this.

Here is the code for my mobile-nav component:

import React, { useEffect } from "react";
import { css, connect, useConnect, styled } from "frontity";
import { getDisplayType } from "./../../../util/util";

const MobileNav = (props) => {

	const { state, actions } = useConnect();

	useEffect(() => {
		const windowResized = () => {
			if(props.type === getDisplayType()) {
				actions.theme.setShowMobile(true);
			} else {
				actions.theme.setShowMobile(false);
			}
		};

		document.defaultView.addEventListener('resize', windowResized);
		windowResized();

		return () => {
			document.removeEventListener('click', windowResized);
		};
	});

	return (
		<Header visible={state.theme.showMobile}>
		Mobile Nav
		</Header>
	);
};

export default connect(MobileNav,{ injectProps: false });

const Header = styled.header`
    display: ${props => props.visible === true ? 'flex' : 'none'};
    align-items: center;
    justify-content: space-between;
    height: 60px !important;
    width: auto;
    position: fixed;
    top: 0px;
    right: 0;
    left: 0;
    z-index: 99999;
    background-color: #FFFFFF;
    padding: 0 15px;
`;

I have similar code in my DesktopNav and then in my index.js:

state: {
    /**
     * State is where the packages store their default settings and other
     * relevant state. It is scoped to the `theme` namespace.
     */
    theme: {
      autoPrefetch: "in-view",
      menu: [],
      showMobile: false,
      showDesktop: false,
      isMobileMenuOpen: false,
      featured: {
        showOnList: false,
        showOnPost: false,
      },
    },
  },

  /**
   * Actions are functions that modify the state or deal with other parts of
   * Frontity like libraries.
   */
  actions: {
    theme: {
      // special Frontity action fired to prepare the state for the React render made in the server
      beforeSSR: async ({actions}) => {
        // All received data are populated in state.source and are accessible using the methods like the handler in libraries.source
        // await actions.source.fetch('http://ripple-web.lndo.site/wp-json/menus/v1/menus/footer-about-us') // this invokes our footerMenuHandler
        await actions.source.fetch('menus/footer-menu') // /menus/v1/menus/<slug> data for a specific menu location.
        await actions.source.fetch('all-categories')
      },
      setShowMobile: ({ state }) => value => {
        state.theme.showMobile = value;
      },
      setShowDesktop: ({ state }) => value => {
        state.theme.showDesktop = value;
      },
      toggleMobileMenu: ({ state }) => {
        state.theme.isMobileMenuOpen = !state.theme.isMobileMenuOpen;
      },
      closeMobileMenu: ({ state }) => {
        state.theme.isMobileMenuOpen = false;
      },
    },
  },

Basically I’m updating some state variables to show either menu, you can see that I’m altering the css display property based on the state value which I’m passing as a prop. This works but each time I load the page / refresh the browser the component flickers from invisible to visible.

I guess this leads to the bigger question of is there a way to do this on the server side? What I’m trying to do with useEffect won’t work on the server because I can’t access the window which is needed by my calls to matchMedia.

I feel like I’m spending lots of time on what should be simple things with Frontity. Anyone else have any thoughts on this?

Hi! I think that in order to know better what might be causing the issue you could share the code where you are using window.matchMedia.

Regarding the device detection on SSR, after a quick search, apparently the way to achieve this is to analyse the use-agent header of the request. To do this with frontity you can use the beforeSSR action like in the example:

{
  actions: {
    beforeSSR: ({ state }) => async ({ ctx }) => {
      const userAgent = ctx.header["user-agent"];

      if (userAgent.match(/something/) {
        // update the needed state.
      }
    },
  }
}

Hope this is of help :slight_smile:

1 Like

Hi @sattley,

I think a simpler approach is the one used in mars-theme that uses media queries to display a different NavBar and Menu on mobile devices

Hope this helps

Hi @orballo,
Sorry for my slow response. I really appreciate your quick feedback. Sorry that I forgot to share the matchMedia code. Here it is for posterity:

export function getDisplayType() {
const mq = window.matchMedia( "(max-width: 570px)" );
if (mq.matches) {
	return "mobile";
} else {
	return "desktop";
}

}

I think I was looking for the “correct” way to do media queries, maybe what is the best for performance. In the end I went with just media queries in my styled components - like this:

const Nav = styled(animated.nav)`
  @media (max-width: 768px) {
	display: block;
}
@media (min-width: 769px) {
	display: none;
}
  position: fixed;
top: 60px;
right: 0;
left: 0;
z-index: 99999;
overflow-y: auto;

`;

I knew I could do this all along but again was wondering if it’s more performant or possible to render a component based on the media query. It sounds like it’s doable by getting the user-agent as you mentioned in the beforeSSR action. I may look back into doing it that way when I have some time.

Thanks again for your help!

1 Like

Hi @juanma,
Thank you for responding also. I ended up doing exactly what you suggested. Something like this:

const Nav = styled(animated.nav)`
  @media (max-width: 768px) {
	display: block;
}
@media (min-width: 769px) {
	display: none;
}
  position: fixed;
top: 60px;
right: 0;
left: 0;
z-index: 99999;
overflow-y: auto;

`;

I knew that I could use this method all along but was wondering if there is a better more performant way.

Cheers!

In my opinion the more performant way to do anything is always without JS, but in case you necessarily need to stop rendering a component, or run some JS depending on the device, then you should use the matchMedia API.

Glad you found a solution that worked for you :slight_smile:

1 Like