Update UI when useRef Div Width Changes - reactjs

I have a useRef attached to a div. I need to update my UI when the div's width changes. I can access this using ref.current.innerWidth, however, when its width changes, it doesn't update other elements that depend on ref.current.innerWidth.
How can I do this?
CODE:
let ref = useRef();
return (
<>
<Box resizable ref={ref}>
This is a resizable div
</Box>
<Box width={ref.current.innerWidth}>
This box needs the same with as the resizable div
</Box>
</>
);

You could use a ResizeObserver. Implemented like so, it will set the width everytime the size of the ref changes:
let ref = useRef()
const [width, setwidth] = useState(0)
useEffect(() => {
const observer = new ResizeObserver(entries => {
setwidth(entries[0].contentRect.width)
})
observer.observe(ref.current)
return () => ref.current && observer.unobserve(ref.current)
}, [])
return (
<>
<Box ref={ref}>
This is a resizable div
</Box>
<Box width={width}>
This box needs the same with as the resizable div
</Box>
</>
)

You should make a lifecycle using useEffect and useState and event listener on window to listen the data change then re-render your component based on that.
CodeSandBox
const [size, setSize] = useState(null);
let ref = useRef();
const updateDimensions = () => {
console.log(ref.current.clientWidth);
if (ref.current) setSize(ref.current.clientWidth);
};
useEffect(() => {
window.addEventListener("resize", updateDimensions);
setSize(ref.current.clientWidth);
return () => {
console.log("dismount");
window.removeEventListener("resize", updateDimensions);
};
}, []);
return (
<>
<div ref={ref}>This is a resizable div</div>
<div
style={{
width: size,
border: "1px solid"
}}
>
This div needs the same with as the resizable div
</div>
</>
);

For anyone looking for a reusable logic and a Typescript support, I created the below custom hook based on #fredy's awesome answer, and also fixed some issues I've found in his answer:
import { useState, useRef, useEffect } from "react";
export const useObserveElementWidth = <T extends HTMLElement>() => {
const [width, setWidth] = useState(0);
const ref = useRef<T>(null);
useEffect(() => {
const observer = new ResizeObserver((entries) => {
setWidth(entries[0].contentRect.width);
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
ref.current && observer.unobserve(ref.current);
};
}, []);
return {
width,
ref
};
};
Then, import useObserveElementWidth, and use it like this:
const YourComponent = () => {
const { width, ref } = useObserveElementWidth<HTMLDivElement>();
return (
<>
<Box resizable ref={ref}>
This is a resizable div
</Box>
<Box width={width}>
This box needs the same with as the resizable div
</Box>
</>
);
};
I've created an example codesandbox for it.

Related

useMemo hook not working with map elements

I am using useMemo hook to render map items.I added items parameter to useMemo hook, based on items change it will render. But changing loading state and items change, Item custom component rendering twice. Am i doing any mistake on using useMemo hook, please correct me.
Home:
import React, { useState, useEffect, useMemo } from "react";
import Item from "./Item";
const array = [1];
const newArray = [4];
const Home = () => {
const [items, setItems] = useState(array);
const [loading, setLoading] = useState(false);
const [dataChange, setDataChange] = useState(1);
const renderItems = (item, index) => {
return (
<div key={item}>
<Item id={item}></Item>
</div>
);
};
useEffect(() => {
if (dataChange === 2) {
setLoading(true);
setTimeout(() => {
setLoading(false);
setItems(newArray);
}, 3000);
}
}, [dataChange]);
const memoData = useMemo(() => {
return <div>{items.map(renderItems)}</div>;
}, [items]);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<input
onClick={() => {
setDataChange(2);
}}
style={{ height: 40, width: 100, margin: 20 }}
type="button"
value="ChangeItem"
></input>
<div>{loading ? <label>{"Loading"}</label> : <div>{memoData}</div>}</div>
</div>
);
};
export default React.memo(Home);
Item:
import React,{useEffect} from "react";
const Item = (props) => {
console.log("props", props);
useEffect(() => {
// call api with props.id
}, [props]);
return <div>Hello world {props.id}</div>;
};
export default React.memo(Item);
Result:
first time :
props {id: 1}
After click :
props {id: 1}
props {id: 4}
There are a few things which are not right in the code above.
key should be passed to the parent element in an array iteration - in your case the renderItems should pass the key to the div element
you are turning off the loading state before updating the items array, switching the two setState expressions will resolve your case most of the time although setState is an async function and this is not guaranteed
if a constant or a function is not tightly coupled to the component's state it is always best to extract it outside the component as is the case with renderItems
Here's why there is one more console.log executed
also should keep in mind that memoization takes time and you would want to keep it as efficient as possible hence you can totally skip the useMemo with a React.memo component which takes care of the array because it is kept in the state and it's reference won't change on rerender if the state remains the same
const array = [1];
const newArray = [4];
const Home = () => {
const [items, setItems] = useState(array);
const [loading, setLoading] = useState(false);
const [dataChange, setDataChange] = useState(1);
useEffect(() => {
if (dataChange === 2) {
setLoading(true);
setTimeout(() => {
setItems(newArray);
setLoading(false);
}, 3000);
}
}, [dataChange]);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<input
onClick={() => {
setDataChange(2);
}}
style={{ height: 40, width: 100, margin: 20 }}
type="button"
value="ChangeItem"
></input>
<div>
{loading ? <label>{"Loading"}</label> : <ItemsMemo items={items} />}
</div>
</div>
);
};
const renderItems = (item) => {
return (
<span key={item} id={item}>
{item}
</span>
);
};
const Items = ({ items }) => {
console.log({ props: items[0] });
return (
<div>
Hello world <span>{items.map(renderItems)}</span>
</div>
);
};
const ItemsMemo = React.memo(Items);
UPDATE
This codesandbox shows that useMemo gets called only when the items value changes as it is supposed to do.
useCustomHook:
import { useEffect, useRef } from "react"
export default function useUpdateEffect(callback, dependencies) {
const firstRenderRef = useRef(true)
useEffect(() => {
if (firstRenderRef.current) {
firstRenderRef.current = false
return
}
return callback()
}, dependencies)
}
Create these custom hooks in your project and use them. It will prevent your first calling issue.

