Set state when testing functional component with useState() hook - reactjs

When I tested class component with enzyme I could do wrapper.setState({}) to set state. How can I do the same now, when I am testing function component with useState() hook?
For example in my component I have:
const [mode, setMode] = useState("my value");
And I want to change mode inside my test

When using state from hooks, your test must ignore implementation details like state in order to properly test it.
You can still make sure the component passes the correct state into its children.
You can find a great example in this blog post written by Kent C. Dodds.
Here's an excerpt from it with a code example.
Test that relies on state implementation details -
test('setOpenIndex sets the open index state properly', () => {
const wrapper = mount(<Accordion items={[]} />)
expect(wrapper.state('openIndex')).toBe(0)
wrapper.instance().setOpenIndex(1)
expect(wrapper.state('openIndex')).toBe(1)
})
Test that does not rely on state implementation details -
test('counter increments the count', () => {
const {container} = render(<Counter />)
const button = container.firstChild
expect(button.textContent).toBe('0')
fireEvent.click(button)
expect(button.textContent).toBe('1')
})

This is the way that I found to do it, I'm not saying this is right or wrong. In my case, a block of code was dependent on state being set to a particular value. I will keep my opinions about testing in React to myself.
In your test file:
Adjust your import for the react library
import * as React from 'react'
Then in your test spy on useState and mock its implementation
const stateSetter = jest.fn()
jest
.spyOn(React, 'useState')
//Simulate that mode state value was set to 'new mode value'
.mockImplementation(stateValue => [stateValue='new mode value', stateSetter])
Please be aware that mocking useState this will way apply to all instances where useState is called for your test, so if you have more than one state value that you are looking at, they will all be set to 'new mode value'. Someone else may be able to help you sort that out. Hope it helps.

At top of test file, can be defined first as:
import { useState } from 'react';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn()
}));
const useStateMock: jest.Mock<typeof useState> = useState as never;
After that at each test can be used with different value which is wanted to be tested:
const setValue = jest.fn();
useStateMock
.mockImplementation(() => ['value', setValue]);

Related

Why is the updated state variable not shown in the browser console?

I understand that the method returned by useState is asynchronous, however, when I run the this code, I am delaying the console.log by upto 5 seconds, but it still logs the previous value and not the updated value of the state variable. The updated value would be 2, but it still logs 1. In the react developer tools however, I can see the state changing as I press the button, though I am curious to know why after even such a delay the console prints an obsolete value? This is not the case with class components and setState but with function components and useState.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
setTimeout(() => {
console.log(variable);
}, 2000);
};
return <button onClick={handleClick}>Button</button>;
}
export default App;
In your code your setTimeout is getting the variable value from the closure at the time it was invoked and the callback function to the setTimeout was created. Check this GitHub issue for the detailed explanation.
In the same issue, they talk about utilizing useRef to do what you are attempting. This article by Dan Abramov packages this into a convenient useInterval hook.
State updates are asynchronous. That means, that in order to view the new value, you need to log It on the next render using useEffect and adding it to the dependencies array:
In this example, give a look at the order the logs appear:
First, you will have the current one, and once triggered, you will have the new value, and then it will become the 'old value' until triggered again.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(`new state rolled: ${counter}`);
}, [counter]);
console.log(`Before rolling new State value: ${counter}`);
const handleClick = () => setCounter(counter++)
return <button onClick={handleClick}>Button</button>;
}
export default App;
Another technic to console.log a value afterward a state change is to attach a callback to the setState:
setCounter(counter++, ()=> console.log(counter));
I hope it helps.
A state take some time to update. The proper way to log state when it updates, is to use the useEffect hook.
setTimeout attaches the timer and wait for that time, but it will keep the value of variable from the beginning of the timer, witch is 1
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
};
useEffect(() => {
console.log(variable);
}, [variable]);
return <button onClick={handleClick}>Button</button>;
}
export default App;
This is not the case with class components and setState but with
function components and useState
In class components, React keep the state in this.state & then call the Component.render() method whenever its need to update due to a setState or prop change.
Its something like this,
// pseudocode ( Somewhere in React code )
const app = MyClassComponent();
app.render();
// user invoke a callback which trigger a setState,
app.setState(10);
// Then React will replace & call render(),
this.state = 10;
app.render();
Even though you cannot do this.state = 'whatever new value', React does that internally with class components to save the latest state value. Then react can call the render() method and render method will receive the latest state value from this.state
So, if you use a setTimeout in a class component,
setTimeout(() => {
console.log(this.state) // this render the latest value because React replace the value of `this.state` with latest one
}, 2000)
However in functional component, the behaviour is little bit different, Every time when component need to re render, React will call the component again, And you can think the functional components are like the render() method of class components.
// pseudocode ( Somewhere in React code )
// initial render
const app = MyFuctionalComponent();
// state update trigger and need to update. React will call your component again to build the new element tree.
const app2 = MyFunctionalComponent();
The variable value in app is 1 & variable value in app2 is 2.
Note: variable is just a classic variable which returned by a function that hooked to the component ( The value save to the variable is the value return by the hook when the component was rendering so it is not like this.state i.e its hold the value which was there when the component is rendering but not the latest value )
Therefore according to the Clouser, at the time your setTimeout callback invoke ( Which was called from app ) it should log 1.
How you can log the latest value ?
you can use useEffect which getting invoke once a render phase of a component is finished. Since the render phase is completed ( that mean the local state variables holds the new state values ) & variable changed your console log will log the current value.
useEffect(() => {
console.log(variable);
}, [variable])
If you need the behaviour you have in class components, you can try useRef hook. useRef is an object which holds the latest value just like this.state but notice that updating the value of useRef doesn't trigger a state update.
const ref = useRef(0);
const handleClick = () => {
setVariable(2); // still need to setVariable to trigger state update
ref.current = 2 // track the latest state value in ref as well.
setTimeout(() => {
console.log(ref.current); // you will log the latest value
}, 2000);
};

