Motivation:
The other day I found myself looking for info on how to implement responsive design in React components, I could not find anything clear, nothing that could make reference about any pattern or recommended method, so I decided to start thinking a little about this subject.
As soon as I started to search information about responsive design, the use of media queries come up quickly, but commonly related to the device's window in which it is being drawn, which does not seem to contribute much for isolated components.
Making a component to respond to the changes of the entire window dimensions does not seem to make sense, the component should respond to it's own dimensions, shouldn't it??
It is also true that some css tools can be used to manage the layout of the elements within the available space, for example with flexbox or css-grid some responsive behavior can be given to the elements but I don't think that can get to the same level as using media queries.
For this reason I thought that maybe using the same concept of media queries but oriented to components could be a good idea.
What do we want to achieve?
Something like this...
How to implement it?
As soon as I started to wonder how I could implement something like this the ResizeObserver appeared, a browser API that allows us to detect the component's size changes and react to that, so it seems that it could be useful for what I want to do.
The other thing that would be needed is to provide a standard way to define breakpoints for the element and a method to detect the component's size range in any given time, both of which can be implemented without much difficulties.
My approach for this task was:
- First, choose a structure to establish how the breakpoints for the component should be defined.
- From those breakpoints identify a list of size ranges and generate a css class for each one of them.
- Also it will be needed to identify the component's size after each change, find in which range it's on and assign the corresponding css class to it.
This way it could have the same behavior as with media queries. Each time a component changes its range we can assign the propper css class and the necessary styles will be applied.
As you can see the idea is simple and so is the procedure. I decided to encapsulate the logic in a hook to be able to reuse it in a fast way where it is necessary. https://www.npmjs.com/package/@jrx2-dev/use-responsive-class
How does this hook work?
The hook receives a reference to the component to be controlled and optionally breakpoints to be used.
In case of not receiving breakpoints, predefined ones will be used.
Breakpoints must implement the following interface:
interface breakpointsInput {
readonly [key: string]: number;
}
Example (defaults breakpoints):
const MEDIA_BREAKPOINTS: breakpointsInput = {
small: 420,
big: 768,
};
Width ranges (mediaBreakpoints) will be created according to the breakpoints used (with their respective generated css classes).
The generated mediaBreakpoints will comply with the following interface:
interface mediaBreakpoints {
class: string;
from: number;
toUnder: number;
}
And...
createMediaBreakpoints(MEDIA_BREAKPOINTS);
...should return:
[
{
class: "to-small",
from: 0,
toUnder: 420,
},
{
class: "from-small-to-under-big",
from: 420,
toUnder: 768,
},
{
class: "from-big",
from: 768,
toUnder: Infinity,
},
];
Whenever a change in the size of the component is detected, the getCurrentSizeClass method will be called and the css class corresponding to that width range will be returned.
getCurrentSizeClass(elementWidth, mediaBreakpoints)
How to use this hook:
npm i @jrx2-dev/use-responsive-class
import { useResponsiveClass } from '@jrx2-dev/use-responsive-class';
/*
const elementBreakpoints: breakpointsInput = {
small: 420,
big: 768,
};
*/
const elRef = createRef<HTMLDivElement>();
const [responsiveClass] = useResponsiveClass(elRef);
// const [responsiveClass] = useResponsiveClass(elRef, elementBreakpoints);
return (
<div ref={elRef} className={classes[responsiveClass]}>
Some content
</div>
);
The styles should be something like this (css modules are used in demo project):
.root {
&.to-small {
background-color: green;
}
&.from-small-to-under-big {
background-color: yellow;
}
&.from-big {
background-color: red;
}
}
Demo:
I used this custom hook in a component library that I made to use in personal demo projects . https://www.npmjs.com/package/@jrx2-dev/react-components
You can see this technique at work with an example component in the project's Storybook. https://jrx2-dev.github.io/react-components
Note:
I have to say that I got a little distracted adding an animation between the change of layouts of the component, the logic is encapsulated in the hook useFadeOnSizeChange, I think it was necessary to make the transition between layouts a little more fluid.
Conclusion:
This experiment served me as a proof of concept to develop a system that allows the design of truly responsive components in react.
Obviously the code can be improved, any comments or suggestions are welcome. The idea of this article was mostly a veiled question... how would you do it? :)
Regarding the peformance, a third party hook (@react-hook/resize-observer) optimized for the ResizeObserver implementation is being used and seems to give good results.
What I am interested in highlighting here is not so much the implementation itself but the concept used, I would like to hear opinions and suggestions on how you handle this issue.
Top comments (3)
How about a zero packages approach using css media queries? They are clean, easy to write and by using them you don't need to use javascript. Just css
Thanks for your comment.
At first I thought the same thing and tried to find a way to do this with css only but media queries always work depending on the size of the device/viewport, or its orientation/resolution, it doesn't work when what is important for us is to apply styles depending on the space an element is going to be able to occupy at a given time.
There is an interesting discussion about this on the w3c github and the recommended way for this seems to be the use of js.
That's actually right. First time I read your article I did not understand that you wanted different styles depending on the "space" of the element and not on the device/viewport. I have not yet encountered this scenario in real projects though, but it is a nice approach, will definitely use it if I ever need it.