React Hooks: How to get updated value of a state in useEffect - reactjs

Need to update the boolean state value on map click. And based on that updated value, new component rendered.
const [layerClick, setLayerClick] = useState(false);
useEffect(() => {
if (streetsCurrentHeatStateMap !== null) {
streetsCurrentHeatStateMap.on('click', (e) => {
if (layerClick === false) {
popupToggler();
}
})
}
}, [
layerClick,
]);
And here is the popupToggler function
const popupToggler = async () => {
setLayerClick((layerClick) => !layerClick);
}
And it is used for the conditional rendering of the Popup Component.
{layerClick && (
<PopupContent />
)}
So problem is: It always call the popuptoggler function, mean the value of state not updated. Any help?

Related

State in useEffect is not updating

I have this component that when I click on the button it toggles the state of isMenuActive, the problem comes when implementing the function inside the useEffect, for a strange reason when I try to access to the value of isMenuActive inside the closeTooltip function it always reflects false, even though the value of isMenuActive has changed to true.
import { useState, useEffect, useRef } from "react";
import Tooltip from "./Tooltip";
export default function Navbar() {
const [isMenuActive, setIsMenuActive] = useState(false);
const menu = useRef(null);
const handleOnClick = (e) => {
setIsMenuActive((prev) => !prev);
};
useEffect(() => {
const closeTooltip = (e) => {
if (menu.current && !menu.current.contains(e.target)) {
//Here isMenuActive is always false, even after using handleOnClick func
console.log(isMenuActive);
}
};
document.addEventListener("click", closeTooltip, true);
return () => document.removeEventListener("click", closeTooltip, true);
}, [menu]);
return (
<button
className="navbar__notifications"
aria-label="Notificaciones"
name="notification"
onClick={handleOnClick}
ref={menu}
>
{isMenuActive && <Tooltip title="Notificaciones" />}
</button>
);
}
Any idea of why is this happening?
menu (with useRef) always refers to the same object, so useEffect won't be triggered for every change of menu (even though menu.current).
useEffect is only triggered in 3 cases
Props change with dependencies
States change with dependencies
Initial mounted component
The log you get from closeTooltip is from the initial useEffect call (initial mounted component)
For the fix, you should add isMenuActive (state changes) in the dependency list of useEffect instead. That will keep listening to isMenuActive state changes and update your closeTooltip event accordingly.
useEffect(() => {
const closeTooltip = (e) => {
if (menu.current && !menu.current.contains(e.target)) {
//Here isMenuActive is always false, even after using handleOnClick func
console.log(isMenuActive);
}
};
document.addEventListener("click", closeTooltip, true);
return () => document.removeEventListener("click", closeTooltip, true);
}, [isMenuActive]);
You can check this sandbox for the test

Scroll to element on page load with React Hooks

