React hooks - how to test multiple useState hooks - reactjs

I have a component that has few useState hooks:
const [resizing, setResizing] = React.useState(false);
const [x, setX] = React.useState(0);
const [y, setY] = React.useState(0);
And in some places I am calling more than one hook at a time, for example here in onResize, I am calling both setResizing and setX or setY hook:
<ResizableBox
height={520}
width={370}
minConstraints={[370, 520]}
maxConstraints={[Infinity, Infinity]}
className={classes.resizable}
onResize={(e) => {
if (e.movementX !== 0) {
setResizing(true);
setX((prev) => prev + e.movementX);
} else if (e.movementY !== 0) {
setResizing(true);
setY((prev) => prev + e.movementY / 2);
}
}}
onResizeStop={() => {
setResizing(false);
}}
>
I am used to testing class components where it is easy to test state changes.
I would like to test it with something like this:
const setXSpy = jest.spyOn(React, 'setX');
const setYSpy = jest.spyOn(React, 'setY');
const setResizeSpy = jest.spyOn(React, 'setResize');
it('calls useState hooks correctly', () => {
resizableBox.props.onResize({movementX: 1});
expect(setXSpy).toHaveBeenCalledWith(1);
expect(setYSpy).not.toHaveBeenCalled();
expect(setResizeSpy).toHaveBeenCalledWith(true);
});
But, I am not sure how test hooks like that in this example?

You're testing the implementation details of that component which is not a good idea (you're basically coupling the test to that implementation, instead of using the test to determine that the component does what it is supposed to do).
Instead I would try and find a way to test the component as a user would and determine that the output is correct.
Start by integrating testing-library and then follow some of the patterns described here and here.
The question is, what are you trying to test? what is the expected outcome of this test and which behaviour will it cover?
LE: in your case, as I see you're trying to test React-Resizable, you can try and mouseDown on the resize handle, then emit mouseMove and mouseUp and see if the right things happens (I don't see from the code what you are doing with the state values, what are you using them for)

