DISCLAIMER: If you're unfamiliar with React's HOCs or state management in general, I recommend doing further research before proceeding with this article.
Introduction
When designing a web application a common concern is managing desktop and mobile views. Utilizing React's Higher Order Components (HOCs) and context API, we can render components based on if a mobile layout is detected.
The goal is to abstract a React Context consumer and provider into their own HOCs. Then use the provider HOC to wrap the main App component, and use the consumer HOC to wrap any components that need to render differently based on mobile being detected. This is a normal HOC/Context pattern, the chief difference being we need a custom provider HOC to encapsulate updates on resizing of the window.
With this high-level picture in our heads, let's dive into the code.
Setup
Clone the project here and remove the src/components/Mobile
directory if you want to follow along (rm -rf src/components/Mobile
)
Create a new component directory called Mobile
in src/components/
(the directory you just deleted).
Create a file called index.js
in the new directory. We will update this with exports later.
Finally run npm run start
to start the React development server.
Context
Next let's create the Context our HOCs will provide and consume in a file called context.js
:
import React from "react";
const IsMobileContext = React.createContext(false);
export default IsMobileContext;
This just creates a basic React Context with a default value of false. We will use this as described in our HOCs.
HOCS
Let's create the higher-order components that will allow us to easily use the context we just created.
Context.Provider HOC
We must first create a Context provider HOC to register our App component with.
import React from "react";
import IsMobileContext from "./context";
const INITIAL_STATE = {
size: {
width: window.innerWidth,
height: window.innerHeight
}
};
Import React for JSX
access and our Context we just created. Define the initial state for our provider HOC. We could set this to a boolean, but I find a size object to be more expandable.
Next let's create the provider HOC itself:
const withIsMobileViewProvider = Component => {
class WithIsMobileViewProvider extends React.Component {
constructor(props) {
super(props);
this.state = INITIAL_STATE;
}
// add listener to handle window resizing
componentDidMount() {
window.addEventListener("resize", this.handleWindowSizeChange);
}
handleWindowSizeChange = event => {
this.setState({
size: { width: window.innerWidth, height: window.innerHeight }
});
};
render() {
// current logic to determine if isMobileView
const isMobileView = this.state.size.width <= 600;
return (
<IsMobileContext.Provider value={isMobileView}>
<Component {...this.props} />
</IsMobileContext.Provider>
);
}
}
return WithIsMobileViewProvider;
};
// finally export the HOC
export default withIsMobileViewProvider;
This is a simple React HOC setup. Once the component mounts, we add our event listener to handle when the window resizes. The handler calls a simple function that sets the new width and height in our state.size
, forcing the HOC to re-render, and in-turn re-render its child component.
During the render we determine if it's a mobile view. Then pass the resulting boolean to the Context provider for use by consumers and render the component normally.
Context.Consumer HOC
We must now create a HOC to consume the Context we previously created and will provide to via the provider HOC we just created.
import React from "react";
import IsMobileContext from "./context";
Similarly, import our Context and React for access to JSX
. Our consumer HOC doesn't manage state as it just passes the value we provide via the Context to its child.
const withIsMobileView = Component => {
class WithIsMobileView extends React.Component {
render() {
return (
<IsMobileContext.Consumer>
{isMobileView => {
return <Component {...this.props} isMobileView={isMobileView} />;
}}
</IsMobileContext.Consumer>
);
}
}
return withIsMobileView;
};
// finally export the HOC
export default withIsMobileView;
This render method registers a consumer for our Context, which receives the boolean value we defined in our provider HOC. We then pass this to our component as a prop called isMobileView
.
Exports
Now that we have created our Context and HOCs we must export them for use in other components. Update our index.js
file:
import withIsMobileView from "./withIsMobileView";
import withIsMobileViewProvider from "./withIsMobileViewProvider";
import IsMobileContext from "./context";
export { IsMobileContext, withIsMobileView, withIsMobileViewProvider };
Use
Now we have created our HOCs for managing the view state of our application.
First we must register a provider for our consumers to actually pull the value from and pass to their children. We will do this in the App component as it is our parent component for the application.
Navigate to the App component (src/components/App/index.js
) and import the provider HOC we just created:
import { withIsMobileViewProvider } from "../Mobile";
Next use the HOC with our App component on export. Update the last line of the file from:
export default App;
to:
export default withIsMobileViewProvider(App);
Congratulations you just used a HOC to give a Context provider to our over-arching App component! Now we need to do the same with the consumer HOC in any views we want to determine are mobile.
Let's head over to the LandingPage component (src/components/Landing/index.js
) and use this consumer with our landing page to display a mobile landing page for our users. Import the consumer HOC we created, similar to when we imported the provider HOC in the App component.
import { withIsMobileView } from "../Mobile";
Next we need to register our LandingPage component with the HOC when we export it, same as the App.
export default withIsMobileView(LandingPage);
Now our component is receiving the isMobileView
prop via the HOC system we just created. Its value will also be automatically updated upon window resizing. However we are not rendering different pages based on this value, so let's change that.
You can see I have created the components MobileLandingPage
and DesktopLandingPage
with a simple h1 tag to demonstrate this.
const MobileLandingPage = () => {
return <h1>Mobile Landing Page</h1>;
};
const DesktopLandingPage = () => {
return <h1>Desktop Landing Page</h1>;
};
However our landing page is not using them yet, let's change that too. With a simple JSX
expression we can conditionally render either component based on our isMobileView
boolean value. Change the render method of our LandingPage
component to implement this logic:
class LandingPage extends React.Component {
render() {
return (
<>
{this.props.isMobileView ? (
<MobileLandingPage />
) : (
<DesktopLandingPage />
)}
</>
);
}
}
That's it! Save the file and check out the effect in your browser. Either use developer tools to swap to a mobile layout or simply resize your window to a point that our HOC determines it's mobile.
One cool feature of this system is the only value provided is a boolean. This means you can do a plethora of things with it, from dynamic rendering like you see above to simply changing the styling on a couple elements. You could even switch up the HOC system to provide the size object we created in addition to or instead of the boolean!
I hope this post was insightful into one of the many ways you can manage mobile and desktop views when building scalable and reliable web applications.
Feel free to follow me on twitter to stay up to date on my latest shenanigans!
Top comments (0)