I have a event binding from the window object on scroll. It gets properly fired everytime I scroll. Until now everything works fine. But the setNavState function (this is my setState-function) does not update my state properties.
export default function TabBar() {
const [navState, setNavState] = React.useState(
{
showNavLogo: true,
lastScrollPos: 0
});
function handleScroll(e: any) {
const currScrollPos = e.path[1].scrollY;
const { lastScrollPos, showNavLogo } = navState;
console.log('currScrollPos: ', currScrollPos); // updates accordingly to the scroll pos
console.log('lastScrollPos: ', lastScrollPos); // last scroll keeps beeing 0
if (currScrollPos > lastScrollPos) {
setNavState({showNavLogo: false, lastScrollPos: currScrollPos});
} else {
setNavState({showNavLogo: true, lastScrollPos: currScrollPos});
}
}
useEffect(() => {
window.addEventListener('scroll', handleScroll.bind(this));
}, []);
...
}
So my question is how do I update my state properties with react hooks in this example accordingly?
it's because how closure works. See, on initial render you're declaring handleScroll that has access to initial navState and setNavState through closure. Then you're subscribing for scroll with this #1 version of handleScroll.
Next render your code creates version #2 of handleScroll that points onto up to date navState through closure. But you never use that version for handling scroll.
See, actually it's not your handler "did not update state" but rather it updated it with outdated value.
Option 1
Re-subscribing on each render
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
});
Option 2
Utilizing useCallback to re-create handler only when data is changed and re-subscribe only if callback has been recreated
const handleScroll = useCallback(() => { ... }, [navState]);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
Looks slightly more efficient but more messy/less readable. So I'd prefer first option.
You may wonder why I include navState into dependencies but not setNavState. The reason is - setter(callback returned from useState) is guaranteed to be referentially same on each render.
[UPD] forgot about functional version of setter. It will definitely work fine while we don't want to refer data from another useState. So don't miss up-voting answer by giorgim
Just add dependency and cleanup for useEffect
function TabBar() {
const [navState, setNavState] = React.useState(
{
showNavLogo: true,
lastScrollPos: 0
});
function handleScroll(e) {
const currScrollPos = e.path[1].scrollY;
const { lastScrollPos, showNavLogo } = navState;
console.log('showNavLogo: ', showNavLogo);
console.log('lastScrollPos: ', lastScrollPos);
if (currScrollPos > lastScrollPos) {
setNavState({showNavLogo: false, lastScrollPos: currScrollPos});
} else {
setNavState({showNavLogo: true, lastScrollPos: currScrollPos});
}
}
React.useEffect(() => {
window.addEventListener('scroll', handleScroll.bind(this));
return () => {
window.removeEventListener('scroll', handleScroll.bind(this));
}
}, [navState]);
return (<h1>scroll example</h1>)
}
ReactDOM.render(<TabBar />, document.body)
h1 {
height: 1000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
I think your problem could be that you registered that listener once (e.g. like componendDidMount), now every time that listener function gets called due to scroll, you are referring to the same value of navState because of the closure.
Putting this in your listener function instead, should give you access to current state:
setNavState(ps => {
if (currScrollPos > ps.lastScrollPos) {
return { showNavLogo: false, lastScrollPos: currScrollPos };
} else {
return { showNavLogo: true, lastScrollPos: currScrollPos };
}
});
Related
So I am using SwipeableViews no swipe between pages. My problem is each page have a different dynamic length. I have animateHeight as true but it only animate the height when the tab changes. In the documentation it says that the function updateHeight() can solve this.
https://react-swipeable-views.com/api/api/
Due to my lack of knowledge I just could not get the updateHeight since all the exampels I saw on class based app. I built my app in function based app. I just could not figure it out to pass it as props to call it later.
I've found a way to use the updateHeight function. It's a bit hacky, but it works π
. I've added an interval and timeout to compensate for data loading in/slow machines. The useEffect cleans the interval when the page is unmounted.
There's probably a better solution, but this is what I've found so far!
export function SwipeView() {
const [ref, setRef] = useState(null);
const onRefChange = useCallback((node: SwipeableViews & HTMLDivElement) => {
// hacky solution to update the height after the first render. Height is not set correctly on initial render
setRef(node); // e.g. change ref state to trigger re-render
if (node === null) {
return;
} else {
interval = setInterval(() => {
// #ts-ignore typings are not correct in this package
node.updateHeight();
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 10000);
}
}, []);
useEffect(() => {
return () => clearInterval(interval);
}, [interval]);
return (
<SwipeableViews
animateHeight
ref={onRefChange}
>
{children}
</SwipeableViews>
);
}
I have this callback function in which I set some state. The state seems to mutate just fine. However, I cannot refer to the updated state inside the callback function - it always prints out the initial state.
So my question is why this happens and how should I proceed if i want to check on the updated state inside the callback?
import React, { useState, useEffect } from "react";
import Context from "./ctx";
export default props => {
const [state, setState] = useState({
x: 0,
y: 0
});
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
});
return () => {
window.removeEventListener("resize");
};
}, []);
console.log(`${JSON.stringify(state)}`); <<---logs updated versions of the state.
return (
<Context.Provider
value={{
...state
}}
>
{props.children}
</Context.Provider>
);
};
The problem you're facing is related to two things:
How React works
Closures (function + environment in which it was created).
1. How React works
You've created and exported functional React component, which accepts some props, uses hooks for state management, and renders some content.
Whenever some props or state change in your component (or in it's parent component) react will re-render your component, meaning: it will literally call your function, like yourComponent(props).
The point is: the body of your function will be executed every time re-render happens, alongside with calls to useState and useEffect.
2. Closures (function + environment in which it was created)
Whenever we create/define some function in JavaScript it is stored in the runtime memory alongside with the environment in which it was created.
In your case, you're defining the function in question (and providing it as a callback to useEffect at the same time) here:
() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
});
return () => {
window.removeEventListener("resize");
};
}
and the environment in which it's created is the body of your functional component.
So, whenever React calls your component the new environment is created, and new callback function is defined. But, because you provided empty array as second argument (which represents dependencies array docs) here:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}, []); // <-- HERE
provided function will be executed only once - on component mount/first render, because you said it doesn't depend on any other value, so there's no need to fire that effect again.
Bottom line: You're defining new callback function on every render, but only first one was actually called. When it was defined, value of state was { x: 0, y: 0 }. That's why you're always getting that value.
Solution
Depending on your needs, you can set dependencies in the second argument, like:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}, [state]); // <-- NOW THE CALLBACK WILL BE EXECUTED EVERY TIME STATE CHANGES
or, you can omit second argument altogether, that way it will be fired on every render:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}); // <-- WITHOUT SECOND ARGUMENT
It may seem expensive, but the official React documentation says it shouldn't have any performance hits with event listeners usage, because addEventListener DOM api is very efficient. But if you notice some performance issues, you can always use the first solution :).
Hope it helps!
The reason in javascript closures. useEffect callback has an old version of data because of empty deps array, outside data don't update. So your old version of state object is used. That is why you get the same values
SetState is asynchronous, right after you execute that line of code you will get old values, one thing that you can do is listen for the state to be updated then output the console.log inside that effect, something like:
useEffect(() => {
console.log(`${JSON.stringify(state)}`); //<<---Now it logs the actual state
}, [state.x, state.y]);
Full Example:
import React, { useState, useEffect } from "react";
export const App = props => {
const [state, setState] = useState({
x: 0,
y: 0
});
useEffect(() => {
console.log(`${JSON.stringify(state)}`); //<<--- Now it logs the actual state
}, [state]);
useEffect(() => {
const handler = window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log("updated");
});
return () => {
window.removeEventListener("resize", handler);
};
}, []);
console.log(`${JSON.stringify(state)}`); //<<---logs updated versions of the state.
return (
<div style={{ width: "500px", height: "500px", backgroundColor: "red" }}>
test
</div>
);
};
Codesandbox: https://codesandbox.io/s/unruffled-kowalevski-n87km
State value showMenu is not updating within useEffect hook.
When testing, when the button is first clicked and the screen is touched to move, showMenu properly consoles to true. When the button is clicked a second time (and third, forth, etc) and the screen is touched to move, showMenu continues to console as true when it should alternate to false.
const [showMenu, setShowMenu] = useState(false)
useEffect(_ => {
const listener = e => {
e.preventDefault()
console.log(showMenu, ' useEffect - touchmove')
}
showMenu
? document.body.addEventListener('touchmove', listener, {passive: false})
: document.body.removeEventListener('touchmove', listener)
}, [showMenu])
return (
<button onclick={_ => {
console.log(!showMenu, ' button click')
setShowMenu(!showMenu)
}} />
)
Console Result
I think the event of body is not properly removed, because listener is changed every time useEffect.
So you can return a function in useEffect to clear the previous useEffect.
useEffect(() => {
if (showMenu) {
const listener = e => {
e.preventDefault();
console.log(showMenu, ' useEffect - touchmove');
};
document.body.addEventListener('touchmove', listener, { passive: false });
return () => {
document.body.removeEventListener('touchmove', listener);
}
}
}, [showMenu]);
You can also read cleaning-up-an-effect to learn more
I don't know what your intent is, but what you are doing with useEffect is probably not what you're expecting. When showMenu is false, you're removing a listener function that has not been bound because objects are compared by reference in JS and listener is being redefined each time showMenu changes.
The typical way to unbind a listener when useEffect changes is to return a function that handles the cleanup from your useEffect callback. Like so:
useEffect(() => {
const listener = e => {
e.preventDefault()
console.log(showMenu, ' useEffect - touchmove')
}
document.body.addEventListener('touchmove', listener, { passive: false })
return () = {
document.body.removeEventListener('touchmove', listener, { passive: false })
}
}, [showMenu])
I'm trying to use this.$refs.cInput.focus() (cInput is a ref) and it's not working. I'd be able to hit g and the input should pop up and the cursor should focus in it, ready to input some data. It's showing but the focus part is not working. I get no errors in the console.
Vue.component('coordform', {
template: `<form id="popup-box" #submit.prevent="process" v-show="visible"><input type="text" ref="cInput" v-model="coords" placeholder =""></input></form>`,
data() {
{
return { coords: '', visible: false }
}
},
created() {
window.addEventListener('keydown', this.toggle)
},
mounted() {
},
updated() {
},
destroyed() {
window.removeEventListener('keydown', this.toggle)
},
methods: {
toggle(e) {
if (e.key == 'g') {
this.visible = !this.visible;
this.$refs.cInput.focus() //<--------not working
}
},
process() {
...
}
}
});
You can use the nextTick() callback:
When you set vm.someData = 'new value', the component will not
re-render immediately. It will update in the next βtickβ, when the
queue is flushed. [...]
In order to wait until Vue.js has finished updating the DOM after a data
change, you can use Vue.nextTick(callback) immediately after the data
is changed. The callback will be called after the DOM has been
updated.
(source)
Use it in your toggle function like:
methods: {
toggle(e) {
if (e.key == 'g') {
this.visible = !this.visible;
this.$nextTick(() => this.$refs.cInput.focus())
}
}
}
In my case nextTick does not worked well.
I just used setTimeout like example below:
doSearch () {
this.$nextTick(() => {
if (this.$refs['search-input']) {
setTimeout(() => {
this.$refs['search-input'].blur()
}, 300)
}
})
},
I think that for your case code should be like below:
toggle(e) {
if (e.key == 'g') {
this.visible = !this.visible;
setTimeout(() => { this.$refs.cInput.focus() }, 300)
}
}
Not sure if this is only applicable on Vue3 but if you want to show focus on an input box from inside a component, it's best for you to setup a transition. In the transition, there is an event called #after-enter. So after the animation transition on enter is completed, the #after-enter is executed. The #after-enter will be the event that will always get executed after your component shows up.
I hope this helps everyone who are having issues with the:
***this.$nextTick(() => this.$refs.searchinput.focus());***
It may not be working the way you wanted it to because chances are the initial focus is still on the parent and the input element has not been displayed yet or if you placed it under mounted, it only gets executed once and no matter how you show/hide the component (via v-if or v-show), the nextTick line was already executed on mount() and doesn't get triggered on show of the component again.
Try this solution below:
<template>
<transition name="bounce" #after-enter="afterEnter" >
<div>
<input type="text" ref="searchinput" />
</div>
</transition>
</template>
<script>
methods: {
afterEnter() {
setTimeout(() => {
this.$refs.searchinput.focus();
}, 200);
},
}
</script>
I have a usecase where i need to unmount my react component. But in some cases, the particular react component is unmounted by a different function.
Hence, I need to check if the component is mounted before unmounting it.
Since isMounted() is being officially deprecated, you can do this in your component:
componentDidMount() {
this._ismounted = true;
}
componentWillUnmount() {
this._ismounted = false;
}
This pattern of maintaining your own state variable is detailed in the ReactJS documentation: isMounted is an Antipattern.
I'll be recommended you to use the useRef hook for keeping track of component is mounted or not because whenever you update the state then react will re-render the whole component and also it will trigger the execution of useEffect or other hooks.
function MyComponent(props: Props) {
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true;
return () => { isMounted.current = false }
}, []);
return (...);
}
export default MyComponent;
and you check if the component is mounted with if (isMounted.current) ...
I think that Shubham answer is a workaround suggested by react for people that need to transition their code to stop using the isMounted anti-pattern.
This is not necessarily bad, but It's worth listing the real solutions to this problem.
The article linked by Shubham offers 2 suggestions to avoid this anti pattern. The one you need depends on why you are calling setState when the component is unmounted.
if you are using a Flux store in your component, you must unsubscribe in componentWillUnmount
class MyComponent extends React.Component {
componentDidMount() {
mydatastore.subscribe(this);
}
render() {
...
}
componentWillUnmount() {
mydatastore.unsubscribe(this);
}
}
If you use ES6 promises, you may need to wrap your promise in order to make it cancelable.
const cancelablePromise = makeCancelable(
new Promise(r => component.setState({...}}))
);
cancelablePromise
.promise
.then(() => console.log('resolved'))
.catch((reason) => console.log('isCanceled', reason.isCanceled));
cancelablePromise.cancel(); // Cancel the promise
Read more about makeCancelable in the linked article.
In conclusion, do not try to patch this issue by setting variables and checking if the component is mounted, go to the root of the problem. Please comment with other common cases if you can come up with any.
Another solution would be using Refs . If you are using React 16.3+, make a ref to your top level item in the render function.
Then simply check if ref.current is null or not.
Example:
class MyClass extends React.Component {
constructor(props) {
super(props);
this.elementRef = React.createRef();
}
checkIfMounted() {
return this.elementRef.current != null;
}
render() {
return (
<div ref={this.elementRef} />
);
}
}
Using #DerekSoike answer, however in my case using useState to control the mounted state didn't work since the state resurrected when it didn't have to
What worked for me was using a single variable
myFunct was called in a setTimeout, and my guess is that when the same component initialized the hook in another page it resurrected the state causing the memory leak to appear again
So this didn't work for me
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => setIsMounted(false)
}, [])
const myFunct = () => {
console.log(isMounted) // not always false
if (!isMounted) return
// change a state
}
And this did work for me
let stillMounted = { value: false }
useEffect(() => {
stillMounted.value = true
return () => (stillMounted.value = false)
}, [])
const myFunct = () => {
if (!stillMounted.value) return
// change a state
}
I got here because I was looking for a way to stop polling the API.
The react docs does cover the websocket case, but not the polling one.
The way I worked around it
// React component
React.createClass({
poll () {
if (this.unmounted) {
return
}
// otherwise, call the api
}
componentWillUnmount () {
this.unmounted = true
}
})
it works. Hope it helps
Please, let me know if you guys know any failing test case for this =]
If you're using hooks:
function MyComponent(props: Props) {
const [isMounted, setIsMounted] = useState<boolean>(false);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
return () => {
setIsMounted(false);
}
}, []);
return (...);
}
export default MyComponent;
The same idea but enother implementation
/**
* component with async action within
*
* #public
*/
class MyComponent extends Component {
constructor ( ...args ) {
// do not forget about super =)
super(...args);
// NOTE correct store "setState"
let originSetState = this.setState.bind(this);
// NOTE override
this.setState = ( ...args ) => !this.isUnmounted&&originSetState(...args);
}
/**
* no necessary setup flag on component mount
* #public
*/
componentWillUnmount() {
// NOTE setup flag
this.isUnmounted = true;
}
/**
*
* #public
*/
myCustomAsyncAction () {
// ... code
this.setState({any: 'data'}); // do not care about component status
// ... code
}
render () { /* ... */ }
}
I have solve with hot reload and react to different it events β
const {checkIsMounted} = useIsMounted(); //hook from above
useEffect(() => {
//here run code
return () => {
//hot reload fix
setTimeout(() => {
if (!checkIsMounted()) {
//here we do unmount action
}
}, 100);
};
}, []);
Pproblem
There is a problem when using the useState() hook. If you are also trying to do something else in a useEffect function (like fetching some data when the component is mounted) at the same time with setting the new value for the hook,
const [isMounted, setIsMounted] = useState(false)
useEffect(() =>
{
setIsMounted(true) //should be true
const value = await fetch(...)
if (isMounted) //false still
{
setValue(value)
}
return () =>
{
setIsMounted(false)
}
}, [])
the value of the hook will remain the same as the initial value (false), even if you have changed it in the beggining. It will remain unchanged for that first render, a new re-render being required for the new value to be applied.
For some reason #GWorking solution did not work too. The gap appears to happen while fetching, so when data arrives the component is already unmounted.
Solution
You can just combine both and and check if the component is unmounted during any re-render and just use a separate variable that will keep track to see if the component is still mounted during that render time period
const [isMounted, setIsMounted] = useState(false)
let stillMounted = { value: false }
useEffect(() =>
{
setIsMounted(true)
stillMounted.value = true
const value = await fetch(...)
if (isMounted || stillMounted.value) //isMounted or stillMounted
{
setValue(value)
}
return () =>
{
(stillMounted.value = false)
setIsMounted(false)
}
}, [isMounted]) //you need to also include Mounted values
Hope that helps someone!
There's a simple way to avoid warning
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
You can redefine setState method inside your class component using this pattern:
componentWillUnmount() {
this._unmounted = true;
}
setState(params, callback) {
this._unmounted || super.setState(params, callback);
}
i found that the component will be unmounted, generate fill this var
if(!this._calledComponentWillUnmount)this.setState({vars});
You can use:
myComponent.updater.isMounted(myComponent)
"myComponent" is instance of your react component.
this will return 'true' if component is mounted and 'false' if its not..
This is not supported way to do it. you better unsubscribe any async/events
on componentWillUnmount.