The code I use to do that is the following:
const mockSetState = jest.fn();
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: () => ['', mockSetState],
}));
it('...', () => {
expect(mockSetState).toHaveBeenNthCalledWith(1, first expected value);
expect(mockSetState).toHaveBeenNthCalledWith(2, second expected value');
})

Related

React Hook not notifying state change to components using the hook

So I have a Hook
export default function useCustomHook() {
const initFrom = localStorage.getItem("startDate") === null? moment().subtract(14, "d"): moment(localStorage.getItem("startDate"));
const initTo = localStorage.getItem("endDate") === null? moment().subtract(1, "d"): moment(localStorage.getItem("endDate"));
const [dates, updateDates] = React.useState({
from: initFrom,
to: initTo
});
const [sessionBreakdown, updateSessionBreakdown] = React.useState(null);
React.useEffect(() => {
api.GET(`/analytics/session-breakdown/${api.getWebsiteGUID()}/${dates.from.format("YYYY-MM-DD")}:${dates.to.format("YYYY-MM-DD")}/0/all/1`).then(res => {
updateSessionBreakdown(res.item);
console.log("Updated session breakdown", res);
})
},[dates])
const setDateRange = React.useCallback((startDate, endDate) => {
const e = moment(endDate);
const s = moment(startDate);
localStorage.setItem("endDate", e._d);
localStorage.setItem("startDate", s._d);
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
}, [])
const getDateRange = () => {
return [dates.from, dates.to];
}
return [sessionBreakdown, getDateRange, setDateRange]
}
Now, this hook appears to be working in the network inspector, if I call the setDateRanger function I can see it makes the call to our API Service, and get the results back.
However, we have several components that are using the sessionBreakdown return result and are not updating when the updateSessionBreakdown is being used.
i can also see the promise from the API call is being fired in the console.
I have created a small version that reproduces the issue I'm having with it at https://codesandbox.io/s/prod-microservice-kq9cck Please note i have changed the code in here so it's not reliant on my API Connector to show the problem,
To update object for useState, recommended way is to use callback and spread operator.
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
Additionally, please use useCallback if you want to use setDateRange function in any other components.
const setDateRange = useCallback((startDate, endDate) => {
const e = moment(endDate);
const s = moment(startDate);
localStorage.setItem("endDate", e._d);
localStorage.setItem("startDate", s._d);
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
}, [])
Found the problem:
You are calling CustomHook in 2 components separately, it means your local state instance created separately for those components. So Even though you update state in one component, it does not effect to another component.
To solve problem, call your hook in parent component and pass the states to Display components as props.
Here is the codesandbox. You need to use this way to update in one child components and use in another one.
If wont's props drilling, use Global state solution.

How to create safe dispatch function in React

I am trying to tackle the common warning message in React tests
console.error
Warning: An update to EntryList inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
I have created a hook that can be passed a react dispatcher function
export const useSafeDispatches = (...dispatches) => {
const mounted = useRef(false);
useLayoutEffect(() => {
mounted.current = true;
return () => (mounted.current = false);
}, []);
const safeDispatch = useCallback(
(dispatch) =>
(...args) =>
mounted.current ? dispatch(...args) : void 0,
// eslint-disable-next-line
[mounted.current]
);
return dispatches.map(safeDispatch);
};
and, I am using it like this
function MyComponent() {
const [counter, d] = useState(0);
const [setCounter] = useSafeDispatches(d);
return <button onClick={() => setCounter(counter + 1)}>{counter}<button>
}
Yet, I am getting the same error in my tests (where I try to call setState after the component been unmounted)
You are getting this warning because an update has been made to the state of your component after the test is finished.
Search for an async update to the state, and then include an assertion for it in your test.
This warning is the way React is telling you that something happened to your component after the test is finished, and you are not fully testing the component, and you may have missed testing that.
In case you just need to get it done right I suggest you to use existing hooks library with appropriate functionality.
For example that library does exactly what you want
import { useSafeState } from '#react-hookz/web';
function MyComponent() {
const [counter, setCounter] = useSafeState(0);
return <button onClick={() => setCounter((prev) => prev + 1)}>{counter}<button>
}
The warning is not due to an unmounted component. It's due to a test finishing before the last state update. Therefor as another comment says use act which is designed for that.
Using waitFor from testing-library which uses act under the hood:
// this test is using testing-library waitFor
test('does something', async() => {
const { getByRole } = render(<MyCustomButton />);
await waitFor(() => fireEvent.click(getByRole('button')));
expect(someCallback).toBeCalled(); // or some value to be increased
});

react function component issue with usEffect and useState

Sometimes I have to use some native js libaray api, So I may have a component like this:
function App() {
const [state, setState] = useState(0)
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1.innerHTML = 'h1h1h1h1h1h1'
container.append(h1)
h1.onclick = () => {
console.log(state)
}
}, [])
return (
<div>
<button onClick={() => setState(state => state + 1)}>{state}</button>
<div id="container"></div>
</div>
)
}
Above is a simple example. I should init the lib after react is mounted, and bind some event handlers. And the problem is coming here: As the above shown, if I use useEffect() without state as the item in dependencies array, the value state in handler of onclick may never change. But if I add state to dependencies array, the effect function will execute every time once state changed. Above is a easy example, but the initialization of real library may be very expensive, so that way is out of the question.
Now I find 3 ways to reslove this, but none of them satisfy me.
Create a ref to keep state, and add a effect to change it current every time once state changed. (A extra variable and effect)
Like the first, but define a variable out of the function instead of a ref. (Some as the first)
Use class component. (Too many this)
So is there some resolutions that solve problems and makes code better?
I think you've summarised the options pretty well. There's only one option i'd like to add, which is that you could split your code up into one effect that initializes, and one effect that just changes the onclick. The initialization logic can run just once, and the onclick can run every render:
const [state, setState] = useState(0)
const h1Ref = useRef();
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1Ref.current = h1;
// Do expensive initialization logic here
}, [])
useEffect(() => {
// If you don't want to use a ref, you could also have the second effect query the dom to find the h1
h1ref.current.onClick = () => {
console.log(state);
}
}, [state]);
Also, you can simplify your option #1 a bit. You don't need to create a useEffect to change ref.current, you can just do that in the body of the component:
const [state, setState] = useState(0);
const ref = useRef();
ref.current = state;
useEffect(() => {
const container = document.querySelector('#container');
// ...
h1.onClick = () => {
console.log(ref.current);
}
}, []);

Logical understanding react hooks, difference between useState and useEffect (or state and lifecycle methods)

