Create responsive image display in React - css

I'm new to frontend, react and have a question on display responsive images.
Inside a React component, I have fetched some image info from an API endpoint and stored them inside a React.useRef in order to display them in a carousel. Like:
const carouselItem = React.useRef<Array<ImageTypeA>>([]);
apiResponse.forEach((image) => {
carouselItem.current.push({
images: (
<img
id={`${image.title}`}
src={image.imageUrl}
),
});
});
But there are two different sizes of carousels I need to display in the same component with the same set of images (In this case carouselItem).
For the two carousels, I want to display a smaller set of images and a larger set of images. And they should display according to the size of the screen (responsive).
const CarouselDiv1 = styled.div`
img: [];
`;
const CarouselDiv2 = styled.div`
img: [];
`;
... return method
return (
<div>
<CarouselDiv1>
// display smaller image carousel
</CarouselDiv1>
<CarouselDiv2>
// display larger image carousel
</CarouselDiv2>
</div>
)
I'm thinking of creating two styled components for each carousel and displaying them inside the return function. But I'm not sure how to do it or is there a better way to do it? Thanks

You can do something like this.
export default function App() {
const [isDesktop, setDesktop] = useState(window.innerWidth > 500);
const updateMedia = () => {
console.log(window.innerWidth);
setDesktop(window.innerWidth > 500);
};
useEffect(() => {
window.addEventListener("resize", updateMedia);
return () => window.removeEventListener("resize", updateMedia);
});
return (
<div>
{isDesktop ? (
<div>higher then 500px</div>
) : (
<div>lower then 500px</div>
)}
</div>
);
}
Or you can use the React-responsive package.
https://www.npmjs.com/package/react-responsive

Related

How do you style your React.js components differently depending on where you are using them in your application?

Let's say you have a navbar and when you're using this component on your homepage you want it to have a certain background color and display property, but when you use that same navbar component on another page in your application you want to change these CSS properties. Seeing as the component has one CSS file linked how would you change the style of a component depending on where it is being rendered?
My personal favourite method nowadays is styled components. Your component might look something like this:
// NavBar.js
import styled from 'styled-components'
const StyledDiv = styled.div`
width: 100%;
height: 2rem;
background-color: ${props => props.bgColor};
`
const NavBar = (bgColor) => {
return <StyledDiv bgColor={bgColor}>
}
Then to use it in your different contexts you simply pass the color prop:
// homepage.js
<NavBar bgColor="red" />
// otherpage.js
<NavBar bgColor="#123ABC" />
Styled components are becoming a very popular way of doing things, but be aware that there are a huge array of ways you can do this.
https://styled-components.com/
(Code not tested)
Well If you just want to use plain CSS then you can change the className based on route so the styles also changes.
Example:
import { useLocation } from "react-router-dom";
const Navigation = () => {
let location = useLocation();
...
return(
<nav className={location.pathname === "/home" ? "homepage-navbar" : "default-navbar"}>
...
</nav>
)
}
You can write longer condition for multiple pages as well.
Other better thing you can do is pass the location.pathname and value of className as prop.
import { useLocation } from "react-router-dom";
const Home = () => {
let location = useLocation();
...
return (
<>...
<Navigation location={location.pathname} styleClass={"homepage-navbar"}/>
</>
)
}
const Navigation = ({location, styleClass}) => {
...
return(
<nav className={location === "/home" ? styleClass : "default-navbar"}>
...
</nav>
)
}
So now you can pass different values for className from different components and get different styles for the navbar.

Why scrollWidth returns the same value of offsetWidth even when content overflows?

I'm trying to create a simple drag n drop carousel.
In order to set the constraints of the inner carousel, I need to calculate the difference between scrollWidth and offsetWidth. Unexpectedly, I get the same value for both. This shouldn't be happening, since the content overflows its parent div.
const fotos = [picOne, picTwo, picThree, picFour]
export const App = () => {
const [width, setWidth] = useState(0)
const ref = useRef(null)
useEffect(() => {
setWidth(ref.current.scrollWidth - ref.current.offsetWidth)
}, [])
return (
<>
<div className="carousel" ref={ref}>
<motion.div className="carousel-insider"
drag="x"
dragConstraints={{right: 0, left: -width}}>
{fotos.map((foto, index) => <img key={index} src={foto}/>)}
</motion.div>
</div>
</>
)
}
Carousel code:
https://jsfiddle.net/oa6tLrfq/
What am I doing wrong here?
Ok, I've come up with a work around.
What caused that issue?
All elements widths' were dependent upon html and body widths. This was caused by defining all widths as a percentage. Somehow, this caused a delay between rendering the component and scrollWidth getting, in fact, the correct value.
What's the work around?
You can use a setTimeout inside useEffect to delay its beginning. 3 seconds worked for me.
useEffect(() => {
setTimeout(setWidth(ref.current.scrollWidth - ref.current.offsetWidth)}, 3), [])
You can wrap img with a div and set a fixed width for the div.
Add ref.current to the array in useEffect so that when page fully loads scrollWidth and offsetWidth will be updated with thier actual values thus causing useEffect to re-run.
useEffect(() => {
setWidth(ref.current.scrollWidth - ref.current.offsetWidth)
}, [ref.current])