How to update state for device width using Hooks in react

I am working on a React project, according to my scenario, a have button in my project and I have written two functions to change background color. First function will call if device width is less than or equal to 320px. Second function will call if device width is === 768px. but here the problem is when my device width is 320px when I click the button at that time the background color is changing to red here the problem comes now when I go to 768px screen then initially my button background color has to be in blue color, but it is showing red. to show button background color blue I have to update state for device size.
So someone please help me to achieve this.
This is my code
This is App.js
import React, { useState } from 'react';
import './App.css';
const App = () => {
const [backGroundColor, setBackGroundColor] = useState(null)
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
const changeBackGroundColorForMobile = () => {
if(deviceSize <= 320) {
setBackGroundColor({
backgroundColor: 'red'
})
}
}
const changeBackGroundColorForTab = () => {
if(deviceSize === 768) {
setBackGroundColor({
backgroundColor: 'green'
})
}
}
return (
<div className='container'>
<div className='row'>
<div className='col-12'>
<div className='first'>
<button onClick={() => {changeBackGroundColorForMobile(); changeBackGroundColorForTab() }} style={backGroundColor} className='btn btn-primary'>Click here</button>
</div>
</div>
</div>
</div>
)
}
export default App
If you have any questions please let me know thank you.
You're always running two functions. Don’t need that.
You’re updating the deviceSize only on the initial render. You have to update that in orientation change also.
Set the default colour always to blue.
import React, { useEffect, useState } from "react";
import "./App.css";
const App = () => {
const [backGroundColor, setBackGroundColor] = useState({
backgroundColor: "blue"
}); // Initialize bgColor with "blue"
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
useEffect(() => {
const resizeW = () => changeDeviceSize(window.innerWidth);
window.addEventListener("resize", resizeW); // Update the width on resize
return () => window.removeEventListener("resize", resizeW);
});
const changeBgColor = () => {
let bgColor = "blue";
if (deviceSize === 768) {
bgColor = "green";
} else if (deviceSize <= 320) {
bgColor = "red";
}
setBackGroundColor({
backgroundColor: bgColor
});
}; // Update the bgColor by considering the deviceSize
return (
<div className="container">
<div className="row">
<div className="col-12">
<div className="first">
<button
onClick={changeBgColor}
style={backGroundColor}
className="btn btn-primary"
>
Click here
</button>
</div>
</div>
</div>
</div>
);
};
export default App;
I would follow the previous advice to get the width and if you have lots of child components that rely on the width then I would suggest using the useContext hook so you don't have to keep passing the window data as a prop.
You can use useWindowSize() hook to get window width. And whenever width changes you can change background color by calling the functions in useEffect()
import { useState, useEffect } from "react";
// Usage
function App() {
const [backGroundColor, setBackGroundColor] = useState(null)
const { width } = useWindowSize();
useEffect(()=>{
if(width <= 320) {
changeBackGroundColorForMobile();
}
if(width === 768) {
changeBackGroundColorForTab()
}
}, [width])
const changeBackGroundColorForMobile = () => {
setBackGroundColor({
backgroundColor: 'red'
})
}
const changeBackGroundColorForTab = () => {
setBackGroundColor({
backgroundColor: 'green'
})
}
return (
<div className='container'>
<div className='row'>
<div className='col-12'>
<div className='first'>
<button style={backGroundColor} className='btn btn-primary'>Click here</button>
</div>
</div>
</div>
</div>
)
}
// Hook
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
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;
}
You can use useEffect hook to add an event listener to window resize.
export default function App() {
const [bgClassName, setBgClassName] = useState("btn-primary");
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function updateWidth() {
setWidth(window.innerWidth);
if(window.innerWidth === 768){
setBgClassName('btn-primary')
}
}
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}, []);
const changeColor = () => {
if (window.innerWidth < 320) {
setBgClassName("btn-danger");
} else if (window.innerWidth === 768) {
setBgClassName("btn-success");
}
};
console.log(width);
return (
<div className="container">
<div className="row">
<div className="col-12">
<div className="first">
<button
onClick={() => changeColor()}
className={`btn ${bgClassName}`}
>
Click here
</button>
</div>
</div>
</div>
</div>
);
}