I cannot understand the difference between useState and useEffect. Specifically, the difference between state and lifecycle methods. For instance, I have watched tutorials and seen this example for useEffect:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
const add = () => {
setVal((x) => { return x + 1 })
}
useEffect(() => {
if (value > 0) {
document.title = `Title: ${value}`
}
},[value])
return <>
<h1>{value}</h1>
<button className="btn" onClick={add}>add</button>
</>;
};
When we click the button, the title of the document shows us increasing numbers by one. When I removed the useEffect method and did this instead:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
document.title = `Title: ${value}`
const add = () => {
setVal((x) => { return x + 1 })
}
return <>
<h1>{value}</h1>
<button className="btn" onClick={add}>add</button>
</>;
};
It worked same as the previous code.
So, how does useEffect actually work? What is the purpose of this method?
P.S. Do not send me links of documentation or basic tutorials, please. I have done my research. I know what I am missing is very simple, but I do not know what is it or where to focus to solve it.
Using useEffect to track stateful variable changes is more efficient - it avoids unnecessary calls by only executing the code in the callback when the value changes, rather than on every render.
In the case of document.title, it doesn't really matter, since that's an inexpensive operation. But if it was runExpensiveFunction, then this approach:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
runExpensiveOperation(value);
would be problematic, since the expensive operation would run every time the component re-renders. Putting the code inside a useEffect with a [value] dependency array ensures it only runs when needed - when the value changes.
This is especially important when API calls that result from state changes are involved, which is pretty common. You don't want to call the API every time the component re-renders - you only want to call it when you need to request new data, so putting the API call in a useEffect is a better approach than putting the API call in the main component function body.

how to prevent re-render if fetch from many sources in react hooks?

I'm using react hooks in React Native.
My problem is that the function of useState which to initialize state makes re-render.
So if I set state like below
const [A, setA] = useState(false);
const [B, setB] = useState(false);
const [C, setA] = useState(false);
// ...
const testFunc = () => {
setA(true);
setB(true);
setC(true);
}
EDITED
I think examples were wrong.
Here's another example.
const useFetch(coords) {
const [example, setExample] = useState([])
const [checker, setChecker] = useState(false);
const fetchData = () => {
axios.fetch(`url+${coords.latitue}+${coords.longitude}`).then(){
setExample(res.data());
setChecker(true);
}
}
useEffect(() => {
fetchData();
}, [coords])
return example;
}
const useLocation = () => {
...
return coords;
}
const App = () => {
const coords = useLocation();
const example = useFetch(coords); // example is undefined.
const [data, setData] = useState(example); // data is undefined.
}
It causes many re-render as many as I use the set function.
Is this natural thing?
If I don't want to make this re-render, can't use the set function multiple times?
You can not do it in straightforward way. I will suggest you the two solutions for it.
Solution 1: Combine states in one object.
const [value, setValue] = useState({A: false, B: false, C: false});
// ...
const testFunc = () => {
setValue({A: true, B: true, C: true});
}
Solution 2: Another solution is useReducer.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{A: false, B: false, C: false}
);
// ...
const testFunc = () => {
setState({A: true, B: true, C: true});
}
Here I have implemented your another example: https://stackblitz.com/edit/react-usestate-wcjshg
Hope this will help for you!
React does not batch state updates if they are triggered outside React-based event. That means, if you want your state updates to be batched you need to wrap it on an event handle such as onClick.
If your local component state is non-trival and/or using an event handler is not an option, I'd recommend you to use useReducer as you can batch your state updates within that.
This appears to be normal React behavior. It works the exact same way if you were to call setState() in a class component multiple times.
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like a setTimeout().
I think there's plans long-term to always batch events, but not sure on the details
Sources:
https://github.com/facebook/react/issues/14259#issuecomment-439632622
https://github.com/facebook/react/issues/14259#issuecomment-468937068
As stated in the other answers, React does not batch state updates if they are triggered outside React-based events (in then for example), one of the solutions is to merge your state in one object and call setState one time. But if you like to keep your state separated, the solution is to use ReactDOM.unstable_batchedUpdates like this :
const fetchData = () => {
axios.fetch(`url+${coords.latitue}+${coords.longitude}`).then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setExample(res.data());
setChecker(true);
});
});
}
Recommended by Dan Abramov here

Resources