Add a class to the root container in the Wordpress gutenberg editor

The Wordpress gutenberg editor has a root container for content, identified with the class "is-root-container". I'd like to add classes to this container when the preview changes (between Mobile, Tablet, Desktop) so that I can style the editor content to simulate the respective responsive CSS.
I haven't been able to figure out a way to do this, so I have a workaround to add the classes to the wrapper of every block using the editor.BlockListBlock filter. This isn't particular efficient though, especially with a large number of blocks.
This is what I'm currently working with, but it will run for every single block, which isn't so efficient.
import htm from 'https://unpkg.com/htm?module'
const html = htm.bind(wp.element.createElement);
const { createHigherOrderComponent } = wp.compose;
const { useSelect } = wp.data;
const { addFilter } = wp.hooks;
const withPreviewDeviceClassname = createHigherOrderComponent(
( BlockListBlock ) => {
return ( props ) => {
const previewMode = useSelect( (select) => select('core/edit-post').__experimentalGetPreviewDeviceType() );
const className = 'tw ' + (previewMode == 'Tablet' ? 'tablet' : (previewMode == 'Desktop' ? 'desktop tablet' : '') );
return (
html`<${BlockListBlock}
...${props}
className=${className}
/>`
);
};
},
'withPreviewDeviceClassname'
);
addFilter(
'editor.BlockListBlock',
'cms/blocklist-plugin',
withPreviewDeviceClassname
);

How can I use bootstrap grid mapping an array of images?

I'm building a portfolio website with gatsby.js. All photos are posted in wordpress, fetched by graphQL and rendered to the website.
I'm trying to use bootstrap grid to organize the photos and make it responsive, but because graphQL return an array with all images fetched from wordpress posts, I can't set a div with class='row' as I'm using array.map. And I don't know how to solve it.
From graphQL i'm setting resolution to width=300px and height=300px.
That's the only way I found to organize sizes, as long as I can't use class row and all images are considered in one row. The problem is that the photo size is not responsive, so it will always be 300X300...
I'd like a way to make it a grid system as it's suppose to work... So when I resize the window, all photos are organized and resized.
const IndexPage = () => {
const data = useStaticQuery(graphql`
query {
allWordpressPost {
edges {
node {
title
featured_media {
localFile {
childImageSharp {
resolutions(width: 300, height: 300) {
src
width
height
}
}
}
}
}
}
}
}
`);
const imagesResolutions = data.allWordpressPost.edges.map(
(edge) => edge.node.featured_media.localFile.childImageSharp.resolutions
);
return (
<Layout>
<Jumbotron />
<div className="container">
<h1 className="my-5 text-center">Portfolio</h1>
{imagesResolutions.map((imageRes) => (
<Img className="col-sm-6 col-lg-4 img-rounded img" resolutions={imageRes} key={imageRes.src} />
))}
</div>
</Layout>
);
};
If you split your data.allWordpressPost.edges array into a chunked array, you can loop through the outer array to render rows, and each of the inner arrays to render cols.
For a 3 column bootstrap grid, you want to pass in a size value of 3 (it's the 2nd param of lodash.chunk in this example). This ensures the length of each chunk is 3.
Here is a simple example ignoring the use of graphql, childImageSharp, and gatsby-image.
import ReactDOM from 'react-dom';
import React, { Component } from 'react';
import arrayChunk from 'lodash.chunk';
import 'bootstrap/dist/css/bootstrap.min.css';
const IndexPage = () => {
const rawData = [1, 2, 3, 4, 5, 6];
const chunkedData = arrayChunk(rawData, 3)
return (
<div>
<div className="container">
{chunkedData.map((row, rowIndex) => {
return (<div key={rowIndex} className="row">{
row.map((col, colIndex) => {return (<div key={colIndex} className="col-sm">{col}</div>)})
}</div>)
}
)}
</div>
</div>
);
};
ReactDOM.render(<IndexPage />, document.getElementById('root'));
stackblitz

How to get a react component's size (height/width) before render?

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.

Resources