How to use react-query useQuery inside onSubmit event?

im noob using react query and react with typesript, i don't know how to solve this:
React Hook "useQuery" is called in function "onSubmit" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter.
export const LoginForm = () => {
const { handleSubmit, control } = useForm<IFormInput>({defaultValues: defaultValues, resolver: yupResolver(schema)});
const onSubmit = ({email, password}: IFormInput) => {
const {data, isLoading, error} = useQuery('loginUser', () => startLogin({email, password}));
console.log(data);
console.log(error);
};
...
...
...
}
export const startLogin = ({email, password}: IFormInput) => (
axios.post(loginEndpoint, {email, password}).then(res => res.data)
);
I’d like to add that for logging in a user, you probably don’t want a query but a mutation. Logging someone in is likely not a GET request and it has side effects (it makes the user be logged in), so useMutation is for that. You can define useMutation on the top of your functional component (in accordance with the rules of hooks) and invoke the returned mutate function in the callback:
export const LoginForm = () => {
const { mutate, isLoading } = useMutation(variables => startLogin(variables))
const onSubmit = ({email, password}: IFormInput) => {
mutate({ email, password })
};
First of all, the problem is just an eslint react plugin (rules-of-hooks) complaining about your use of a hooks.
You can tell it to shutup by adding the following comment above the line where the error is happening:
// eslint-disable-line react-hooks/rules-of-hooks
However...
this is only going to prevent you from understanding the correct way of using hooks in react. Before you proceed with the above, this is the correct way to handle your use case.
First move the hook outside of the function, and instead of useQuery, use useMutation:
export const LoginForm = () => {
const { handleSubmit, control } = useForm<IFormInput>({defaultValues: defaultValues, resolver: yupResolver(schema)});
const { data, error, mutate: loginUser } = useMutation(startLogin);
const onSubmit = ({email, password}: IFormInput) => {
loginUser({email, password});
console.log(data);
console.log(error);
};
}
Now that that's done, as I mentioned before, reason you got the complaint from eslint is because the rules-of-hooks plugin has plain no nonsense rule that assumes any function starting with use* must be a react hook. It also assumes that such a function is only used inside a react component hence the part of the message that says:
React component names must start with an uppercase letter.
The reason this rule exists is explained here. Essentially it comes down to the following:
rules-of-hooks assumes that any custom react hook will make use of one of the existing react hooks (useEffect, useState, useMemo, etc...), and since these hooks affect the state of the component, it is assumed that your custom hook does soo to (even though you may not have written the useQuery hook).
React functional components rely on order of hooks to maintain state. If that order is broken at any point during the render of the component, the component is unable to maintain its internal state.
Therefore, specify all potential mutations at the top of your components.
Your onSubmit() doesn't look like being executed inside a React Component, and you also need to rename your component to a PascalCase.
In most cases, rename your component to PascalCase will fix the issue.

useContext values return undefined in testing

I am new to testing and using Enzyme and Jest to write very simple tests. I just want to check whether the component renders or not. However, (I guess) because my component uses useContext hook, test case automatically returns undefined for all values come from the Context.
In component:
const { count, setCount } = useContext(Context);
Test case:
it('should render', () => {
const component = shallow(<MyComponent />);
const wrapper = component.find('myClassName');
expect(wrapper.length).toBe(1);
});
Test Result: Cannot read property 'count' of undefined. I don't know what I am doing wrong. Is there a simple way that always works with useContext and other hooks to test simple things?
I think the problem here is that when you shallow rendering a component, Context will be ignored. So try mounting your component instead of shallow rendering it like so:
import { mount } from "enzyme"; // mount instead of `shallow` here
...
it('should render', () => {
const component = mount(<MyComponent />); // `mount` here as well
const wrapper = component.find('myClassName');
expect(wrapper.length).toBe(1);
});

Testing onChange event with Jest

According to my code coverage, I need to test the function called in my onChange event. This is actually where I update my functional component's state using the useState hook.
Here is my component :
const Component:React.FC<{}> = () => {
const {value, setState} = useState('');
return(
<View>
<CustomComponent
onChange={(value) => setState(value)}
/>
</View>
)
}
The customComponent is a component derived from the React Input component.
When text is changed inside of it, it calls the onChange function passed as its prop with the text. This is how it comes up to the parent Component which sets the value of the text input in its state as displayed above.
My code coverage returns this analysis :
onChange={//(value) => setState(value)//}
Where the code between the // has to be covered. I don't really understand how I can cover this. First thought was use mock functions, but I can't seem to find how to mock it to the onChange event since I don't pass anything as prop to the main Component.
After a few tests, I finally understood that the coverage wasn't asking for the actual test of the onChange function but actually the value that is evaluated. Therefore, here is what I am doing:
Fetching the TextInput Child component
Changing its Text
Evaluating what it renders
I am using #testing-library/react-native here because it makes selecting tree components easier with the use of accessibilityLabel for example (It actually made me understand the importance of that prop).
Here is what a test looks like:
describe('Testing useState functions', () => {
test('test', () => {
//Rendering the component and its tree
const { container, getByLabelText } = render(<SignupView />);
//Extracting the child, username_input component with his accessibilityLabel
const username_input = getByLabelText('username_input');
const email_input = getByLabelText('email_input');
//Fire a native changeText event with a specific value
fireEvent.changeText(username_input, 'doe');
fireEvent.changeText(email_input, 'doe#joe.com');
//Checking the rendered value
expect(username_input.props.value).toEqual('doe');
expect(email_input.props.value).toEqual('doe#joe.com');
});
});

Accessing useState value from jest test

In a class based component I can easily access state by doing:
const control = shallow(<Test />);
control.state()
For a hooks based component like the one below, how can I access count from my test?
const Test = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Count</button>
<div>{count}</div>
</>
)
}
It is not possible, and it shouldn't be, given that the state is an implementation detail. If you rename the state to myCount, the component still works, but the test would fail.
Alternative 1: Test the DOM
However, since you render the count, you can simply expect that the correct count is rendered.
Example using react-testing-library:
import { render, fireEvent } from 'react-testing-library'
const rendered = render(<Test />)
rendered.getByText(0) // Test that 0 is present in the DOM
fireEvent.click(rendered.getByText('Count'))
rendered.getByText(0) // Test that 1 is present in the DOM
Alternative 2: Extract the state logic in a reducer, and test the reducer
If you really want to test the state updates in isolation, you can useReducer, and test the reducer easily with jest, since it is a pure JavaScript function.

Resources