This is my test
it('should run', async () => {
const { container } = renderWithContainers(<Wrapper />)
window.innerHeight = 50
await wait(() => fireEvent(window, new Event('resize')))
expect(getByTestId(container, 'myComponent')).toHaveStyle(`position: relative`)
})
This fails because the original position is fixed and im clearly using the container defined before the resize event fires...so how can I get it to "rerender" to use the updated container?
in my code once it rerenders it then has a different position (just not in the test)
I've done this:
const { container, rerender } = renderWithContainers(<Wrapper />)
window.innerHeight = 50
await wait(() => fireEvent(window, new Event('resize')))
rerender(<Wrapper />
but this breaks with invariant Violation: Could not find "store" react testing library but I have wrapped up all that store/provider logic inside renderWithContainers so how do I reuse that?
I want to do something like this: rerender(renderWithContainers(<Wrapper />)) but clearly that wont work
just to be ultra clear, I want to rerender my component after event listener has fired so I can see the updated component
Related
I'm just playing around with ReactJS and trying to figure out some strange behavior with the useState hook.
A component should not re-rendered if the state is set with the same primitive value (Boolean) as it was before
const useScroll = ({positionToCross = 10}) => {
const window = useWindow();
const [isPositionCrossed, setIsPositionCrossed] = useState(window.scrollY > positionToCross);
useEffect(() => {
const onScroll = function (e) {
window.requestAnimationFrame(function () {
const lastKnownScrollPosition = window.scrollY;
setIsPositionCrossed(lastKnownScrollPosition > positionToCross);
});
}
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener("scroll", onScroll)
}
}, []);
console.log(`useScroll - render window.scrollY = ${window.scrollY.toFixed(0)} isPositionCrossed = `, isPositionCrossed)
return {isPositionCrossed}
}
here is the console output - you can see the component and the hook are both rendered two times with "true" (after scrolled over 100px)
"useScroll - render window.scrollY = 101 isPositionCrossed = ", true
"useScroll - render window.scrollY = 103 isPositionCrossed = ", true
If you try simple code that on click handler setState and if you click two times and in each update state with same value the component again re-render.
As react doc says:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
I hope the answers from this post and this github discussion help you to understand why this happens
and there are another related topics like this post and this one
I'm trying to test whether a React button component's text changes on-click. This is roughly what gets returned inside the component I'm testing (let's call it RandComponent):
return (<>
...
<ButtonComponent
data-testid='testButton'
>
{getButtonText()}
</ButtonComponent>
</>)
I want to be able to access whatever getButtonText() returns in Jest. I've looked online and in enzyme you can do something like this in the test file:
it('test button text toggle', async ()=>{
let container;
await act(async ()=> container = render(<RandComponent/>));
const button = getByTestId('testButton');
expect(button.text()).toEqual(whatever getButtonText() is supposed to return);
}
My problem is that I can't use enzyme... is there a way to get the child prop text like in the last line (button.text()) in Jest?
This requires to stub ButtonComponent by means of Jest and assert props that were provided to it:
jest.mock('...ButtonComponent module...', () => jest.fn());
...
let buttonProps;
ButtonComponent.mockImplementation(props => {
buttonProps = props;
return <div>ButtonComponent</div>
});
container = render(<RandComponent/>));
...
expect(buttonProps.children).toBe(...);
This is boilerplate code that Enzyme is supposed to save from.
I'm trying to implement some single input form with easy "expand" animation to when going to/from edit mode.
Basically I created a ghost element containing value, next to this element is icon button working as edit/save. When you click on the edit button, input with value should appear instead of ghost element and width of input should expand/decrease to constant defined.
I have so far this piece of code, which mostly works fine, but for expand it sometimes doesn't animate and I don't know why.
toggleEditMode = () => {
const { editMode } = this.state
if (editMode) {
this.setState(
{
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
setTimeout(() => {
this.setState({
editMode: false
})
}, 150)
})
}
)
} else {
this.setState(
{
editMode: true,
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
this.setState({
inputWidth: INPUT_WIDTH
})
})
}
)
}
}
You can take a look on example here. Could someone explain what's wrong or help me to find solution? If I'll add another setTimeout(() => {...expand requestAnimationFrame here...}, 0) in code, it starts to work, but I don't like the code at all.
This answer explains what's going on in detail and how to fix it. However, I wouldn't actually suggest implementing it.
Custom animations are messy, and there are great libraries that handle the dirty work for you. They wrap the refs and requestAnimationFrame code and give you a declarative API instead. I have used react-spring in the past and it has worked very well for me, but Framer Motion looks good as well.
However, if you'd like to understand what's happening in your example, read on.
What's happening
requestAnimationFrame is a way to tell the browser to run some code every time a frame is rendered. One of the guarantees you get with requestAnimationFrame is that the browser will always wait for your code to complete before the browser renders the next frame, even if this means dropping some frames.
So why doesn't this seem to work like it should?
Updates triggered by setState are asynchronous. React doesn't guarantee a re-render when setState is called; setState is merely a request for a re-evaluation of the virtual DOM tree, which React performs asynchronously. This means that setState can and usually does complete without immediately changing the DOM, and that the actual DOM update may not occur until after the browser renders the next frame.
This also allows React to bundle multiple setState calls into one re-render, which it sometimes does, so the DOM may not update until the animation is complete.
If you want to guarantee a DOM change in requestAnimationFrame, you'll have to perform it yourself using a React ref:
const App = () => {
const divRef = useRef(null);
const callbackKeyRef = useRef(-1);
// State variable, can be updated using setTarget()
const [target, setTarget] = useState(100);
const valueRef = useRef(target);
// This code is run every time the component is rendered.
useEffect(() => {
cancelAnimationFrame(callbackKeyRef.current);
const update = () => {
// Higher is faster
const speed = 0.15;
// Exponential easing
valueRef.current
+= (target - valueRef.current) * speed;
// Update the div in the DOM
divRef.current.style.width = `${valueRef.current}px`;
// Update the callback key
callbackKeyRef.current = requestAnimationFrame(update);
};
// Start the animation loop
update();
});
return (
<div className="box">
<div
className="expand"
ref={divRef}
onClick={() => setTarget(target === 100 ? 260 : 100)}
>
{target === 100 ? "Click to expand" : "Click to collapse"}
</div>
</div>
);
};
Here's a working example.
This code uses hooks, but the same concept works with classes; just replace useEffect with componentDidUpdate, useState with component state, and useRef with React.createRef.
It seems to be a better direction to use CSSTransition from react-transition-group, in your component:
function Example() {
const [tr, setIn] = useState(false);
return (
<div>
<CSSTransition in={tr} classNames="x" timeout={500}>
<input
className="x"
onBlur={() => setIn(false)}
onFocus={() => setIn(true)}
/>
</CSSTransition>
</div>
);
}
and in your css module:
.x {
transition: all 500ms;
width: 100px;
}
.x-enter,
.x-enter-done {
width: 400px;
}
It lets you avoid using setTimeouts and requestAnimationFrame and would make the code cleaner.
Codesandbox: https://codesandbox.io/s/csstransition-component-forked-3o4x3?file=/index.js
I have a functional component that keeps a state. This state I try to manipulate using an onClick event in an SVG. The SVG is in another component and has the addAndRemoveSelectedCabin method passed to it via props. I loop through the elements in an useEffect and add an eventListener. This doesn't work. The useEffect with the selectedCabins dependency logs the new number only. It seems the state returns to the initial state after every stateChange.
This is the state and method in the parent component.
const [selectedCabins, setSelectedCabins] = useState([0]);
const addRemoveSelectedCabin = id => {
const newArr = [...selectedCabins, id];
setSelectedCabins(newArr);
}
useEffect(() => {
console.log(selectedCabins);
}, [selectedCabins])
This is how I call the method. [UPDATE]
useEffect(() =>
{
const cabins = document.querySelectorAll(".cabin");
cabins.forEach(cabin =>
{
const id = cabin.getAttributeNS(null, "id").substring(1, 5);
const found = cabinsData.find(el => el.id === id)
if (found && found.status === "available")
{
cabin.classList.add("green")
cabin.addEventListener('click', () => addRemoveSelectedCabin(id));
} else if (found && found.status === "booked")
{
cabin.classList.add("gray")
}
})
}, [])
Console:
[0]
(2) [0, "1105"]
(2) [0, "1101"]
This works if I put the onClick directly in the SVG element. Does anyone know why this is?
<rect
id="C1105"
x="749.4"
y="58.3"
className="cabin"
width="36.4"
height="19.9"
onClick={() => addRemoveSelectedCabin(1105)}
>
<title>1105</title>
</rect>
As I said in my comment, you are binding addRemoveSelectedCabin in the first render. useEffect is only executed once since you pass an empty dependency list. addRemoveSelectedCabin closes over selectedCabins which at that point in time has the value [0].
Why am I seeing stale props or state inside my function? from the React documentation has more information about this.
The solution in your case is simple: Pass a function to the setter to get the "current" state value. Don't reference the state value in the component:
const addRemoveSelectedCabin = id => {
setSelectedCabins(selectedCabins => [...selectedCabins, id]);
}
Having said that, this is still an odd thing to do in React world. You should reevaluate your assumptions that make you think you have to do it that way.
It's not all the elements that should have a click listener.
Depending on how you actually render the elements, that's easy to do. JSX/React is just JavaScript. Whether you have a condition that adds the event handler or not or whether you have a condition that sets onClick or not is basically the same.
But without a more complete example there is not much we can suggest.
So I've got this hook to return the windowWidth for my App components. I'll call this Option #1.
import {useEffect, useState} from 'react';
function useWindowWidth() {
const [windowWidth,setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}
export default useWindowWidth;
And right now I'm basically using it on every component that depends on the window width to render, like:
function Component(props) {
const windowWidth = useWindowWidth();
return(
// RETURN SOMETHING BASED ON WINDOW WIDTH
);
}
And since the hook has an event listener for the resize events, the component stays responsive even after window resizes.
But I'm worried that I'm attaching a new listener for every component that uses that hook and it might slow things down at some point. And I've though of other approach:
Option #2
I use the useWindowWidth() hook only one time, inside a top level component like <App/> and I'll provide the windowWidth value down the chain via context.
Like:
function App() {
const windowWidth = useWindowWidth();
return(
<WindowWidthContext.Provider value={windowWidth}>
<Rest_of_the_app/>
</WindowWidthContext.Provider>
);
}
And then, every component that needs it could get it via:
function Component() {
const windowWidth = useContext(WindowWidthContext);
return(
// SOMETHING BASED ON WINDOW WIDTH
);
}
QUESTION
Am I right in being bothered by that fact that I'm setting up multiple resize listeners with Option #1 ? Is Option #2 a good way to optmize that flow?
If your window with is used by so many components as you mentioned, you must prefer using context. As it reads below:
Context is for global scope of application.
So, #2 is perfect choice here per react.
First approach #1 might be good for components in same hierarchy but only up-to 2-3 levels.
I'm not sure if adding and removing event listeners is a more expensive operation than setting and deleting map keys but maybe the following would optimize it:
const changeTracker = (debounceTime => {
const listeners = new Map();
const add = fn => {
listeners.set(fn, fn);
return () => listeners.delete(fn);
};
let debounceTimeout;
window.addEventListener('resize', () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(
() => {
const width=window.innerWidth;
listeners.forEach(l => l(width))
},
debounceTime
);
});
return add;
})(200);
function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(
() => window.innerWidth
);
useEffect(
() =>//changeTracker returns a remove function
changeTracker((width) =>
setWindowWidth(width)
),
[]
);
return windowWidth;
}
As HMR said in an above thread, my solution was to use redux to hold the width value. With this strategy you only need one listener and you can restrict how often you update with whatever tool you like. You could check if the width value is within the range of a new breakpoint and only update redux when that is true. This only works if your components dont need a steady stream of the window width, in that case just debounce.