Setting state without re-rendering with useEffect not working - reactjs

I simply need my state value to change based on screen sizing live time. Even though my screen size changes, my count value stays the same unless I reload the page. this needs to update live for better responsive design. Here is my code. Thanks!
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive'
const MyCarousel = () => {
useEffect(() => {
loadMediaQuery();
}, []);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
if (phone)
setCount(2)
if (desktop)
setCount(3)
}
const tablet = useMediaQuery({
query: '(max-width: 876px)'
})
const phone = useMediaQuery({
query: '(max-width: 576px)'
})
const desktop = useMediaQuery({
query: '(min-width: 876px)'
})
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}

That's because your useEffect has no dependencies so it loads one time only after the component has been mounted.
To fix that you should have the following code:
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive';
const MyCarousel = () => {
const tablet = useMediaQuery({
query: '(max-width: 876px)'
});
const phone = useMediaQuery({
query: '(max-width: 576px)'
});
const desktop = useMediaQuery({
query: '(min-width: 876px)'
});
useEffect(() => {
loadMediaQuery();
}, [tablet, phone, desktop]);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
else if (phone)
setCount(2)
else if (desktop)
setCount(3)
}
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}

useEffect means: runs the callback function that's passed to useEffect after this component is rendered, and since you passed an empty array as the 2nd argument, it means useEffect is executed once, only the first time when this component gets rendered (and not when the state changes) , since your function call is inside useEffect, it will work only when you reload the page, you can either add the variables to the empty array like Mohamed Magdy did, or simply call loadMediaQuery again outside useEffect

Expanding on the custom hook provided in this answer, you can do something like
const [width, height] = useWindowSize();
useEffect(() => {
loadMediaQuery();
}, [width]);
That useEffect function says: Execute loadMediaQuery() every time the value of width changes.

Related

React: Trigger a function when a child asynchronously updates its DOM after rendering

Within ParentComponent, I render a chart (ResponsiveLine). I have a function (calculateHeight) calculating the height of some DOM elements of the chart.
To work fine, my function calculateHeight have to be triggered once the chart ResponsiveLine is rendered.
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
How to trigger my function calculateHeight once the chart ResponsiveLine is done rendering?
Here's a simplified code
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
setMarginBottom(calculateHeight(myref));
});
return (
<div ref={myref}>
<ResponsiveLine marginBottom={marginBottom}/>
</div>)
}
EDIT
I can't edit the child ResponsiveLine, it's from a library
You can use the ResizeObserver API to track changes to the dimensions of the box of the div via its ref (specifically the height, which is the block size dimension for content which is in a language with a horizontal writing system like English). I won't go into the details of how the API works: you can read about it at the MDN link above.
The ResponsiveLine aspect of your question doesn't seem relevant except that it's a component you don't control and might change its state asynchronously. In the code snippet demonstration below, I've created a Child component that changes its height after 2 seconds to simulate the same idea.
Code in the TypeScript playground
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.5/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom/client';
// import {useEffect, useRef, useState, type ReactElement} from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {useEffect, useRef, useState} = React;
// You didn't show this function, so I don't know what it does.
// Here's something in place of it:
function calculateHeight (element: Element): number {
return element.getBoundingClientRect().height;
}
function Child (): ReactElement {
const [style, setStyle] = useState<React.CSSProperties>({
border: '1px solid blue',
height: 50,
});
useEffect(() => {
// Change the height of the child element after 2 seconds
setTimeout(() => setStyle(style => ({...style, height: 150})), 2e3);
}, []);
return (<div {...{style}}>Child</div>);
}
function Parent (): ReactElement {
const ref = useRef<HTMLDivElement>(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
if (!ref.current) return;
let lastBlockSize = 0;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!(entry.borderBoxSize && entry.borderBoxSize.length > 0)) continue;
// #ts-expect-error
const [{blockSize}] = entry.borderBoxSize;
if (blockSize === lastBlockSize) continue;
setMarginBottom(calculateHeight(entry.target));
lastBlockSize = blockSize;
}
});
observer.observe(ref.current, {box: 'border-box'});
return () => observer.disconnect();
}, []);
return (
<div {...{ref}}>
<div>height: {marginBottom}px</div>
<Child />
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
reactRoot.render(<Parent />);
</script>
You said,
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
However, parent useEffect does not do that, It fires only after all the children are mounted and their useEffects are fired.
The value of myref is stored in myref.current So your useEffect should be
useEffect(() => {
setMarginBottom(calculateHeight(myref.current));
});
Why don't you send a function to the child component that is called from the useEffect of the child component.
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
someFunction = () => {
setMarginBottom(calculateHeight(myref));
}
return (
<div ref={myref}>
<ResponsiveLine func={someFunction} marginBottom={marginBottom}/>
</div>)
}
// CHILD COMPONENT
const ChildComponent = ({func, marginBotton}) => {
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
func();
}, []);
return <div></div>
}