I'm trying to create a functional component that fetches data from an API and renders it to a list. After the data is fetched and rendered I want to check if the URL id and list item is equal, if they are then the list item should be scrolled into view.
Below is my code:
import React, { Fragment, useState, useEffect, useRef } from "react";
export default function ListComponent(props) {
const scrollTarget = useRef();
const [items, setItems] = useState([]);
const [scrollTargetItemId, setScrollTargetItemId] = useState("");
useEffect(() => {
const fetchData = async () => {
let response = await fetch("someurl").then((res) => res.json());
setItems(response);
};
fetchData();
if (props.targetId) {
setScrollTargetItemId(props.targetId)
}
if (scrollTarget.current) {
window.scrollTo(0, scrollTarget.current.offsetTop)
}
}, [props]);
let itemsToRender = [];
itemsToRender = reports.map((report) => {
return (
<li
key={report._id}
ref={item._id === scrollTargetItemId ? scrollTarget : null}
>
{item.payload}
</li>
);
});
return (
<Fragment>
<ul>{itemsToRender}</ul>
</Fragment>
);
}
My problem here is that scrollTarget.current is always undefined. Please advice what I'm doing wrong. Thanks in advance.
Using useCallback, as #yagiro suggested, did the trick!
My code ended up like this:
const scroll = useCallback(node => {
if (node !== null) {
window.scrollTo({
top: node.getBoundingClientRect().top,
behavior: "smooth"
})
}
}, []);
And then I just conditionally set the ref={scroll} on the node that you want to scroll to.
That is because when a reference is changed, it does not cause a re-render.
From React's docs: https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
constructor(props) {
thi.modal = React.createRef();
}
handleSwitch() {
// debugger
this.setState({ errors: [] }, function () {
this.modal.current.openModal('signup') // it will call function of child component of Modal
});
// debugger
}
return(
<>
<button className="login-button" onClick={this.handleSwitch}>Log in with email</button>
<Modal ref={this.modal} />
</>
)
React Hooks will delay the scrolling until the page is ready:
useEffect(() => {
const element = document.getElementById('id')
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [])
If the element is dynamic and based on a variable, add them to the Effect hook:
const [variable, setVariable] = useState()
const id = 'id'
useEffect(() => {
const element = document.getElementById(id)
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [variable])

State property updated value cannot be accessed in onClick function

I'm using React Hooks. I set the state property questions after an axios fetch call. Now when I click a button, in its function questions state is still empty
const [questions, setQuestions] = useState([]);
const [customComponent, setCustomComponent] = useState(<div />);
useEffect(() => {
axios.get("urlhere").then(res => {
console.log(12, res.data);
setQuestions(res.data);
res.data.map(q => {
if (q.qualifyingQuestionId == 1) {
setCustomComponent(renderSteps(q, q.qualifyingQuestionId));
}
});
});
}, []);
const handleNext = i => {
console.log(32, questions); //questions is still an empty array here
};
const renderSteps = (step, i) => {
switch (step.controlTypeName) {
case "textbox":
return (
<div key={i}>
<input type="text" placeholder={step.content} />
<button onClick={() => handleNext(i)}>Next</button>
</div>
);
}
};
return <>{customComponent}</>;
Do I need to use reducers here and put the custom component in another "file"?
setQuestions does not update state immediately, you should use the prevState instead to access the new value.
Here's a sandbox to match your codes with some explanation on why it was empty > https://codesandbox.io/s/axios-useeffect-kdgnw
You can also read about it here: Why calling react setState method doesn't mutate the state immediately?
Finally I have my own solution
I passed down the data from the fetch function to another component as props
useEffect(() => {
axios.get('url')
.then((data) => {
setCustomComponent(<Questions questions={data} />)
})
}, [])

changes to state issued from custom hook not causing re-render even though added to useEffect

I have a custom hook that keeps a list of toggle states and while I'm seeing the internal state aligning with my expectations, I'm wondering why a component that listens to changes on the state kept by this hook isn't re-rendering on change. The code is as follows
const useToggle = () => {
const reducer = (state, action) => ({...state, ...action});
const [toggled, dispatch] = useReducer(reducer, {});
const setToggle = i => {
let newVal;
if (toggled[i] == null) {
newVal = true;
} else {
newVal = !toggled[i];
}
dispatch({...toggled, [i]: newVal});
console.log('updated toggled state ...', toggled);
};
return {toggled, setToggle};
};
const Boxes = () => {
const {setToggle} = useToggle()
return Array.from({length: 8}, el => null).map((el,i) =>
<input type="checkbox" onClick={() => setToggle(i)}/>)
}
function App() {
const {toggled} = useToggle()
const memoized = useMemo(() => toggled, [toggled])
useEffect(() => {
console.log('toggled state is >>>', toggled) // am not seeing this on console after changes to toggled
}, [toggled])
return (
<div className="App">
<Boxes />
</div>
);
}
It's because you are using useToggle twice.
once in the App
another one in the Boxes.
When you dispatch the action in Boxes, it's updating the toggled instance for Boxes (which is not retrieved in it).
Think of your custom hook like how you use useState. When you use useState, each component gets its own state. Same goes for the custom hook.
So there are a few ways you can address the issue.
Pass the setToggle from App to Boxes via prop-drilling
Use Context API (or Redux or other statement management library to pass
setToggle instance in the App component down)
Here is an example of prop-drilling.
You can follow along
const Boxes = ({ setToggle }) => {
// const { setToggle } = useToggle();
return Array.from({ length: 8 }, el => null).map((el, i) => (
<input key={i} type="checkbox" onClick={() => setToggle(i)} />
));
};
function App() {
const { toggled, setToggle } = useToggle();
useEffect(() => {
console.log("toggled state is >>>", toggled); // am not seeing this on console after changes to toggled
}, [toggled]);
return (
<div className="App">
<Boxes setToggle={setToggle} />
</div>
);
}
Note: that I added key props in Boxes using the index i(and it is a bad practice by the way)
You can see that it's now working as you'd expect.

React Hooks - Ref is not available inside useEffect

I am using ReactHooks. I am trying to access ref of User component in useEffect function, but I am getting elRef.current value as null, though I passed elRef.current as second argument to useEffect. I am supposed to get reference to an element, but outside (function body) of useEffect, ref value is available. Why is that ? How can I get elRef.current value inside useEffect?
code
import React, { Component, useState, useRef, useEffect } from "react";
const useFetch = url => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(
() => {
setIsLoading(true);
fetch(url)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => {
setIsLoading(false);
setData(json.data);
})
.catch(error => {
setIsLoading(false);
setError(error);
});
},
[url]
);
return { data, isLoading, error };
};
const User = ({ id }) => {
const elRef = useRef(null);
const { data: user } = useFetch(`https://reqres.in/api/users/${id}`);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
};
class App extends Component {
state = {
userId: 1
};
handleNextClick = () => {
this.setState(prevState => ({
userId: prevState.userId + 1
}));
};
handlePrevNext = () => {
this.setState(prevState => ({
userId: prevState.userId - 1
}));
};
render() {
return (
<div>
<button
onClick={() => this.handlePrevClick()}
disabled={this.state.userId === 1}
>
Prevoius
</button>
<button onClick={() => this.handleNextClick()}>Next</button>
<User id={this.state.userId} />
</div>
);
}
}
export default App;
Codesandbox link
Thanks !
You should use useCallback instead of useRef as suggested in the reactjs docs.
React will call that callback whenever the ref gets attached to a different node.
Replace this:
const elRef = useRef(null);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
with this:
const elRef = useCallback(node => {
if (node !== null) {
console.log("ref", node); // node = elRef.current
}
}, []);
It's a predictable behaviour.
As mentioned #estus you faced with this because first time when it's called on componentDidMount you're getting null (initial value) and get's updated only once on next elRef changing because, actually, reference still being the same.
If you need to reflect on every user change, you should pass [user] as second argument to function to make sure useEffect fired when user is changed.
Here is updated sandbox.
Hope it helped.
When you use a function as a ref, it is called with the instance when it is ready. So the easiest way to make the ref observable is to use useState instead of useRef:
const [element, setElement] = useState<Element | null>(null);
return <div ref={setElement}></div>;
Then you can use it in dependency arrays for other hooks, just like any other const value:
useEffect(() => {
if (element) console.log(element);
}, [element]);
See also How to rerender when refs change.
useEffect is used as both componentDidMount and componentDidUpdate,
at the time of component mount you added a condition:
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
because of the above condition at the time of mount, you don't have the user, so it returns null and div is not mounted in the DOM in which you are adding ref, so inside useEffect you are not getting elRef's current value as it is not rendered.
And on the click of next as the div is mounted in the dom you got the value of elRef.current.
The assumption here is that useEffect needs to detect changes to ref.current, so needs to have the ref or ref.currentin the dependencies list. I think this is due to es-lint being a bit over-pedantic.
Actually, the whole point of useEffect is that it guarantees not to run until the rendering is complete and the DOM is ready to go. That is how it handles side-effects.
So by the time useEffect is executed, we can be sure that elRef.current is set.
The problem with your code is that you don't run the renderer with <div ref={elRef}...> until after user is populated. So the DOM node you want elRef to reference doesn't yet exist. That is why you get the null logging - nothing to do with dependencies.
BTW: one possible alternative is to populate the div inside the effect hook:
useEffect(
() => {
if(!user) return;
elRef.current.innerHTML = `${user.first_name} ${user.last_name}`;
}, [user]
);
...
//if (!user) return null;// Remove this line
return <div ref={elRef}></div>; //return div every time.
That way the if (!user) return null; line in the User component is unnecessary. Remove it, and elRef.current is guaranteed to be populated with the div node from the very beginning.
set a useEffect on the elem's.current:
let elem = useRef();
useEffect(() => {
// ...
}, [elem.current]);

Resources