How do I test a react hook method? - javascript

I am using jest/istanbul to track code coverage. I have the following component:
// Menu.jsx
const myComponent = ({ initialState }) = {
const [state, setState] = useState(initialState);
const [menuContent, setMenuContent] = useState(undefined);
return (
<Menu>
{state.accordions.map(item, index) => (
<MenuPanel id={item.id}>
{item.details && typeof item.details === 'function'
? <item.details setContent={myContent => setMenuContent(myContent)} />
: undefined}
</MenuPanel>
)}
</Menu>
)
}
In my test for Menu.jsx the jest coverage report is complaining that setMenuContent is not covered in my tests. How am I supposed to test a hook like that? I thought it wasn't possible. I tried testing that the setContent prop exists on the subcomponent but that didn't help. Any ideas on how to get this to pass the coverage report? I am using shallow rendering in my test.

You can mock useState for this specific component, try this:
const stateMock = jest.fn();
jest.mock('react', () => {
const ActualReact = require.requireActual('react');
return {
...ActualReact,
useState: () => ['value', stateMock], // what you want to return when useContext get fired goes here
};
});
Everytime your component calls useState your stateMock will be fired and your case will be covered
This is just a minimal example of what you can do, but you can enhance the mock to recognize each state call
If you want to keep the default behaviour of React state you can declare your callback function outside of the component body, in your case the concerned line of code is here:
<item.details setContent={myContent => setMenuContent(myContent)} />
So instead of this anonymous function which can lead to memory leak anyways, you can take it out of your component and do something like so:
const setContent = (setMenuContent) => (myContent) => setMenuContent(myContent)
const myComponent = ({ initialState }) = {
const [state, setState] = useState(initialState);
const [menuContent, setMenuContent] = useState(undefined);
return (
<Menu>
{state.accordions.map(item, index) => (
<MenuPanel id={item.id}>
{item.details && typeof item.details === 'function'
? <item.details setContent={setContent(setMenuContent)} />
: undefined}
</MenuPanel>
)}
</Menu>
)
}
Now you can export this function and cover it with a text, which will allow you to mock setMenuContent
export const setContent = (setMenuContent) => (myContent) => setMenuContent(myContent)
Your last option is to use something like enzyme or react-testing-lib, find this item.details component in the dom and trigger a click action

Related

React - change value of parent variable from child

I know this could be a noob question but I'm learning React for a few months and now I'm stucked with this problem. I got this code over here:
import React, { useCallback, useEffect, useRef, useState } from 'react'
import ReactTags from 'react-tag-autocomplete'
const TagsHandler = ({ tagPlaceholder, suggestions }) => {
const [tags, setTags] = useState([])
const reactTags = useRef()
const onDelete = useCallback(
(tagIndex) => {
setTags(tags.filter((_, i) => i !== tagIndex))
},
[tags]
)
const onAddition = useCallback(
(newTag) => {
setTags([...tags, newTag])
},
[tags]
)
useEffect(() => {
suggestions.map((suggestion) => {
suggestion.disabled = tags.some((tag) => tag.id === suggestion.id)
})
}, [tags])
return (
<ReactTags
ref={reactTags}
tags={tags}
suggestions={suggestions}
onDelete={onDelete}
onAddition={onAddition}
placeholderText={tagPlaceholder}
/>
)
}
export default TagsHandler
Which implements a tag list inside my parent component. This parent component has a bool value which enables a save button. I should enable this button whenever a user adds or removes a tag to the list. My question is: how can I handle this bool from the child component? I've read about Redux but I'd like to avoid using it. I was thinking about a SetState function or a callback but I can't figure out the syntax.
Any help would be really appreciated, thanks :)
You can simply create a function in your parent component: toggleButton, and pass the function to your child component.
function Parent = (props) => {
const [isToggle, setIsToggle] = useState(false);
const toggleButton = () => {
setIsToggle(!isToggle)
}
return <Child toggled={isToggle} toggle={toggleButton} />
}
So the general approach is as follows:
In the Parent.jsx:
const [childrenActive, setChildrenActive] = useState(false)
// later in the render function
<Children setIsActive={(newActive) => setChildrenActive(newActive)} />
In the Children.jsx:
const Children = ({ setIsActive }) => {
return <button onClick={() => setIsActive(true)}>Click me</button>
}
So, as you have guessed, we pass a callback function. I would avoid passing setState directly because it makes your component less flexible.
In the parent component, the function that is responsible for changing that bool variable, you pass as a prop to the child component. At the child component you get that props and you can update if you want.

Why is calling setState twice causing a single state update?

