I am currently writing a map component using Mapbox. But I encounter an error on React hooks during development.
In useEffect state variable that is declared prints two different values.
As i explain in the below code. startDrawing is console.logs both true and false after second click on <IconButton/> button.
import React from "react";
import mapboxgl from "mapbox-gl";
import { Add, Delete } from "#material-ui/icons";
import IconButton from "#material-ui/core/IconButton";
export default function MapComponent() {
const mapContainerRef = React.useRef(null);
const [startDrawing, setStartDrawing] = React.useState(false);
const [map, setMap] = React.useState(null);
const initMap = () => {
mapboxgl.accessToken = "mapbox-token";
const mapbox = new mapboxgl.Map({
container: mapContainerRef.current,
style: "mapbox://styles/mapbox/streets-v11",
center: [0, 0],
zoom: 12,
});
setMap(mapbox);
};
React.useEffect(() => {
if (!map) {
initMap();
} else {
map.on("click", function (e) {
// After second click on set drawing mode buttons
// startDrawing value writes two values for each map click
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
console.log(startDrawing);
if (startDrawing) {
// do stuff
} else {
// do stuff
}
});
}
}, [map, startDrawing]);
return (
<>
<div>
{/* set drawing mode */}
<IconButton onClick={() => setStartDrawing(!startDrawing)}>
{startDrawing ? <Delete /> : <Add />}
</IconButton>
</div>
<div ref={mapContainerRef} />
</>
);
}
So my question is how can i solve this problem?
Thank you for your answers.
The issue is that you are adding a new event listener to map every time startDrawing changes. When you click on the rendered element all of those listeners are going to be fired, meaning you get every state of startDrawing the component has seen.
See this slightly more generic example of your code, and note that every time you click Add or Delete a new event listener gets added to the target element:
const { useState, useRef, useEffect } = React;
function App() {
const targetEl = useRef(null);
const [startDrawing, setStartDrawing] = useState(false);
const [map, setMap] = useState(null);
const initMap = () => {
setMap(targetEl.current);
};
useEffect(() => {
if (!map) {
initMap();
} else {
const log = () => console.log(startDrawing);
map.addEventListener('click', log);
}
}, [map, startDrawing]);
return (
<div>
<div>
<button onClick={() => setStartDrawing(!startDrawing)}>
{startDrawing ? <span>Delete</span> : <span>Add</span>}
</button>
</div>
<div ref={targetEl}>target element</div>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can fix this by adding a return statement to your useEffect. This is triggered immediately before the effect updates with new values from the dependency array, and also when the component unmounts. Inside the return statement you should remove the previous event listener so that only one is attached to the element at any given point. For the above example it would look like this:
useEffect(() => {
if (!map) {
initMap();
} else {
const log = () => console.log(startDrawing);
map.addEventListener('click', log);
return () => {
map.removeEventListener('click', log);
};
};
}, [map, startDrawing]);
Ideally you would not use the standard JS event syntax at all, as the convention in React is to attach events declaratively in the return/render function so that they can always reference the current state. However, you are using an external library, and I don't know whether it has any explicit support for React - you should probably check that out.
The issue is that useEffect will trigger both on mount and unmount (render and destroy). Refer to this documentation for a detailed explanation.
To run the function only on the first render, you can pass an empty array as the second parameter of useEffect, just like this:
useEffect(()=>{
//do stuff
},[]); // <--- Look at this parameter
The last parameter serves as a flag and usually a state should be passed, which will make useEffect's function trigger only if the parameter's value is different from the previous.
Let's assume you want to trigger useEffect each and every time your state.map changes - you cold do the following:
const [map, setMap] = React.useState(null);
useEffect(()=>{
//do stuff
},map); // if map is not different from previous value, function won't trigger
Related
When I click the MultipleComponent button, all logs in the function return null.
The second time I click it, it returns the previous values.
How can I get the current status in each log within the map function?
When I call the function in the useEffect hook, useEffect runs after the first render. I don't want it to run after the first render.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react-lite";
function App() {
const [component, setComponent] = useState([]);
useEffect(() => {});
const newArray = [1, 2, 3];
const Test = observer(() => {
return (
<div>
<p>Test</p>
</div>
);
});
const Test2 = observer(() => {
return (
<div>
<p>Test2</p>
</div>
);
});
const Test3 = observer(() => {
return (
<div>
<p>Test3</p>
</div>
);
});
function MultipleComponent() {
newArray.map(async (x) => {
if (x === 1) {
setComponent((ps) => [...ps, Test]);
console.log(component);
} else if (x === 2) {
setComponent((ps) => [...ps, Test2]);
console.log(component);
} else {
setComponent((ps) => [...ps, Test3]);
console.log(component);
}
});
}
return (
<div>
{component.map((Input, index) => (
<Input components={component} key={index} />
))}
<button onClick={() => setComponent([...component, Test])}>
Single Component
</button>
<button onClick={() => MultipleComponent()}>Multiple Component</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
codensadbox: https://codesandbox.io/s/react-hooks-useeffect-forked-shxvl6
When I click the MultipleComponent button, all logs in the function return null.
The second time I click it, it returns the previous values.
React state updates are asynchronous and do not update any values in the current scope. They trigger a render, and then your component executes again with that new state.
See this for more info: The useState set method is not reflecting a change immediately
How can I get the current status in each log within the map function?
You can't get the state that has been changed until the next render, but you don't need to because you have the value that you set. Just use that if you need to. For example:
setComponent((ps) => {
const newState = [...ps, Test];
console.log(newState);
return newState;
});
When I call the function in the useEffect hook, useEffect runs after the first render. I don't want it to run after the first render.
Your effect has no dependencies, which means that it will run after every render. Just pass an array of dependencies to the effect in order to only execute it when those change. If you pass an empty array, then it will only every execute once.
useEffect(() => console.log('I run only once'), []);
See the docs on useEffect for more: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Hi as per the documentation "calling the set function does not change state in the running code". If you need to use the next state, you can save it in a variable before passing it to the set function.
Here is a link of your updated code.
https://codesandbox.io/s/react-hooks-useeffect-forked-m7ipwb?file=/src/index.js
When I call the function in the useEffect hook, useEffect runs after the first render. I don't want it to run after the first render.
By default, Effects run after every render. You can tell React to skip unnecessarily re-running the Effect by specifying an array of dependencies as the second argument to the useEffect call. Start by adding an empty [] array
In React, I have a number of buttons (imagine a PIN layout with numbers) that update the state on click. I also added an event listener to the document so pressing keys on the keyboard updates the pin too. However, there's a strange problem. When I add a number by clicking a button, the state is working correctly and everything is fine, but when I press a key on a physical keyboard, the state updates, but logs as <empty string>!
Here is the code:
export default function Keypad() {
const [pin, setPin] = useState("");
function addNumber(num) {
console.log(pin); // returns the correct pin with handleKeyClick, returns <empty string> with handleKeyDown
if (pin.length < 6) { // only works if the pin is not <empty string>
setPin((pin) => [...pin, num.toString()]); // works correctly with both handleKeyClick and handleKeyDown even if pin logged <empty string>!
}
}
function handleKeyClick(num) {
addNumber(num);
}
function handleKeyDown(e) {
if (!isNaN(e.key)) {
addNumber(e.key);
}
}
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div>
{/* just one button for example */}
<button onClick={() => handleKeyClick(9)}>9</button>
</div>
)
}
I guess this is because document can't access the pin state, but if it was the case, the setPin shouldn't work either. Am I right?
Your component does not keep a reference when listening to DOM events, this answer has some neat code for listening to window events using a fairly simple hook. When applied to your code, it works as expected:
const {useState, useEffect, useRef} = React;
// Hook
function useEventListener(eventName, handler, element = window){
// Create a ref that stores handler
const savedHandler = useRef();
// Update ref.current value if handler changes.
// This allows our effect below to always get latest handler ...
// ... without us needing to pass it in effect deps array ...
// ... and potentially cause effect to re-run every render.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(
() => {
// Make sure element supports addEventListener
// On
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// Create event listener that calls handler function stored in ref
const eventListener = event => savedHandler.current(event);
// Add event listener
element.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // Re-run if eventName or element changes
);
};
const Keypad = (props) => {
const [pin, setPin] = useState([]);
function addNumber(num) {
console.log(pin); // returns the correct pin with handleKeyClick, returns <empty string> with handleKeyDown
if (pin.length < 6) { // only works if the pin is not <empty string>
setPin((pin) => [...pin, num.toString()]); // works correctly with both handleKeyClick and handleKeyDown even if pin logged <empty string>!
}
}
function handleKeyClick(num) {
addNumber(num);
}
function handleKeyDown(e) {
if (!isNaN(e.key)) {
addNumber(e.key);
}
}
useEventListener("keydown", handleKeyDown)
return (
<div>
{/* just one button for example */}
<button onClick={() => handleKeyClick(9)}>9</button>
</div>
)
return "Hello World"
}
ReactDOM.render(<Keypad />, document.getElementById("root"))
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
For context, I have a web app that displays an image in a React-Bootstrap Container component (Arena) that holds an image where users are to look and find specific characters.
Separately, I created a div component (CustomCursor) where the background is set to a magnifying glass SVG image.
The Arena component tracks mouse position through an OnMouseMove handler function (handleMouseMove) and passes those coordinates as props to the CustomCursor component.
Here is my Arena component code:
import { useState, useEffect } from 'react';
import { Container, Spinner } from 'react-bootstrap';
import CustomCursor from '../CustomCursor/CustomCursor';
import Choices from '../Choices/Choices';
import { getImageURL } from '../../helpers/storHelpers';
import './Arena.scss';
export default function Arena(props) {
const [arenaURL, setArenaURL] = useState('');
const [loaded, setLoaded] = useState(false);
const [clicked, setClicked] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function handleClick(e) {
setClicked(true);
}
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
useEffect(() => {
retreiveArena();
// FUNCTION DEFINITIONS
async function retreiveArena() {
const url = await getImageURL('maps', 'the-garden-of-earthly-delights.jpg');
setArenaURL(url);
setLoaded(true);
}
}, [])
return (
<Container as='main' fluid id='arena' className='d-flex flex-grow-1 justify-content-center align-items-center' onClick={handleClick}>
{!loaded &&
<Spinner animation="border" variant="danger" />
}
{loaded &&
<img src={arenaURL} alt='The Garden of Earthly Delights triptych' className='arena-image' onMouseMove={handleMouseMove} />
}
{clicked &&
<Choices x={x} y={y} />
}
<CustomCursor x={x} y={y} />
</Container>
)
}
Here is my CustomCursor code:
import './CustomCursor.scss';
export default function CustomCursor(props) {
const { x, y } = props;
return (
<div className='custom-cursor' style={{ left: `${x - 64}px`, top: `${y + 50}px` }} />
)
}
When I first created the OnMouseMove handler function I simply set the x and y state values by passing them into their respective state setter functions directly:
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
However, I noticed this was slow and laggy and when I refactored this function to use setter functions instead it was much faster (what I wanted):
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
Before:
After:
Why are using setter functions faster than passing in values directly?
This is interesting. First of all, we need to focus on reacts way of updating state. In the documentation of react https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous There you can see:
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
For example, this code may fail to update the counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
A pretty good article on this is written by Jan Hesters here:
https://medium.com/#jan.hesters/updater-functions-in-reacts-setstate-63c7c162b16a
And more details here:
https://learn.co/lessons/react-updating-state
From the React Docs, what I have learnt is that the component will re-render only if there is a change in the value of a state.
For instance
import React, { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
console.log("I am rendering");
const handleButtonClick = () => {
setCount(0);
};
return (
<>
<button onClick={handleButtonClick}>Increment</button>
Count value is: {count}
</>
);
}
The message I am rendering is printed only once even if we click the button because the setCount function is setting the value to 0 which is the present value of count
Since there is no change in the present and future value therefore, the Component does not re-render.
Unexpected Behaviour
However, the similar behaviour is not observed when we add an extra line setCount(1) before setCount(0)
import React, { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
console.log("I am rendering");
const handleButtonClick = () => {
setCount(1); //this line has been added extra
setCount(0);
};
return (
<>
<button onClick={handleButtonClick}>Increment</button>
Count value is: {count}
</>
);
}
In principle, there is no change in the output of the final count value. However, if we click the button, the component re-renders and prints the message I am rendering
I could not find an explanation for this behaviour. Is this behaviour on expected lines?.
Shouldn't the component re-render only when the final value of the state is different from the current value ?
Sometimes, Reacts needs another render phase to decide if it needs a bailout. By the way, when we saying "bailout" meaning bailing out the Reconciliation process.
Notice the documentation on Bailing out a state update:
Note that React may still need to render that specific component again before bailing out.
Here is another example of such case demonstrating the idea:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
}, [state]);
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
You notice the next logs and their explanations:
A // First render
B // Mount
A // State change from 0 -> 42, triggers render
B // useEffect dep array change, triggers callback
A // **Our issue**, React needs another render
The value does change when you press the button. First, it changes to 1 then to 0 but this runs very fast that you don't get to see it.
to see this, you could add a setTimeout
const handleButtonClick = () => {
setCount(1); //this line has been added extra
setTimeout(() => {
setCount(0);
}, 500);
};
Everytime I work with addEventListener(), and also want to access some state inside useEffect, I get the same issue. I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.
I almost everytime find myself stuck with the "React Hook useEffect has a missing dependency" warning.
Let's say I have a component that needs to change it state on window.onClick() and on window.onDoubleClick(). If the state is true, click should change it to false, and if the state is false, double click should change it to true.
So here's what I whould write:
import React, { useState, useEffect } from 'react';
export default function someComponent() {
const [toggle, setToggle] = useState(false);
useEffect(() => {
window.addEventListener('click', (event) => {
if (toggle) setToggle(false)
})
window.addEventListener('dblclick', (event) => {
if (!toggle) setToggle(true)
})
}, [])
return (
<p>The toggle state is {toggle.toString()}</p>
);
}
This code works, but I get the missing dependency warning. I can't add toggle to the dependency array, because then it will add another event listener each time the toggle state changes.
What am I doing wrong here? how should I fix this?
Edit: Maybe this example wasn't too good, but it's the simplest I could think of. But, this issue is also for when I create other event listeners, that have to be on the windows object, like scroll. I know I can use return to remove the event listener everytime, but for events like scroll it makes it much slower. It doesn't make sense to me that I have to remove and add it everytime, when I just don't need it to fire again.
With react you don't have to use the window element in this case. Not even a useEffect.
By using the useEffect hook you are telling react to do something after render (depending on the dependency array). In this case changing state is not necessary immediately after rendering the page, only when the user interacts with the element.
Adding click events through the useEffect is probably not needed most of the time and and doing it like the example below will probably save you time and a headache and maybe even performance (correct me if i'm wrong).
I would personally do it like this.
import React, { useState } from 'react';
export default function someComponent() {
const [toggle, setToggle] = useState(false);
return (
<p
onClick={() => setToggle(false)}
onDoubleClick={() => setToggle(true)}
>
The toggle state is {toggle.toString()}
</p>
);
}
You could also call functions from the element like so
const [toggle, setToggle] = useState(false);
const handleClick = () => {
if (toggle) {
setToggle(false);
}
};
const handleDoubleClick = () => {
if (!toggle) {
setToggle(true);
}
};
return (
<p
onClick={() => handleClick()}
onDoubleClick={() => handleDoubleClick()}
>
The toggle state is {toggle.toString()}
</p>
);
CodeSandbox example
You can add a clean-up function to the useEffect hook to remove old listeners. This way you can pass toggle into the dependency array and you won't have stacking event listeners.
https://reactjs.org/docs/hooks-effect.html
useEffect(() => {
const handleClick = () => toggle ? setToggle(false) : setToggle(true);
window.addEventListener('click', handleClick);
window.addEventListener('dblclick', handleClick);
return () => {
window.removeEventListener('click', handleClick);
window.removeEventListener('dblclick', handleClick);
}
}, [toggle]);
I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.
There is a way around this, and that is to return a cleanup function from the useEffect callback. I would encourage you to read the linked section of the docs, then the below solution would become much clearer:
useEffect(() => {
const handleClick = () => {
setToggle(!toggle)
}
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [toggle])
with the above solution, each time toggle is updated, the cleanup function is called, which removes the current event listener before running the effect again.
Also note that you can provide a callback function to setToggle, which receives the current value of toggle and returns the new value. With this approach you wouldn't need to pass toggle as a dependency to useEffect:
useEffect(() => {
const handleClick = () => {
setToggle(currentValue => !currentValue)
}
window.addEventListener("click", handleClick)
return () => {
window.removeEventListener("click", handleClick)
}
}, [])