Why isn't my useState() hook setting a new state inside useEffect()?

Trying to detect if a <section> element is focused in viewport, I'm unable console.log a single true statement. I'm implementing a [isFocused, setIsFocused] hook for this.
This is my window:
I needed so when Section 2 is positioned at the top of the window, a single console.log(true) shows up. But this happens:
This is my implementation:
import React, { useEffect, useRef, useState } from "react";
const SectionII = (props) => {
const sectionRef = useRef();
const [isFocused, setIsFocused] = useState(false);
const handleScroll = () => {
const section = sectionRef.current;
const { y } = section.getBoundingClientRect();
if(!isFocused && y <= 0) {
setIsFocused(true);
console.log(isFocused, y);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section id="mentorship" ref={sectionRef} style={{borderTop: "1px solid"}}>
<h1>Section 2</h1>
<button>Set hash</button>
</section>
);
};
export default SectionII;
Why wouldn't my state by updated to true with setIsFocused(true) inside if(!isFocused && y <= 0)?
Thanks so much for the insight. I'm really stuck.
When you're using any state management in react, you need to ensure that the change is set before attempting to access the new state value. For your example, you immediately console.log(isFocused, y) following your setState function (changes will only appear on the next DOM render). Rather, you should use a callback with the set state function, setIsFocused(true, () => console.log(isFocused, y)).

What's wrong with my custom hook in React?

Hi Stack Overflow Community!
I am learning react and now I am practicing custom hooks. I get the example data from here:
https://jsonplaceholder.typicode.com/posts
I've got two components.
App.js
import React from "react";
import useComments from "./hooks/useComments"
const App = () => {
const Comments = useComments();
const renderedItems = Comments.map((comment) => {
return <li key={comment.id}>{comment.title}</li>;
});
return (
<div>
<h1>Comments</h1>
<ul>{renderedItems}</ul>
</div>
);
}
export default App;
useComments.js
import {useState, useEffect} from "react";
const useComments = () => {
const [Comments, setComments] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts", {
"mode": "cors",
"credentials": "omit"
}).then(res => res.json()).then(data => setComments(data))
}, []);
return [Comments];
};
export default useComments;
My output looks like this and i don't know why. There are no warnings or errors.
This line makes Comments be an array:
const [Comments, setComments] = useState([]);
...and then you're wrapping it in an additional array:
return [Comments];
But when you use it, you're treating it as a single dimensional array.
const Comments = useComments();
const renderedItems = Comments.map...
So you'll just need to line those two up. If you want two levels of array-ness (perhaps because you plan to add more to your hook, so that it's returning more things than just Comments), then the component will need to remove one of them. This can be done with destructuring, as in:
const [Comments] = useComments();
Alternatively, if you don't need that complexity, you can change your hook to not add the extra array, and return this:
return Comments;

How can I prevent the MaterialUI Switch from animating?

