Sandbox: https://codesandbox.io/s/falling-brook-de970
Packages: React Spectrum, Tailwind Transition Component
The Problem: The useOverlayPosition users two refs, a triggerRef and an overlayRef to determine the absolute positioning that should be applied to the popup.
Without transitions, there are no issues with positioning.
When I add the Transition component, I notice that the overlayRef isn't set once rendered and therefore doesn't allow the useOverlayPosition hook to determine the right positioning props to apply. I assume this is because the state.isOpen is false and the children of the Transition component aren't rendered yet.
A subsequent press while open fixes the issue.
Clicking off the button continues the issue.
Clicking on the button after it's open will fix the issue, but I don't understand why.
The issue was resolved using a callbackRef instead of useRef. This allowed the component to re-render when the ref was assigned.
In a useRef, updating ref.current does not trigger a re-render but with a callbackRef it will.
Idea taken from: https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
const [overlayRef, setOverlayRef] = React.useState<
React.RefObject<HTMLElement>
>({ current: null });
const callbackRef = React.useCallback((node) => {
if (node !== null) {
setOverlayRef({ current: node });
}
}, []);
Related
I thought react-spring useSpring() causes the component to re-render a lot, so if it is a component that already has a lot of CPU intensively work to do, then react-spring is not best suited for the task.
I can see that it re-renders a lot, in their example:
https://codesandbox.io/s/musing-dew-9tfi9?file=/src/App.tsx
(by looking at the console.log output, which has a lot of print out of renderCount. The print out is a lot more when we change the duration to 5000 which is 5 seconds).
Likewise, if it is a component that is similar to react-spring, it'd render a lot:
https://codesandbox.io/s/wonderful-currying-9pheq
However, the following code:
let renderCount = 0
export default function App() {
const styles = useSpring({
loop: true,
to: [
{ opacity: 1, color: '#ffaaee' },
{ opacity: 0.5, color: 'rgb(14,26,19)' },
{ transform: 'translateX(100px)' },
{ transform: 'translateX(0px)' },
],
from: { opacity: 0, color: 'red', transform: 'translateX(0px)' },
config: { duration: 500 },
})
console.log('renderCount', ++renderCount)
return <a.div style={styles}>I will fade in and out</a.div>
}
Demo: https://codesandbox.io/s/dazzling-rgb-j2bx3?file=/src/App.tsx
We can see that the renderCount hardly get printed out at all. react-spring should need to keep on updating the style of the component, so after a minute, I'd expect a lot of print out of renderCount like the first two examples above, but it does not.
How and why doesn't react-spring cause a lot of re-rendering in this case, and how do we know in what situation would react-spring cause a lot of re-rendering (and how to prevent it)?
react-spring updates styles incrementally to create animations (as opposed to css animations with transition).
Naive animations outside React
If react-spring was to exist outside of React (which it OBVIOUSLY doesn't because then it wouldn't be named react-spring), this could most easily be done by modifying a given element's style by means of Javascript according to some predetermined pattern based on multiple factors (like delay, duration, etc....). One scenario could be
...
setTimeout(() => document.getElementById("#el").style.opacity = 0.34,100)
setTimeout(() => document.getElementById("#el").style.opacity = 0.39,150)
setTimeout(() => document.getElementById("#el").style.opacity = 0.42,200)
...
setTimeout(() => document.getElementById("#el").style.opacity = 1.0, 1000)
Exactly how this would be implemented is of course not the point of this answer and the above would be a very naive implementation, but this is basically what could go on if we wanted to make some animated transition where the interpolation between two endpoints would be calculated and implemented by ourselves (using spring physics) as opposed to in the browser (with css transition).
Naive animations in React
In React, we know that the preferred way to do things is to provide changes inside React, which React then processes after which necessary changes to the DOM is handled by React. Taking the previous (naive) example to React, this would imply some scheme where a state storing the opacity would be updated repeatedly until the desired endpoint was reached.
const Component = () => {
...
const [opacity, setOpacity] = useState(0)
useEffect(() => {
...
setTimeout(() => setOpacity(0.34),100)
setTimeout(() => setOpacity(0.39),150)
setTimeout(() => setOpacity(0.42),200)
...
setTimeout(() => setOpacity(1.0), 1000)
}, [])
return (
<div style={{ opacity }} ... />
)
}
This would work, but as one would expect, it could be quite burdensome since animations are supposed to happen fast and smooth and React rerendering on every animation frame could be problematic; if the component within which animation took place was expensive to render, the animation itself could be suffering and not look very good.
react-spring in React
The solution to this problem by react-spring is to do updates OUTSIDE of React via refs instead. The previous toy example could look like:
const Component = () => {
...
const ref = useRef(null)
useEffect(() => {
if(ref.current) {
...
setTimeout(() => ref.current.style.opacity = 0.34,100)
setTimeout(() => ref.current.style.opacity = 0.39,150)
setTimeout(() => ref.current.style.opacity = 0.42,200)
...
setTimeout(() => ref.current.style.opacity = 1.0, 1000)
}
}, [])
...
return (
<div ref={ref} ... />
)
}
Again, this is an example, exactly how one would implement this in the best way (as in react-spring) is a different story. But we can agree on that if we would log to the console every time the above component rendered, it would only log once even though the opacity would continue to change.
To sum up, when react-spring is used optimally, it uses refs to update properties on DOM elements whereupon React is by-passed. Thanks to this, a component may render only once but still make repeated animations. This particularly applies to the situation when the api is used to perform updates (as oppose to storing a state in a parent component which is set every time we want an animation to take place):
const [spring, api] = useSpring(() => ({ <INITIAL PROPS> })) // use api to make changes
const spring = useSpring({ <INITIAL PROPS }) // rerender component to update props
When using the basic HTML elements supplied by react-spring (such as animated.div, animated.span etc...), react-spring takes care of attaching a ref on the corresponding DOM element and via this ref, it manages to animate the element and therefore also all the content in it. When creating your own custom component wrapped in animated, it is your concern to make sure that your custom component can take a ref (via forwardRef) and to pass it on to the element which should be animated, if you want optimal animations. If you don't do this, the element will be rerendered on every animation frame by react-spring. Even though this works too, it is suboptimal from a performance point of view.
Your examples
In your examples some other things are at play as well. In the first example, the hook useMeasure is being used from react-use-measure. This hook will continuously provide different values from the child component (here height is provided) whereupon the parent will rerender. Since Tree components are nested, whenever ONE Tree component changes height, all parent Tree components whose heights will be changed will also rerender. We therefore see quite a lot of rerendering. Also, because StrictMode is enabled, the number is doubled. useSpring is used without api but it doesn't matter here since the parent rerenders a lot due to useMeasure anyways.
In your second example with react-spring, the api is not used either but since the animation is looped, it doesn't require any state in the parent to be set and so it doesn't rerender. Because the parent doesn't rerender, the animated component doesn't rerender either and so also in this case, it doesn't matter if we use the api or not. In this example if we would like to update the animated props however, using the api to do so would cause the parent NOT to rerender whereas otherwise, the state holding the props to update to would reside in the parent (or a grand-parent) and so the animated component would rerender as well when the parent rerenders.
I have a React Component using a hook to save the scroll position of the component when the component unmounts. This works great but fails when navigating from one set of data to another set of data without the component unmounting.
For instance, imagine the Slack Interface where there is a sidebar of message channels on the left and on the right is a list of messages (messageList). If you were to navigate between two channels, the messageList component would update with a new set of data for the messageList, but the component was never unmounted so scroll position never gets saved.
I came up with a solution that works, but also throws a warning.
My current useEffect hook for the component (stripped down) and the code that currently saves scroll position whenever the messageList ID changes:
// Component...
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Save scroll position when Component unmounts
useEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, []);
// Save scroll position when Parent ID changes
const oldParent = usePrevious(parent);
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
// ...Component
The error this throws is:
Warning: Cannot update a component from inside the function body of a different component.
And the line that is causing it is the setScrollOffset call inside of the last if block. I'm assuming that while this works it is not the way that I should be handling this sort of thing. What is a better way to handle saving scroll position when a specific prop on the component changes?
Add parent._id to the dependency array. Refactor your code to still cache the previous parent id, add that to the dependency, and move the conditional test inside the effect.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect.
// Return previous parent id and cache current
const oldParent = usePrevious(parent);
// Save scroll position when Component unmounts or parent id changes
useEffect(() => {
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id, oldParent]);
If this does't quite fit the bill, then use two effects, one for the mount/unmount and the other for just updates on the parent id.
Thanks to the suggestions of #drew-reese, he got me pointed down the right path. After adopting his solution (which previously I could not get working properly), I was able to isolate my problem to usage with react-router. (connected-react-router in my case). The issue was that the component was rendering and firing the onScroll event handler and overwriting my scroll position before I could read it.
For me the solution ended up being to keep my existing useEffect hook but pull the scroll offset save out of it and into useLayoutEffect (Had to keep useEffect since there is other stuff in useEffect that I removed for the sake of keeping the sample code above lean). useLayoutEffect allowed me to read the current scroll position before the component fired the onScroll event which was ultimately overwriting my saved scroll position reference to 0.
This actually made my code much cleaner overall by removing the need for my usePrevious hook entirely. My useLayoutEffect hook now looks like this:
useLayoutEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id]);
Am getting this warning:
Can't perform a React state update on unmounted component. This is a no-op...
It results from a child component and I can't figure out how to make it go away.
Please note that I have read many other posts about why this happens, and understand the basic issue. However, most solutions suggest cancelling subscriptions in a componentWillUnmount style function (I'm using react hooks)
I don't know if this points to some larger fundamental misunderstanding I have of React,but here is essentially what i have:
import React, { useEffect, useRef } from 'react';
import Picker from 'emoji-picker-react';
const MyTextarea = (props) => {
const onClick = (event, emojiObject) => {
//do stuff...
}
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
});
useEffect(() => {
return () => {
console.log('will unmount');
isMountedRef.current = false;
}
});
return (
<div>
<textarea></textarea>
{ isMountedRef.current ? (
<Picker onEmojiClick={onClick}/>
):null
}
</div>
);
};
export default MyTextarea;
(tl;dr) Please note:
MyTextarea component has a parent component which is only rendered on a certain route.
Theres also a Menu component that once clicked, changes the route and depending on the situation will either show MyTextarea's parent component or show another component.
This warning happens once I click the Menu to switch off MyTextarea's parent component.
More Context
Other answers on StackOverflow suggest making changes to prevent state updates when a component isn't mounted. In my situation, I cannot do that because I didn't design the Picker component (rendered by MyTextarea). The Warning originates from this <Picker onEmojiClick={onClick}> line but I wouldn't want to modify this off-the-shelf component.
That's explains my attempt to either render the component or not based on the isMountedRef. However this doesn't work either. What happens is the component is either rendered if i set useRef(true), or it's never rendered at all if i set useRef(null) as many have suggested.
I'm not exactly sure what your problem actually is (is it that you can't get rid of the warning or that the <Picker> is either always rendering or never is), but I'll try to address all the problems I see.
Firstly, you shouldn't need to conditionally render the <Picker> depending on whether MyTextArea is mounted or not. Since components only render after mounting, the <Picker> will never render if the component it's in hasn't mounted.
That being said, if you still want to keep track of when the component is mounted, I'd suggest not using hooks, and using componentDidMount and componentWillUnmount with setState() instead. Not only will this make it easier to understand your component's lifecycle, but there are also some problems with the way you're using hooks.
Right now, your useRef(true) will set isMountedRef.current to true when the component is initialized, so it will be true even before its mounted. useRef() is not the same as componentDidMount().
Using 'useEffect()' to switch isMountedRef.current to true when the component is mounted won't work either. While it will fire when the component is mounted, useEffect() is for side effects, not state updates, so it doesn't trigger a re-render, which is why the component never renders when you set useRef(null).
Also, your useEffect() hook will fire every time your component updates, not just when it mounts, and your clean up function (the function being returned) will also fire on every update, not just when it unmounts. So on every update, isMountedRef.current will switch from true to false and back to true. However, none of this matters because the component won't re-render anyways (as explained above).
If you really do need to use useEffect() then you should combine it into one function and use it to update state so that it triggers a re-render:
const [isMounted, setIsMounted] = useState(false); // Create state variables
useEffect(() => {
setIsMounted(true); // The effect and clean up are in one function
return () => {
console.log('will unmount');
setIsMounted(false);
}
}, [] // This prevents firing on every update, w/o it you'll get an infinite loop
);
Lastly, from the code you shared, your component couldn't be causing the warning because there are no state updates anywhere in your code. You should check the picker's repo for issues.
Edit: Seems the warning is caused by your Picker package and there's already an issue for it https://github.com/ealush/emoji-picker-react/issues/142
I'm trying to test a state change via React Hooks (useState) by simulating a changed value on a Material UI slider, since that calls my hooks state update function. I'm then trying to verify the change by checking text that's displayed on the page. I feel like I've gotten close, no syntax errors yelling at me anymore, but it seems like the change isn't happening.
I've tried multiple different selectors and found that most people online are importing their Material UI element and using the imported item as the selector. I've tried this with and without .as(0), with a value of 500 as both a string and a number, and other variations.
My slider's onChange method directly calls my hooks state update method with the e.target.value to set as the new state value (and that all works fine in the actual app).
Any ideas of other things I can try? Or maybe there's another better way to test state changing with React hooks?
import ReactDOM from 'react-dom';
import App from './App';
import { createMount } from '#material-ui/core/test-utils';
import Slider from "#material-ui/core/Slider";
describe('App', () => {
let mount;
beforeEach(() => {
mount = createMount();
});
afterEach(() => {
mount.cleanUp();
});
it('updates the volts displayed when the slider value changes', () => {
const wrapper = mount(<App />);
wrapper.find(Slider).at(0).simulate('change', { target: { value: 500 } });
expect(wrapper.find('.volts-screen').text()).toEqual('0.500');
})
}```
ERROR
expect(received).toEqual(expected) // deep equality
Expected: "0.500"
Received: "0.000"
Ended up figuring it out. I imported the slider from material core (in the same way as in my component), and then I used the imported element in my wrapper.find and simulated a change with a second argument of { target: { value: 500 }}.
I thought this wasn't working at first, but realized that because in my case the effect was updating my component state which is async, I needed to add a setTimeout to make sure the update was captured before my check ran.
Saw some similar guidance on other Material UI elements, but didn't realize the slider would be so similar. Will leave this here just in case some other poor soul is scouring the internet for "Slider"-related info in particular ;)
//imported the element as below
import Slider from "#material-ui/core/Slider";
// found in wrapper using that element and simulated change with target value on timeout
it('updates the volts displayed when the slider value changes', () => {
const wrapper = mount(<App />);
wrapper.find(Slider).at(0).simulate('change', { target: { value: 500 } });
setTimeout(() => expect(wrapper.find('.volts-screen').text()).toEqual('0.500'), 0);
})
Just for general knowledge:
To set the value and interact not just with the component (i.e. the Context):
sliderToFind
.prop("children")[1]
.props.ownerState.onChangeCommitted(null, YOUR_VALUE);
sliderToFind.prop("children")[1].props.ownerState.value = YOUR_VALUE;
This is changing the value and firing the prop onChangeCommitted (or others i.e. onChange)
I am currently using react-navigation to do stack- and tab- navigation.
Is it possible to re-render a component every time the user navigates to specific screens? I want to make sure to rerun the componentDidMount() every time a specific screen is reached, so I get the latest data from the server by calling the appropriate action creator.
What strategies should I be looking at? I am pretty sure this is a common design pattern but I failed to see documented examples.
If you are using React Navigation 5.X, just do the following:
import { useIsFocused } from '#react-navigation/native'
export default function App(){
const isFocused = useIsFocused()
useEffect(() => {
if(isFocused){
//Update the state you want to be updated
}
}, [isFocused])
}
The useIsFocused hook checks whether a screen is currently focused or not. It returns a boolean value that is true when the screen is focused and false when it is not.
React Navigation lifecycle events quoted from react-navigation
React Navigation emits events to screen components that subscribe to them. There are four different events that you can subscribe to: willFocus, willBlur, didFocus and didBlur. Read more about them in the API reference.
Let's check this out,
With navigation listeners you can add an eventlistener to you page and call a function each time your page will be focused.
const didBlurSubscription = this.props.navigation.addListener(
'willBlur',
payload => {
console.debug('didBlur', payload);
}
);
// Remove the listener when you are done
didBlurSubscription.remove();
Replace the payload function and change it with your "refresh" function.
Hope this will help.
You can also use also useFocusEffect hook, then it will re render every time you navigate to the screen where you use that hook.
useFocusEffect(()=> {
your code
})
At the request of Dimitri in his comment, I will show you how you can force a re-rendering of the component, because the post leaves us with this ambiguity.
If you are looking for how to force a re-rendering on your component, just update some state (any of them), this will force a re-rendering on the component. I advise you to create a controller state, that is, when you want to force the rendering, just update that state with a random value different from the previous one.
Add a useEffect hook with the match params that you want to react to. Make sure to use the parameters that control your component so it rerenders. Example:
export default function Project(props) {
const [id, setId] = useState(props?.match?.params?.id);
const [project, setProject] = useState(props?.match?.params?.project);
useEffect(() => {
if (props.match) {
setId(props.match?.params.id);
setProject(props.match?.params.project);
}
}, [props.match?.params]);
......
To trigger a render when navigating to a screen.
import { useCallback } from "react";
import { useFocusEffect } from "#react-navigation/native";
// Quick little re-render hook
function useForceRender() {
const [value, setValue] = useState(0);
return [() => setValue(value + 1)];
}
export default function Screen3({ navigation }) {
const [forceRender] = useForceRender();
// Trigger re-render hook when screen is focused
// ref: https://reactnavigation.org/docs/use-focus-effect
useFocusEffect(useCallback(() => {
console.log("NAVIGATED TO SCREEN3")
forceRender();
}, []));
}
Note:
"#react-navigation/native": "6.0.13",
"#react-navigation/native-stack": "6.9.0",