This is probably a beginner React mistake but I want to call "addMessage" twice using "add2Messages", however it only registers once. I'm guessing this has something to do with how hooks work in React, how can I make this work?
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
I'm using React 17.0.2
When a normal form of state update is used, React will batch the multiple setState calls into a single update and trigger one render to improve the performance.
Using a functional state update will solve this:
const addMessage = (message) => {
setMessages(prevMessages => [...prevMessages, message]);
};
const add2Messages = () => {
addMessage('Message1');
addMessage('Message2');
};
More about functional state update:
Functional state update is an alternative way to update the state. This works by passing a callback function that returns the updated state to setState.
React will call this callback function with the previous state.
A functional state update when you just want to increment the previous state by 1 looks like this:
setState((previousState) => previousState + 1)
The advantages are:
You get access to the previous state as a parameter. So when the new state depends on the previous state, the parameter is helpful as it solves the problem of stale state (something that you can encounter when you use normal state update to determine the next state as the state is updated asynchronously)
State updates will not get skipped.
Better memoization of handlers when using useCallback as the dependencies can be empty most of the time:
const addMessage = useCallback((message) => {
setMessages(prevMessages => [...prevMessages, message]);
}, []);
import React from "react";
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages => [...messages, message]);
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
This is because messages still refers to the original array. It will get the new array at the next re-render, which will occur after the execution of add2Messages.
Here are 2 solutions to solve your problem :
Use a function when calling setMessages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(prevMessages => prevMessages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
Modify addMessage to handle multiple messages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (...messagesToAdd) => {
setMessages(prevMessages => prevMessages.concat(messagesToAdd));
// setMessages(messages.concat(messagesToAdd)); should also work
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => addMessage("Message1", "Message2")}>
Add 2 messages
</button>
</div>
);
}
Changing addMessage function as below will make your code work as expected
const addMessage = (message) => {
setMessages(messages => messages.concat(message));
};
Your code didn't work because in case of synchronous event handlers(add2Messages) react will do only one batch update of state instead of updating state after every setState calls. Which is why when second addMessage was called here, the messages state variable will have [] only.
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage('Message1'); // -> [].concat("Message1") = Message1
addMessage('Message2'); // -> [].concat("Message2") = Message2
};
So if you want to alter the state value based on previous state value(especially before re-rendering), you can make use of functional updates.

Too many re-renders - while trying to put props(if exists) in state