I'm new to React and making a Chrome extension with it.
Currently, I am using the Switch component from MaterialUI inside my popup page. How I am saving its state right now is by storing the state of each change in chrome.storage.local API. When I click back to the pop-up, I simply use the useEffect hook & fetch the state from chrome.storage.local & pass it as an argument to setState().
My issue with this is that it causes the toggle button to animate from off to on very briefly when you reopen the popup (as if you were manually toggling it). I'm aware it's because of the way I'm doing it (i.e, initializing the state of the toggle as false each time the pop-up is opened) but I'm currently stumped on doing this another way. Could anyone please help me? Thanks for reading!
MySwitchComponent.jsx
import React, { useState } from "react";
import { withStyles } from "#material-ui/core/styles";
import { Switch } from '#material-ui/core/';
const StyledSwitch = withStyles({
root: {
position: 'relative',
marginTop: '20px',
marginLeft: '90px'
},
})(Switch);
export default function NewSwitch() {
const [state, setState] = React.useState(false)
const handleChange = (event) => {
setState(event.target.checked);
chrome.storage.local.set({auto_delete_toggle: event.target.checked });
}
React.useEffect(() => {
chrome.storage.local.get(null, function(res){
setState(res.auto_delete_toggle);
})
});
return (
<StyledSwitch
checked={state}
onChange={handleChange}
>
</StyledSwitch>)
}
My popup.js just renders all the components encapsulated in a single in popup.html. Also, chrome.storage.local.get is asynchronous.
EDIT:
Here is a GIF to better illustrate my issue:
First, here's a sandbox that reproduces your issue. I've used a mockAsyncStorage (which is backed by window.localStorage) to mimic the async chrome.storage.local.get. You can see the undesired transition by clicking the switch so that it is checked, and then refreshing the page.
import React from "react";
import Switch from "#material-ui/core/Switch";
import mockAsyncStorage from "./mockAsyncStorage";
export default function App() {
const [checked, setChecked] = React.useState(false);
const handleChange = event => {
setChecked(event.target.checked);
mockAsyncStorage.set({ auto_delete_toggle: event.target.checked });
};
React.useEffect(() => {
mockAsyncStorage.get(null, function(res) {
setChecked(
res.auto_delete_toggle === undefined ? false : res.auto_delete_toggle
);
});
}, []);
return <Switch checked={checked} onChange={handleChange} />;
}
Below is one way to get rid of the transition. This initializes checked to undefined and while it is still undefined, it returns null instead of the switch so nothing is rendered until the appropriate initial state of the switch is known.
import React from "react";
import Switch from "#material-ui/core/Switch";
import mockAsyncStorage from "./mockAsyncStorage";
export default function App() {
const [checked, setChecked] = React.useState(undefined);
const handleChange = event => {
setChecked(event.target.checked);
mockAsyncStorage.set({ auto_delete_toggle: event.target.checked });
};
React.useEffect(() => {
mockAsyncStorage.get(null, function(res) {
setChecked(
res.auto_delete_toggle === undefined ? false : res.auto_delete_toggle
);
});
}, []);
if (checked === undefined) {
return null;
}
return <Switch checked={checked} onChange={handleChange} />;
}

How to avoid allocations in functional stateful components?

Consider the following component:
function SomeComponent(props){
const [isMouseOver, setIsMouseOver] = useState(false);
return (
<div onMouseOver={_ => setIsMouseOver(true)}
onMouseOut={_ => setIsMouseOver(false)}>
<img src={isMouseOver ? EditIconHover : EditIcon} alt="icon"/>
</div>
);
}
New instance of arrow function is created on every render. It creates a closure over setIsMouseOver function, though this function never changes.
Sure, it does not drastically affect performance in this case, but I'd like to know how to avoid these unnecessary memory allocations.
Do I have to attach all dependencies required for event handler to DOM element
<div data-deps={setIsMouseOver} onMouseOver={onMouseOverHandler} onMouseOut={onMouseOutHandler}></div>
and then access deps property inside onMouseOverHandler and onMouseOutHandler functions?
You might want to use memoization, although in this particular example its an overhead.
Refer to useCallback
import React, { useState, useCallback, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggle = useCallback(() => setIsEnabled(v => !v), []);
const toggleRef = useRef();
const setterRef = useRef();
useEffect(() => {
toggleRef.current = toggle;
setterRef.current = setIsEnabled;
}, [toggle]);
useEffect(() => {
console.log(toggle === toggleRef.current);
console.log(setIsEnabled === setterRef.current);
});
return <button onClick={toggle}>{isEnabled ? 'Enabled' : 'Disabled'}</button>;
};
ReactDOM.render(<App />, document.getElementById('root'));

Resources