Passing Property To Another Component - React

I need to pass "notecards" (an array) down from "Notecard.js" to "LoadQuestions.js". Console log shows that it is passing, but when I use {notecards} within the "return" it errors as "undefined". Could you please take a look?
Notecard.js (without the imports):
const useStyles = makeStyles((theme) => ({
root: {
maxWidth: 345,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
}));
export default function Notecard( {notecards} ) {
const classes = useStyles();
const next = () => {
console.log('Next Button Clicked')
};
const previous = () => {
console.log('Back Button Clicked')
};
const hint = () => {
console.log('Hint Button Clicked')
};
console.log({notecards});
return (
<Card className={classes.root}>
<div id="cardBody">
<CardHeader
title="Kate Trivia"
// subheader="Hint: In the 20th century"
/>
<CardContent>
<LoadQuestions notecards={notecards}/>
</CardContent>
</div>
</Card>
);
}
LoadQuestions.js (without imports)
const {useState} = React;
export default function LoadQuestions( {notecards} ) {
const [currentIndex, setCounter] = useState(0);
console.log({notecards});
return (
<div>
<Toggle
props={notecards}
render={({ on, toggle }) => (
<div onClick={toggle}>
{on ?
<h1>{props.notecards} hi</h1> :
<h1>{this.props[currentIndex].backSide}</h1>
}
</div>
)}
/>
<button onClick={() => {
console.log({notecards})
if (currentIndex < (this.props.length-1)) {
setCounter(currentIndex + 1);
} else {
alert('no more cards')
}
}}>Next Card
</button>
<button onClick={() => {
if (currentIndex > 0 ) {
setCounter(currentIndex -1);
} else {
alert('no previous cards')
}
}}>Previous Card
</button>
</div>
);
}
Thanks in advance!
That's all the details I have for you, but stack overflow really wants me to add more before it will submit. Sorry!
You should check if props exists, first time it renders the component it has no props so it shows undefined.
First i must say you destructured notecards out, so no need to use props.
If you want to use props you should change
({notecards}) to (props)
and if not you can directly use notecards since it is destructured
I suggest you two ways
adding question mark to check if exists
<h1>{props?.notecards} hi</h1>//in the case you want to use props
or
add the props in a if statement
<h1>{props.notecards?props.notecards:''} hi</h1> // if notecards is destructured remove the "props."

A resizable `antd` Drawer?

I would like to provide a way to make an antd Drawer resizable ?
I read a popular answer specifically for material-ui/Drawer but I am looking to do something very similar with antd.
Does anyone have a similar antd example - or have a better idea how to handle info getting chopped off at side of the drawer.
You can extend the width of Drawer by specifying it on the width props. If you don't want to extend it but you want the content to be still fit, you can set the width on bodyStyle prop and use overflow: "auto":
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
visible={isDrawerVisible}
bodyStyle={{
width: 400,
overflow: "auto"
}}
onClose={toggleDrawerVisible}
>
I also made a resizable drawer based on the link that you provide in antd version (react hooks version answer).
ResizableDrawer.jsx
import React, { useState, useEffect } from "react";
import { Drawer } from "antd";
let isResizing = null;
const ResizableDrawer = ({ children, ...props }) => {
const [drawerWidth, setDrawerWidth] = useState(undefined);
const cbHandleMouseMove = React.useCallback(handleMousemove, []);
const cbHandleMouseUp = React.useCallback(handleMouseup, []);
useEffect(() => {
setDrawerWidth(props.width);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.visible]);
function handleMouseup(e) {
if (!isResizing) {
return;
}
isResizing = false;
document.removeEventListener("mousemove", cbHandleMouseMove);
document.removeEventListener("mouseup", cbHandleMouseUp);
}
function handleMousedown(e) {
e.stopPropagation();
e.preventDefault();
// we will only add listeners when needed, and remove them afterward
document.addEventListener("mousemove", cbHandleMouseMove);
document.addEventListener("mouseup", cbHandleMouseUp);
isResizing = true;
}
function handleMousemove(e) {
let offsetRight =
document.body.offsetWidth - (e.clientX - document.body.offsetLeft);
let minWidth = 256;
let maxWidth = 600;
if (offsetRight > minWidth && offsetRight < maxWidth) {
setDrawerWidth(offsetRight);
}
}
return (
<Drawer {...props} width={drawerWidth}>
<div className="sidebar-dragger" onMouseDown={handleMousedown} />
{children}
</Drawer>
);
};
export default ResizableDrawer;
and to use it:
import ResizableDrawer from "./ResizableDrawer";
<ResizableDrawer
title="Resizable Drawer"
placement="right"
closable={false}
visible={isResizableDrawerVisible}
onClose={toggleResizableDrawerVisible}
>
...
</ResizableDrawer>
See working demo here:
Have two states for tracking the width of the drawer and whether or not the drawer is being resized (isResizing).
Add two event listeners on the global document where it will listen for mousemove and mouseup. The mousemove event will resize the drawer, only if isResizing is true. And the mouseup event will set isResizing to false.
Add a div in your drawer that acts as the draggable border for making the drawer resizable. This div will listen for a mousedown event, which will set the state of isResizing to true.
Here's the code that has been improved upon from the basic drawer demo from antd's website.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Drawer, Button } from "antd";
const App = () => {
const [visible, setVisible] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [width, setWidth] = useState(256);
const showDrawer = () => {
setVisible(true);
};
const onClose = () => {
setVisible(false);
};
const onMouseDown = e => {
setIsResizing(true);
};
const onMouseUp = e => {
setIsResizing(false);
};
const onMouseMove = e => {
if (isResizing) {
let offsetRight =
document.body.offsetWidth - (e.clientX - document.body.offsetLeft);
const minWidth = 50;
const maxWidth = 600;
if (offsetRight > minWidth && offsetRight < maxWidth) {
setWidth(offsetRight);
}
}
};
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
});
return (
<>
<Button type="primary" onClick={showDrawer}>
Open
</Button>
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
onClose={onClose}
visible={visible}
width={width}
>
<div
style={{
position: "absolute",
width: "5px",
padding: "4px 0 0",
top: 0,
left: 0,
bottom: 0,
zIndex: 100,
cursor: "ew-resize",
backgroundColor: "#f4f7f9"
}}
onMouseDown={onMouseDown}
/>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
</>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
And here's the demo of the code:
DEMO

React.memo isn't working - what am I missing?

I'm in the process of refactoring some of our components so I'm trying to incorporate memoization as some components may re-render with the same values (for example, hotlinked image URLs unless they are the same).
I have a simple component:
const CardHeader = props => {
// img is a stringand showAvatar is a boolean but it's always true
const { ..., showAvatar, img } = props;
return (
<CardHeader>
<ListItem>
// AvatarImage shouldn't re-render if img is the same as previous
{showAvatar && <AvatarImage img={img} />
</ListItem>
</CardHeader>
);
}
And then the AvatarImage:
const AvatarImage = React.memo(props => {
console.log("why is this still re-rendering when the img value hasn't changed?");
const { img } = props;
return (
<ListItemAvatar>
{img ?
<Avatar src={img} />
:
<Avatar>
Some initials
</Avatar>
}
</ListItemAvatar>
);
});
I have also tried passing in second argument of memo:
(prevProps, nextProps) => {
return true; // Don't re-render!
}
But the console.log still shows every time. I'm obviously missing something here or don't quite understand how this works. This component is a few levels down, but it passes in the img if it's available every time so I'd expect it to know that if the img was passed in the previous render and it's the same it knows not to re-render it again but for some reason it does?
Thanks all. It's much appreciated.
Well it is either showAvatar is not always true or CardHeader ListItem component magically decides whether show children or not
Example
const { useState, useEffect, memo, createContext, useContext } = React;
const getAvatars = () => Promise.resolve([
{
src: 'https://i.picsum.photos/id/614/50/50.jpg'
},
{
src: 'https://i.picsum.photos/id/613/50/50.jpg'
}
])
const Avatar = ({src}) => {
console.log('avatar render');
return <img src={src} alt="avatar"/>
}
const MemoAvatarToggle = memo(({src}) => {
console.log('memo avatar with \'expression &&\' render');
return <div>
{src ? <img src={src} alt="avatar"/> : <div>Test </div>}
</div>
})
const CardHeader = ({children}) => {
const luck = Boolean(Math.floor(Math.random() * 1.7));
return <div>
{luck && children}
</div>
}
const ListItem = ({children}) => {
return <div>
{children}
</div>
}
const ShowAvatarContext = createContext()
const App = (props) => {
const [avatars, setAvatars] = useState([]);
const [toggle, setToggle] = useState(false);
const [showAvatar, setShowAvatar] = useContext(ShowAvatarContext);
useEffect(() => {
let isUnmounted = false;
let handle = null;
setTimeout(() => {
if(isUnmounted) {
return;
}
setShowAvatar(true);
}, 500);
getAvatars()
.then(avatars => {
if(isUnmounted) {
return;
}
setAvatars(avatars)
})
const toggle = () => {
setToggle(prev => !prev);
handle = setTimeout(toggle, 1000);
//setShowAvatar(prev => !prev);
}
handle = setTimeout(toggle, 1000);
return () => {
isUnmounted = true;
clearTimeout(handle);
}
}, []);
return <div>
<CardHeader>
<ListItem>
{showAvatar && avatars.map((avatar, index) => <MemoAvatarToggle key={index} src={avatar.src}/>)}
</ListItem>
</CardHeader>
{toggle ? 1 : 0}
</div>
}
const ShowAvatarProvider = ({children}) => {
const state = useState(false);
return <ShowAvatarContext.Provider value={state}>
{children}
</ShowAvatarContext.Provider>
}
ReactDOM.render(
<ShowAvatarProvider>
<App/>
</ShowAvatarProvider>,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
Do you have StrictMode enabled? That will cause a component memoized with React.memo to render twice.
More information:
https://reactjs.org/docs/strict-mode.html
My React Component is rendering twice because of Strict Mode
memo will not block re-render if the component is actually referenced the changing props or functions.
In your scenario your AvatarImage referenced img, in this case if parent's state's img is changed, then your component will be re-rendered.
Alternatively, if your parent is just changed other props instead of img, then the AvatarImage will NOT be re-rendered.
Alternatively, if any props but you didn't add memo to AvatarImage, then AvatarImage will be re-rendered for each of parent's state updated.
You need to memorized img props too.
const CardHeader = props => {
const { showAvatar, img } = props;
const updatedIMG = React.useMemo(() => img, []);
return (
<CardHeader>
<ListItem>
{showAvatar && <AvatarImage img={updatedIMG} />
</ListItem>
</CardHeader>
);
}
Above one would work

Resources