I am transfer an props from father component to child component.
On the child component I want to check if the father component is deliver the props,
If he does, i"m putting it on the state, If not I ignore it.
if(Object.keys(instituteObject).length > 0)
{
setInnerInstitute(instituteObject)
}
For some reason the setInnerInstitute() take me to infinite loop.
I don't know why is that happening and how to fix it.
getInstitutesById() - Is the api call to fetch the objects.
Father component(EditInstitute):
const EditInstitute = props => {
const {id} = props.match.params;
const [institute, setInstitute] = useState({})
useEffect(() => { //act like componentDidMount
getInstitutesById({id}).then((response) => {
setInstitute(response)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<React.Fragment>
<InstituteForm instituteObject={institute.object}/>
</React.Fragment>
)
}
Child component(InstituteForm):
const InstituteForm = (props) => {
const {instituteObject = {}} = props // if not exist default value = {}
const [innerInstitute, setInnerInstitute] = useState({})
if (Object.keys(instituteObject).length > 0) // if exists update the state.
{
setInnerInstitute(instituteObject)
}
return (
<React.Fragment>
not yet.
</React.Fragment>
)
}
Thanks
I think the way you are changing your InstituteForm's state causing this error. You can try using the useEffect hook to change your innerInstitute based on instituteObject. That's why you need to also add instituteObject in the dependency array of that useEffect hook.
import { useEffect, useState } from "react"
const InstituteForm = (props) => {
const {instituteObject = {}} = props // if not exist default value = {}
const [innerInstitute, setInnerInstitute] = useState({})
useEffect(() => {
// this is be evoked only when instituteObject changes
if (Object.keys(instituteObject).length > 0){
setInnerInstitute(instituteObject)
}
}, [instituteObject])
return (
<React.Fragment>
not yet.
</React.Fragment>
)
}

Browser freezing when using React Hook

When I enter into a router that refers to this Component, my browser just crashes. I've made some console tests and notices that when the response.data.message is up, it continually re-renders the page. Can someone help me?
import React from 'react'
import "./UsernameStory.css";
import Axios from "axios";
const UsernameStory = ({match}) => {
const [statue , setStatue] = React.useState("");
const [stories , setStories] = React.useState([]);
const fetchUsername = () => {
const urls = match.params.username;
Axios.post("http://localhost:8080/"+urls, {
}).then((response) => {
if(response.data.statue)
{
setStatue(response.data.statue);
}
if(response.data.message){
setStories(response.data.message);
}
})
}
return (
<div>
{fetchUsername()}
<p>{statue}</p>
<ul>
{stories.map((story , key) => (<li key={key}>{story.username}</li>))}
</ul>
</div>
)
}
export default UsernameStory
On every render, fetchUsername() is called and results in updating statue and stories, which results in another rerender, and thus leads to an infinite loop of rerendering (since every render triggers a state update).
A better practice for handling functions with side-effects like fetching data is to put the fetchUsername in useEffect.
const UsernameStory = ({match}) => {
const [statue , setStatue] = React.useState("");
const [stories , setStories] = React.useState([]);
useEffect(() => {
const urls = match.params.username;
Axios.post("http://localhost:8080/"+urls, {})
.then((response) => {
if(response.data.statue)
{
setStatue(response.data.statue);
}
if(response.data.message){
setStories(response.data.message);
}
});
}, []); // An empty denpendency array allows you to fetch the data once on mount
return (
<div>
<p>{statue}</p>
<ul>
{stories.map((story , key) => (<li key={key}>{story.username}</li>))}
</ul>
</div>
)
}
export default UsernameStory
You can't call function fetchUsername() inside return statement. It
will go in infinite loop.
You can't directly set the state of two variable as setStatue(response.data.statue) & setStories(response.data.message) respectively
use the useEffect hooks and set the status inside that hooks with conditional rendering to avoid looping.
call the fetchUsername() just before the return statement.
I looked at your code carefully. As you can see in this code, the fetchUsername() function is executed when the component is first rendered.
When you call setState() in the fetchUsername function, your component's state variable is updated. The problem is that when the state variable is updated, the component is rendered again. Then the fetchUsername function will be called again, right?
Try the useEffect Hook.
The example code is attached.
eg code
How about trying to change the way you call the fetchUsername() function with the useEffect hook instead?
import React, { useEffect } from 'react'
import "./UsernameStory.css";
import Axios from "axios";
const UsernameStory = ({match}) => {
const [statue , setStatue] = React.useState("");
const [stories , setStories] = React.useState([]);
useEffect(() => {
const fetchUsername = () => {
const urls = match.params.username;
Axios.post("http://localhost:8080/"+urls, {
}).then((response) => {
if(response.data.statue)
{
setStatue(response.data.statue);
}
if(response.data.message){
setStories(response.data.message);
}
})
}
fetchUsername();
// clean up here
return () => {
// something you wanna do when this component being unmounted
// console.log('unmounted')
}
}, [])
return (
<div>
<p>{statue}</p>
<ul>
{stories.map((story , key) => (<li key={key}>{story.username}</li>))}
</ul>
</div>
)
}
export default UsernameStory
But, sometimes your code just doesn't need a cleanup in some scenarios, you could read more about this here: Using the Effect Hook - React Documentation.

Testing method inside functional component in React using Jest / Enzyme

Following is my React Functional Component which I am trying to test using jest / enzyme.
React Functional Component Code -
export const UserForm = props => {
const {labels, formFields, errorMessages} = props;
const [showModal, setShowModal] = React.useState(false);
const [newId, setNewId] = React.useState('');
const showModal = () => {
setShowModal(true);
}
const closeModal = () => {
setShowModal(false);
};
const handleSubmit = data => {
Post(url, data)
.then(resp => {
const userData = resp.data;
setNewId(() => userData.id);
showModal();
})
}
return (
<div className="user-form">
<UserForm
fields={formFields}
handleSubmit={handleSubmit}
labels={labels}
errors={errorMessages}
/>
{showModal && <Modal closeModal={closeModal}>
<div className="">
<h3>Your new id is - {newId}</h3>
<Button
type="button"
buttonLabel="Close"
handleClick={closeModal}
classes="btn btn-close"
/>
</div>
</Modal>}
</div>
)
};
Now I am trying to test showModal, closeModal and handleSubmit method, but my tests are failing. Let me know the correct way of testing React Hooks and methods inside functional component.
My test case -
import React from 'react';
import { UserForm } from '../index';
import { shallow } from 'enzyme';
describe('<UserForm />', () => {
let wrapper;
const labels = {
success: 'Success Message'
};
const formFields = [];
const errorMessages = {
labels: {
firstName: 'First Name Missing'
}
};
function renderShallow() {
wrapper = shallow(<UserForm
labels={labels}
formFields={formFields}
errorMessages={errorMessages}
/>);
}
it('should render with props(snapshot)', () => {
renderShallow();
expect(wrapper).toMatchSnapshot();
});
it('should test showModal method', () => {
const mockSetShowModal = jest.fn();
React.useState = jest.fn(() => [false, mockSetShowModal]);
renderShallow();
expect(mockSetShowModal).toHaveBeenCalledWith(true);
});
});
Error I am getting -
Expected mock function to have been called with:
[true]
But it was not called.
Let me know how can i test the showModal, closeModal and handleSubmit methods in a functional component.
Generally, functional components in React aren't meant to be tested in that way. The React team are suggesting that you use the approach of React Testing Library which is more focused on the actual user interface scenarios. Instead of testing React component instances, you're testing DOM nodes.
This is the main reason why people are moving away from Enzyme and starting to use RTL, because you want to avoid testing implementation details as much as you can.

Categories

Resources