I need to conditionally render components based on screen size.
I use nextjs and getInitialProps for data fetching, the page is server-side rendered. I want to detect device screen size on the client-side, so I implement a customized hook to do it.
useWindowSize.js
import { useEffect, useState } from 'react';
export default function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window === 'undefined' ? 1200 : window.innerWidth, // default width 1200
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
//height: window.innerHeight,
});
}
// Add event listener
window.addEventListener('resize', handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize.width <= 600;
}
then I use this hook to detect window size and conditional render components
export default function IndexPage() {
const isMobile = useWindowSize();
if (typeof window !== "undefined") {
// if you are running it on codesanbox, I don't know why log is not printed
console.log("client side re-render");
}
return (
<div>
{isMobile ? (
<div
style={{
color: "red",
fontSize: 40
}}
>
mobile
</div>
) : (
<div
style={{
color: "blue",
fontSize: 20
}}
>
desktop
</div>
)}
</div>
);
}
IndexPage.getInitialProps = () => {
return {
a: 1
};
};
when I load the page on mobile browser, you will see
text mobile is applied wrong CSS style. video demo: https://share.getcloudapp.com/nOuk08L0
how to reproduce:
https://codesandbox.io/s/thirsty-khayyam-npqpt
Can someone please help me out. Thank you in advance!
This is an issue that is related to how React patch up DOM from SSR. When there is a mismatch between client-side and server-side rendering, React will only patch/sync the text context for the node. The DOM attribute will not be automatically updated. In your case, the SSR result has the desktop style because there is no window object, and client side has the mobile result. After the mismatch, React update the text node from 'desktop' to mobile but not the style attributes.
In my opinion, you can use two different approaches. You can use Media Query to style your component based on the screen width instead of the hook. If you are doing SSR, not SSG, you can use user agent req.headers["user-agent"] to detect the device your device is being viewed on.
For the first approach, you might need to render more DOM node you might need to. For the second approach, you won't be able to know the actual viewport size, which can cause visual issue. You might be able to combine both approach to produce a good viewing experience for your user.
Reference
https://github.com/facebook/react/issues/11128#issuecomment-334882514
Thanks for #Andrew Zheng's detailed explanation! Today I learned.
I know that I can style the layout by using pure CSS media query, but my use case needs a variable like isMobile to
if (isMobile) {
doSomethingOnlyOnMobileWeb();
} else {
doSomethingOnlyForDesktopWeb();
}
So I combined two approaches you provided, and modify my hook this way:
export default function useWindowSize(userAgent) {
let isMobile = Boolean(
userAgent &&
userAgent.match(
/Android|BlackBerry|iPhone|iPod|Opera Mini|IEMobile|WPDesktop/i
)
);
const [windowSize, setWindowSize] = useState({
width: isServer
? isMobile
? BREAKPOINT_SMALL
: BREAKPOINT_LARGE
: window.innerWidth,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
//height: window.innerHeight,
});
}
// Add event listener
window.addEventListener('resize', handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize.width <= BREAKPOINT_SMALL;
}
diff: passing user-agent string to useWindowSize for server-side detection and use window.innerWidth for client-side detection. There won't be a mismatch between server and client.
I have a react component that needs to know its dimensions ahead of time, before it renders itself.
When I'd make a widget in jquery I could just $('#container').width() and get the width of the container ahead of time when I build my component.
<div id='container'></div>
these container's dimensions are defined in CSS, along with a bunch of other containers on the page. who defines the height and width and placement of the components in React? I'm used to CSS doing that and being able to access that. But in React it seems I can only access that information after the component has rendered.
The example below uses react hook useEffect.
Working example here
import React, { useRef, useLayoutEffect, useState } from "react";
const ComponentWithDimensions = props => {
const targetRef = useRef();
const [dimensions, setDimensions] = useState({ width:0, height: 0 });
useLayoutEffect(() => {
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
});
}
}, []);
return (
<div ref={targetRef}>
<p>{dimensions.width}</p>
<p>{dimensions.height}</p>
</div>
);
};
export default ComponentWithDimensions;
Some Caveats
useEffect will not be able to detect it's own influence to width and height
For example if you change the state hook without specifying initial values (eg const [dimensions, setDimensions] = useState({});), the height would read as zero when rendered because
no explicit height was set on the component via css
only content drawn before useEffect can be used to measure width and height
The only component contents are p tags with the height and width variables, when empty will give the component a height of zero
useEffect will not fire again after setting the new state variables.
This is probably not an issue in most use cases, but I thought I would include it because it has implications for window resizing.
Window Resizing
I also think there are some unexplored implications in the original question. I ran into the issue of window resizing for dynamically drawn components such as charts.
I'm including this answer even though it wasn't specified because
It's fair to assume that if the dimensions are needed by the application, they will probably be needed on window resize.
Only changes to state or props will cause a redraw, so a window resize listener is also needed to monitor changes to the dimensions
There's a performance hit if you redraw the component on every window resize event with more complex components. I found
introducing setTimeout and clearInterval helped. My component
included a chart, so my CPU spiked and the browser started to crawl.
The solution below fixed this for me.
code below, working example here
import React, { useRef, useLayoutEffect, useState } from 'react';
const ComponentWithDimensions = (props) => {
const targetRef = useRef();
const [dimensions, setDimensions] = useState({});
// holds the timer for setTimeout and clearInterval
let movement_timer = null;
// the number of ms the window size must stay the same size before the
// dimension state variable is reset
const RESET_TIMEOUT = 100;
const test_dimensions = () => {
// For some reason targetRef.current.getBoundingClientRect was not available
// I found this worked for me, but unfortunately I can't find the
// documentation to explain this experience
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
});
}
}
// This sets the dimensions on the first render
useLayoutEffect(() => {
test_dimensions();
}, []);
// every time the window is resized, the timer is cleared and set again
// the net effect is the component will only reset after the window size
// is at rest for the duration set in RESET_TIMEOUT. This prevents rapid
// redrawing of the component for more complex components such as charts
window.addEventListener('resize', ()=>{
clearInterval(movement_timer);
movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});
return (
<div ref={ targetRef }>
<p>{ dimensions.width }</p>
<p>{ dimensions.height }</p>
</div>
);
}
export default ComponentWithDimensions;
re: window resizing timeout - In my case I'm drawing a dashboard with charts downstream from these values and I found 100ms on RESET_TIMEOUT seemed to strike a good balance for me between CPU usage and responsiveness. I have no objective data on what's ideal, so I made this a variable.
As it was already mentioned, you can't get any element's dimensions until it is rendered to DOM. What you can do in React is to render only a container element, then get it's size in componentDidMount, and then render rest of the content.
I made a working example.
Please note that using setState in componentDidMount is an anti-pattern but in this case is fine, as it is exactly what are we trying to achieve.
Cheers!
Code:
import React, { Component } from 'react';
export default class Example extends Component {
state = {
dimensions: null,
};
componentDidMount() {
this.setState({
dimensions: {
width: this.container.offsetWidth,
height: this.container.offsetHeight,
},
});
}
renderContent() {
const { dimensions } = this.state;
return (
<div>
width: {dimensions.width}
<br />
height: {dimensions.height}
</div>
);
}
render() {
const { dimensions } = this.state;
return (
<div className="Hello" ref={el => (this.container = el)}>
{dimensions && this.renderContent()}
</div>
);
}
}
You cannot. Not reliably, anyway. This is a limitation of browser behavior in general, not React.
When you call $('#container').width(), you are querying the width of an element that has rendered in the DOM. Even in jQuery you can't get around this.
If you absolutely need an element's width before it renders, you will need to estimate it. If you need to measure before being visible you can do so while applying visibility: hidden, or render it somewhere discretely on the page then moving it after measurement.
There's an unexpected "gotcha" with #shane's approach for handling window resizing: The functional component adds a new event listener on every re-render, and never removes an event listener, so the number of event listeners grows exponentially with each resize. You can see that by logging each call to window.addEventListener:
window.addEventListener("resize", () => {
console.log(`Resize: ${dimensions.width} x ${dimensions.height}`);
clearInterval(movement_timer);
movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});
This could be fixed by using an event cleanup pattern. Here's some code that's a blend of #shane's code and this tutorial, with the resizing logic in a custom hook:
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
// Usage
function App() {
const targetRef = useRef();
const size = useDimensions(targetRef);
return (
<div ref={targetRef}>
<p>{size.width}</p>
<p>{size.height}</p>
</div>
);
}
// Hook
function useDimensions(targetRef) {
const getDimensions = () => {
return {
width: targetRef.current ? targetRef.current.offsetWidth : 0,
height: targetRef.current ? targetRef.current.offsetHeight : 0
};
};
const [dimensions, setDimensions] = useState(getDimensions);
const handleResize = () => {
setDimensions(getDimensions());
};
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useLayoutEffect(() => {
handleResize();
}, []);
return dimensions;
}
export default App;
There's a working example here.
This code doesn't use a timer, for simplicity, but that approach is further discussed in the linked tutorial.
As stated, it is a limitation of the browsers - they render in one go and "in one thread" (from JS perspective) between your script that manipulates the DOM, and between event handlers execution. To get the dimensions after manipulating / loading the DOM, you need to yield (leave your function) and let the browser render, and react to some event that rendering is done.
But try this trick:
You could try to set CSS display: hidden; position: absolute; and restrict it to some invisible bounding box to get the desired width. Then yield, and when the rendering is done, call $('#container').width().
The idea is: Since display: hidden makes the element occupy the space it would take if visible, the computation must be done in the background.
I am not sure if that qualifies as "before render".
Disclaimer:
I haven't tried it, so let me know if it worked.
And I am not sure how it would blend with React.
#Stanko's solution is nice and terse, but it's post-render. I have a different scenario, rendering a <p> element inside an SVG <foreignObject> (in a Recharts chart). The <p> contains text that wraps, and the final height of the width-constrained <p> is hard to predict. The <foreignObject> is basically a viewport and if too long it would block clicks/taps to underlying SVG elements, too short and it chops off the bottom of the <p>. I need a tight fit, the DOM's own style-determined height before the React render. Also, no JQuery.
So in my functional React component I create a dummy <p> node, place it to the live DOM outside the document's client viewport, measure it, and remove it again. Then use that measurement for the <foreignObject>.
[Edited with method using CSS classes]
[Edited: Firefox hates findCssClassBySelector, stuck with hardcoding for now.]
const findCssClassBySelector = selector => [...document.styleSheets].reduce((el, f) => {
const peg = [...f.cssRules].find(ff => ff.selectorText === selector);
if(peg) return peg; else return el;
}, null);
// find the class
const eventLabelStyle = findCssClassBySelector("p.event-label")
// get the width as a number, default 120
const eventLabelWidth = eventLabelStyle && eventLabelStyle.style ? parseInt(eventLabelStyle.style.width) : 120
const ALabel = props => {
const {value, backgroundcolor: backgroundColor, bordercolor: borderColor, viewBox: {x, y}} = props
// create a test DOM node, place it out of sight and measure its height
const p = document.createElement("p");
p.innerText = value;
p.className = "event-label";
// out of sight
p.style.position = "absolute";
p.style.top = "-1000px";
// // place, measure, remove
document.body.appendChild(p);
const {offsetHeight: calcHeight} = p; // <<<< the prize
// does the DOM reference to p die in garbage collection, or with local scope? :p
document.body.removeChild(p);
return <foreignObject {...props} x={x - eventLabelWidth / 2} y={y} style={{textAlign: "center"}} width={eventLabelWidth} height={calcHeight} className="event-label-wrapper">
<p xmlns="http://www.w3.org/1999/xhtml"
className="event-label"
style={{
color: adjustedTextColor(backgroundColor, 125),
backgroundColor,
borderColor,
}}
>
{value}
</p>
</foreignObject>
}
Ugly, lots of assumptions, probably slow and I'm nervous about the garbage, but it works. Note that the width prop has to be a number.
All the solutions I found on Stack overflow were either very slow, or out of date with modern React conventions. Then I stumbled across:
https://github.com/wellyshen/react-cool-dimensions
A React hook that measure an element's size and handle responsive components with highly-performant way, using ResizeObserver.
It's fast and works much better than the solutions I tried here.
import { useState, useEffect } from 'react'
const useContainerDimensions = containerRef => {
const getDimensions = () => ({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight
})
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setDimensions(getDimensions())
}
let dimensionsTimeout = setTimeout(() => {
if(containerRef.current) {
setDimensions(getDimensions())
}
}, 100)
window.addEventListener("resize", handleResize)
return () => {
clearTimeout(dimensionsTimeout)
window.removeEventListener("resize", handleResize)
}
}, [containerRef])
return dimensions
}
export default useContainerDimensions
You can use useContainerDimensions Custom hook. if you need width and height as pixel you can use clientWidth and clientHeight instead of offsetWidth and offsetHeight.
I have a react component:
React.createComponent({
render: function () {
return (<div className="some-component"></div>);
}
});
Some seconds after it renders, I want it to add a class from within the component. The class is for the purposes of revealing the component with an animation. I don't regard it as a real change of state, as it has no affect on the application other than to give the component an animated introduction, so I'm loathed to initiate it from outside of the component via a change of store/state.
How would I do this in simple terms? Something like:
{setTimeout(function () {
this.addClass('show'); //pseudo code
}, 1000)}
Obviously I could use jQuery, but that feels anti-React, and prone to side-effects.
I don't regard it as a real change of state, as it has no affect on the application other than to give the component an animated introduction
A change of state in the component seems the natural and proper solution for this scenario. A change in a component state triggers a re-render, which is exactly what you need here. Consider that we're talking about the state of your component, not of your application here.
In React, you don't deal with the DOM directly (e.g. by using jQuery), instead your component state should "drive" what's rendered, so you let React "react" to changes in state/props and update the DOM for you to reflect the current state:
React.createComponent({
componentDidMount () {
this.timeoutId = setTimeout(function () {
this.setState({show: true});
}.bind(this), 1000);
}
componentWillUnmount () {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
render: function () {
return (<div className={this.state.show ? 'show' : null}></div>);
}
});
When using setTimeout in React you need to be careful and make sure to cancel the timeout when the component gets unmounted, otherwise your timeout callback function will run anyway if the timeout is still pending and your component gets removed.
If you need to perform an initial mount animation or more complicated animations, consider using ReactCssTransitionGroup, which handles timeouts and other things for you out of the box: https://facebook.github.io/react/docs/animation.html
Here's a fiddle of what I want to do: https://jsfiddle.net/s7s07chm/7/
But I want to do this with react instead of jquery. Basically, I put the className of the element in the state, and on componentDidMount I update the className to initiate the transition.
But this isn't working. The component is just rendering with the transitioned state. In other words, instead of sliding down, it appears at the bottom from the beginning
Am I doing this wrong?
If so, is there another way to accomplish this?
here's the actual code
getInitialState: function() {
return {
childClass: 'child'
};
},
componentDidMount: function() {
this.setState({
childClass: 'child low'
});
},
the reason that won't work is because your DOM won't be updated until the component is mounted. So the class you're assigning with getInitialState will never appear in the DOM, but the one you set with componentDidMount will. As Ray mentioned, you should take a look at ReactCSSTransitionGroup.
componentDidMount is called after React hands them off to the browser, but the browser might not have finished drawing.
You can however use requestAnimationFrame to ensure setState is called after the browser has finished drawing the component.
componentDidMount() {
requestAnimationFrame(() => this.setState({
childClass: 'child low'
}))
}
Has somebody found a good way to animate state transitions?
The router immediately removes the view from the DOM. The problem with that is that I can't defer that until the end of the animation. Note: I'm using v1.0.0-pre.4.
Billy's Billing just released an Ember module that supports animated transitions.
I'll expand on Lesyk's answer. If you need to apply it to multiple views in a DRY way, you can create a customization class like this:
App.CrossfadeView = {
didInsertElement: function(){
//called on creation
this.$().hide().fadeIn(400);
},
willDestroyElement: function(){
//called on destruction
this.$().slideDown(250);
}
};
And then in your code you apply it on your various view classes. As Ember depends on jQuery you can use pretty much any jQuery animation.
App.IndexView = Ember.View.extend(App.CrossfadeView);
App.PostView = Ember.View.extend(App.CrossfadeView);
I know this is pretty old, but the best solution for this context-specific animation today is probably ember liquid fire.
It allows you to do things like this in a transition file:
export default function(){
this.transition(
this.fromRoute('people.index'),
this.toRoute('people.detail'),
this.use('toLeft'),
this.reverse('toRight')
);
};
Ran into this same requirement on my app. Tried Ember Animated Outlet, but didn't give the granularity I needed (element specific animations).
The solution that worked for me was as follows --
Change linkTo to be an action
{{#linkTo "todos"}}<button>Todos</button>{{/linkTo}}
Becomes...
<a href="#/todos" {{action "goToTodos"}}><button>Todos</button></a>
Create Method for goToTodos in current controller
App.IndexController = Ember.Controller.extend({
goToTodos: function(){
// Get Current 'this' (for lack of a better solution, as it's late)
var holdThis = this;
// Do Element Specific Animation Here
$('#something').hide(500, function(){
// Transition to New Template
holdThis.transitionToRoute('todos');
});
}
});
Finally -- To animate in elements on the Todos Template, use didInsertElement on the view
App.TodosView = Ember.View.extend({
didInsertElement: function(){
// Hide Everything
this.$().hide();
// Do Element Specific Animations Here
$('#something_else').fadeIn(500);
}
});
So far, this is the most elegant solution I've found for element specific animations on transition. If there is anything better, would love to hear!
I've found another drop-in solution that implements animations in Views: ember-animate
Example:
App.ExampleView = Ember.View.extend({
willAnimateIn : function () {
this.$().css("opacity", 0);
},
animateIn : function (done) {
this.$().fadeTo(500, 1, done);
},
animateOut : function (done) {
this.$().fadeTo(500, 0, done);
}
}
Demo: author's personal website
App.SomeView = Ember.View.extend({
didInsertElement: function(){
//called on creation
this.$().hide().fadeIn(400);
},
willDestroyElement: function(){
//called on destruction
this.$().slideDown(250)
}
});