FormContext from react-hook-forms causing error in testing-library-react - react-testing-library

Got an odd error with FormContext when trying to run my tests. All I'm trying to do is render a component.
So this is the error that I am getting and this is the test that I have written.
import React from 'react';
import UserReportQuestion from './UserReportQuestion';
import { render } from '#testing-library/react';
import { useForm, FormContext } from 'react-hook-form';
describe('(Component) UserReport', () => {
let UserReportQuestionRender;
// jest.mock('react-hook-form', () => ({
// FormContext: jest.fn(),
// useForm: jest.fn(),
// }));
beforeEach(() => {
const form = useForm();
UserReportQuestionRender = render(
<FormContext {...form}>
<UserReportQuestion />
</FormContext>
)
});
it('Should render without crashing', () => {
expect(UserReportQuestionRender);
});
});
In the component I am testing I am using FormContext and passing it useForm as its methods. I've commented everything else out so it is just the FormContext in the component to make sure that it is 100% this that is causing the error.
Wondering if anyone has any ideas on how to work round this?
Update with component
import { useForm, FormContext } from "react-hook-form";
const UserReportQuestion = ({ text }) => {
const { t } = useTranslation();
const [isModalOpen, setModal] = useState(false);
const methods = useForm();
return (
<>
<Question>
{t('UserReport.criteria')}:
{' '}
{/* Placeholder text */}
{/* {text} */}
Find new, creative ways of completing tasks
<ModalIcon onClick={() => setModal(true)}>
<Icon
icon="info"
color="#a3a3a3"
modifiers={['size-large', 'solid']}
/>
</ModalIcon>
</Question>
<InfoModal
isModalOpen={isModalOpen}
closeModal={() => setModal(false)}
title={t('UserReport.modalTitle')}
>
<FormContext {...methods}>
{/* <form onSubmit={methods.handleSubmit()}>
<InfoModalParagraph>
{t('UserReport.modalDescription')}
</InfoModalParagraph>
<FeedbackQuestions
questions={mockData}
selfAssessment={false}
submitButtonDisabled
noSubmitButton
/>
</form> */}
</FormContext>
</InfoModal>
</>
);
}

I think you're importing a component which doesn't exist, in the react-hook-form documentation we have a FormProvider if we want to use useFormContext.
read More about it here: https://react-hook-form.com/api/#useFormContext
but in the mean time you need to change
import {FormContext} from 'react-hook-form'
to this:
import { FormProvider } from 'react-hook-form'

Related

React Checkboxes Filtering

I'm struggling to create Checkbox filtering in React. I want my products to be filtered by brand,I have created checkboxes dynamically from database. So each checkbox corresponds to the brand. The quantity of checkboxes equals to quantity of brands.
The problem is when I click on one of the checkboxes, the other products are disappearing from the screen, and this is what I want, but at the same time, the other checkboxes is disappearing as well, and I see only the clicked one.
Also, when I click in that checkbox again I want the products of the brand to be back on the screen.
Any ideas how to do it?
Category.js component:
/** #format */
import { useState, useEffect, createContext } from "react";
import { Link, useParams } from "react-router-dom";
import Header from "../components/Header";
import React from "react";
// Importing styles
import "./styles/Category.css";
import Footer from "../components/Footer";
import Filter from "./Filter";
export const CatgContext = createContext();
export const Category = ({ onAdd }) => {
const [products, setProducts] = useState([]);
const params = useParams();
const getProducts = async () => {
try {
const res = await fetch(`/api/products/${params.type}`);
const data = await res.json();
setProducts(data);
} catch (err) {
console.log(err);
}
};
useEffect(() => {
getProducts();
}, []);
return (
<>
<Header />
<h1>{params.type}</h1>
<aside className="catalogue-aside">
<CatgContext.Provider value={{ setProducts, products }}>
<Filter products={products} />
</CatgContext.Provider>
</aside>
<div className="category-wrapper">
{products.map((product) => {
return (
<div
className="product-card"
key={product.product_id}
id={product.product_id}
>
<Link to={`/product/${product.product_id}`}>
<img className="img-card" src={product.product_image} />
<h3 className="title-card">{product.product_type}</h3>
</Link>
<p>{product.product_brand}</p>
<p>{product.product_name}</p>
<p>{product.product_description}</p>
<p>${product.product_price}</p>
<button onClick={() => onAdd(product)}>Add to Cart</button>
</div>
);
})}
</div>
<Footer />
</>
);
};
Filter.js component:
import { useState, useContext, useEffect } from "react";
import { CatgContext } from "./Category";
const Filter = (props) => {
const { products, setProducts } = useContext(CatgContext);
const handleChange = (e, value) => {
const filteredByBrand = products.filter((product) => {
return product.product_brand === value;
});
setProducts(filteredByBrand);
};
const renderCheckboxLists = () =>
products.map((product) => (
<div className="checkbox-wrapper" key={product.product_id}>
<input
onChange={(e) => handleChange(e, product.product_brand)}
type="checkbox"
/>
<span>{product.product_brand}</span>
</div>
));
return <>{renderCheckboxLists()}</>;
};
export default Filter;
Screenshot 1
Screenshot 2

Register is not a function when passing as a prop?

I use react-hook-form for the first time. I was reading the docs and followed along. Likewise, I already laid out my components and styled them. Now I am trying to alert the data out after the form submits.
This is the ContactForm
import React, { useState } from 'react';
import * as S from './style';
import { PrimaryButton } from '#element/Button';
import TextInput from '#element/TextInput';
import { useForm } from 'react-hook-form';
export const ContactForm = () => {
const { register, handleSubmit } = useForm();
const [firstName, setFirstName] = useState('');
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
return (
<S.ContactFormWrapper onSubmit={handleSubmit(onSubmit)}>
<TextInput
name={'firstName'}
label={'First Name'}
state={firstName}
setState={setFirstName}
placeholder={'John'}
type={'text'}
width={'48%'}
options={{
maxLength: '20',
minLength: '2',
required: true,
}}
register={register}
/>
<PrimaryButton type={'submit'} text={'Send Message'} />
</S.ContactFormWrapper onSubmit={handleSubmit(onSubmit)}>
)
}
This is my Custom created TextInput
import React, { useEffect, useState } from 'react';
import * as S from './style';
const TextInput = ({
name,
label,
placeholder,
state,
setState,
type,
width,
register,
options,
}) => {
const [isActive, setIsActive] = useState(false);
return (
<S.TextInputWrapper inputWidth={width}>
<S.Label htmlFor={name} isActive={isActive}>
{label}
</S.Label>
<S.Input
placeholder={placeholder}
type={type}
name={name}
id={name}
{...register(name, options)}
onChange={(event) => setState(event.target.value)}
onFocus={() => setIsActive(true)}
onBlur={() => setIsActive(false)}
/>
</S.TextInputWrapper>
);
};
export default TextInput;
Error Message
TypeError: register is not a function {...register(name, options)}
I was searching on StackOverflow there was a Post, but the Answer was confusing for me and the Questioner Code was much different than mine. Because I think the error occurred because I use styled-components, and it is nested deep. I am confused because I was reading the docs and followed along.
If I spread the Error says, register is not a function else if I not spread it then the error is ... spread is required.
Hopefully you can bring light to my confusion.
Kind regards
Kuku
The simplest solution is to take advantage of react hook form's context and use the useFormContext hook.
Input Component
import { useFormContext } from "react-hook-form";
const TextInput = ({ name, options }) => {
const { register } = useFormContext();
return (
<S.Input
name={name}
{...register(name, options)}
/>
</S.TextInputWrapper>
);
};
Remove the input register function from the parent form
export const ContactForm = () => {
...other functions
return <TextInput name={'firstName'} options={{maxLength: '20' }} />;
}
An even simpler solution is to let react-hook-form control the form values and use the useController hook or Controller component.
import { useController } from "react-hook-form";
const TextInput = ({ name, options }) => {
const { field } = useController({ name, rules: options });
return <S.Input name={name} {...field} />
};
You can also get the input states using the useContoller hook to reduce the number of events your using.
import { useController } from "react-hook-form";
const TextInput = ({ name, options }) => {
const {
field,
fieldState: { error, invalid, isDirty, isTouched }
} = useController({ name, rules: options });
};
useFormContext is a good solution explained by #Sean W
Here is another solution without useFormContext, you can use register as usual instead of passing it as a prop. You just have to forward the ref of your TextInput.
👉🏻 You can find an instance here: https://stackoverflow.com/a/68667226/4973076

waitForEvent is not finding the DOM change after fireEvent is called

I have the following component that I'm trying to test with react-testing-library:
const PasswordIconButton = ({
stateString
}) => {
const { state, dispatch } = useContext(Store);
const showPassword = getObjectValue(stateString, state);
const toggleShowPassword = event => {
event.preventDefault();
dispatch(toggleBoolean(stateString, !showPassword));
};
return (
<Layout
showPassword={showPassword}
toggleShowPassword={toggleShowPassword}
/>
);
};
export default PasswordIconButton;
const Layout = ({
showPassword,
toggleShowPassword
}) => {
return (
<IconButton onClick={toggleShowPassword} data-testid="iconButton">
{showPassword ? (
<HidePasswordIcon data-testid="hidePasswordIcon" />
) : (
<ShowPasswordIcon data-testid="showPasswordIcon" />
)}
</IconButton>
);
};
This works exactly as intended in production. If the user clicks the button then it calls toggleShowPassword() which toggles the value of boolean const showPassword.
If showPassword is equal to false and the user clicks the button, I can see that the <ShowPasswordIcon /> is removed and <HidePasswordIcon /> appears. Both have the correct data-testid attributes set.
I'm attempting to test the component will the following test:
import React from "react";
import {
render,
cleanup,
fireEvent,
waitForElement
} from "react-testing-library";
import PasswordIconButton from "./PasswordIconButton";
afterEach(cleanup);
const mockProps = {
stateString: "signUpForm.fields.password.showPassword"
};
describe("<PasswordIconButtonIcon />", () => {
it("renders as snapshot", () => {
const { asFragment } = render(<PasswordIconButton {...mockProps} />);
expect(asFragment()).toMatchSnapshot();
});
//
// ISSUE IS WITH THIS TEST:
// ::::::::::::::::::::::::::
it("shows 'hide password' icon on first click", async () => {
const { container, getByTestId } = render(
<PasswordIconButton {...mockProps} />
);
const icon = getByTestId("iconButton");
fireEvent.click(icon);
const hidePasswordIconTestId = await waitForElement(
() => getByTestId("hidePasswordIcon"),
{ container }
);
expect(hidePasswordIconTestId).not.toBeNull();
});
});
The shows 'hide password' icon on first click test always fails and I'm not sure why. The mockProps are definitely correct and work perfectly in production.
What am I missing here?
I figured it out... The issue is that I needed to wrap the component in the context provider as const { state, dispatch } = useContext(Store); won't work properly without it.
So I changed the render to:
const { container, getByTestId } = render(
<StateProvider>
<PasswordIconButton {...mockProps} />
</StateProvider>
);`
And now the test passes fine.

Redux-Form unable to redirect after onSubmit Success

I would like to redirect to another page after a successful submit in redux-form.
I have tried the follow but the redirect either fails or doesn't do anything
REACT-ROUTER-DOM:
This results is an error 'TypeError: Cannot read property 'push' of undefined'
import { withRouter } from "react-router-dom";
FacilityComplianceEditForm = withRouter(connect(mapStateToProps (FacilityComplianceEditForm));
export default reduxForm({
form: "FacilityComplianceEditForm",
enableReinitialize: true,
keepDirtyOnReinitialize: true,
onSubmitSuccess: (result, dispatch, props) => {
props.history.push('/facilities') }
})(FacilityComplianceEditForm);
REACT-ROUTER-REDUX:
This submits successfully, the data is saved to the DB, but the page does not redirect.
import { push } from "react-router-redux";
export default reduxForm({
form: "FacilityComplianceEditForm",
enableReinitialize: true,
keepDirtyOnReinitialize: true,
onSubmitSuccess: (result, dispatch, props) => { dispatch(push('/facilities')) }
})(FacilityComplianceEditForm);
I also tried onSubmitSuccess: (result, dispatch, props) => dispatch(push('/facilities')) without the {} around dispatch statement but it didn't work
APP.JS to show the path does exist
class App extends Component {
render() {
return (
<div>
<Header />
<div className="container-fluid">
<Switch>
<Route exact path="/facilities" render={() => <FacilitySearch {...this.props} />} />
<Route exact path="/facilities/:id" render={(props) => <FacilityInfo id={props.match.params.id} {...this.props} />} />
<Route exact path="/facilities/compliance/:id" render={(props) => <FacilityComplianceEditForm id={props.match.params.id}{...this.props} />
<Redirect from="/" exact to="/facilities" />
<Redirect to="/not-found" />
</Switch>
</div>
<Footer />
</div>
);
}
}
export default App;
REDUCER:
export const complianceByIdReducer = (state = INTIAL_STATE.compId, action) => {
switch (action.type) {
console.log(state, action)
case "CREATE_NEW_COMPLIANCE":
return {
...state,
compId: action.compCreate
}
default:
return state
}
}
ACTION:
export const createCompliance = (id, compObj) => {
return dispatch => {
axios.post("/api/facilities/compliance/" + id, compObj)
.then(res => { return res.data })
.then(compCreate => {
dispatch(createComplianceSuccess(compCreate));
alert("New compliance created successfully") //this does get triggered
})
}
}
const createComplianceSuccess = compCreate => {
return {
type: "CREATE_NEW_COMPLIANCE",
compCreate: compCreate
}
}
REDIRECT OBJECT RETURNED FROM SUBMIT SUCCESS
STORE
import * as redux from "redux";
import thunk from "redux-thunk";
import {
facilityListReducer,
facilityReducer,
facilityLocationReducer,
facilityHistoricalNameReducer,
facilityComplianceReducer,
complianceByIdReducer
} from "../reducers/FacilityReducers";
import { projectFormsReducer } from "../reducers/FormsReducers";
import { errorReducer } from "../reducers/ErrorReducer";
import { reducer as formReducer } from "redux-form";
export const init = () => {
const reducer = redux.combineReducers({
facilityList: facilityListReducer,
facility: facilityReducer,
facilityLocation: facilityLocationReducer,
historicalNames: facilityHistoricalNameReducer,
facilityCompliance: facilityComplianceReducer,
compId: complianceByIdReducer,
countyList: projectFormsReducer,
errors: errorReducer,
form: formReducer
});
const store = redux.createStore(reducer, redux.applyMiddleware(thunk));
return store;
};
react-router-redux is deprecated so I did not want to add it to my project. I followed this post and made some modification to how routing was set up and it now works.

React DnD - "Cannot have two HTML5 backends at the same time."

I am trying to make a POC with Rails5, action Cable, React and Rails and React DnD.
The purpose is to make an app like trello but for an recruitment process.
My front is in ReactJS.
I have 3 components, first, the container call "Candidates", this component call 2 "CardBoard" components that call "Card" component.
I user react DnD library for draggable card and droppable CardBoard. when i drop card on cardboard, i use a post call and a websocket(action cable from rails5) for update my state. I don't understand why i have this message after the post call :
Uncaught Error: Cannot have two HTML5 backends at the same time.
at HTML5Backend.setup (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4175), <anonymous>:87:15)
at DragDropManager.handleRefCountChange (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:3566), <anonymous>:52:22)
at Object.dispatch (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4931), <anonymous>:186:19)
at HandlerRegistry.addSource (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:3594), <anonymous>:104:18)
at registerSource (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4294), <anonymous>:9:27)
at DragDropContainer.receiveType (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:146:32)
at DragDropContainer.receiveProps (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:135:14)
at new DragDropContainer (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:102:13)
at eval (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4399), <anonymous>:295:18)
at measureLifeCyclePerf (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4399), <anonymous>:75:12)
Candidate.jsx =
import React, { PropTypes } from 'react';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import CardBoard from './CardBoard.jsx';
export default class Candidates extends React.Component {
constructor(props, _railsContext) {
super(props);
this.state = {
candidates: this.props.candidates
}
this.filterByStatus = this.filterByStatus.bind(this)
}
componentDidMount() {
this.setupSubscription();
}
setupSubscription() {
App.candidate = App.cable.subscriptions.create("CandidateChannel", {
connected: () => {
console.log("User connected !")
},
received: (data) => {
this.setState({ candidates: data.candidates })
},
});
}
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
<div className="recruitment">
{
["À Rencontrer", "Entretien"].map((status, index) => {
return (
<CardBoard candidates={(this.filterByStatus({status}))} status={status} key={index} />
);
})
}
</div>
</DragDropContextProvider>
);
}
}
CardBoard.jsx =
import React, { PropTypes } from 'react';
import Card from './Card.jsx';
import { DropTarget } from 'react-dnd';
import ItemTypes from './ItemTypes';
const cardTarget = {
drop(props: Props) {
var status = ''
if(props.status == "À Rencontrer") {
status = 'to_book'
} else {
status = 'interview'
}
return { status: status };
},
};
#DropTarget(ItemTypes.CARD, cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}))
export default class CardBoard extends React.Component<Props> {
constructor(props, _railsContext) {
super(props);
}
render() {
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
return connectDropTarget(
<div className={`${this.props.status} ui cards`}>
<h2>{`${this.props.status} (${this.props.candidates.length})`}</h2>
{
(this.props.candidates).map((candidate, index) => {
return <Card candidate={candidate} key={index} />
})
}
{ isActive?
'Release to drop' : 'drag a card here'
}
</div>
);
}
}
Card.jsx=
import React, { PropTypes } from 'react';
import { DragSource } from 'react-dnd';
import ItemTypes from './ItemTypes';
const cardSource = {
beginDrag(props) {
return {
candidate_id: props.candidate.id,
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if (dropResult) {
console.log(`You dropped ${item.candidate_id} vers ${dropResult.status} !`);
$.post(`/update_status/${item.candidate_id}/${dropResult.status}`);
}
},
};
#DragSource(ItemTypes.CARD, cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class Card extends React.Component {
constructor(props, _railsContext) {
super(props);
}
render() {
const { isDragging, connectDragSource } = this.props;
const { name } = this.props;
const opacity = isDragging ? 0 : 1;
var candidate = this.props.candidate;
return (
connectDragSource(
<div className="card" key={candidate.id} style={{opacity}}>
<div className="content">
<div className="header">{`${candidate.first_name} ${candidate.last_name}`}</div>
<div className="description">
{candidate.job_title}
</div>
<span className="right floated">
<i className="heart outline like icon"></i>
{candidate.average_rate}
</span>
</div>
</div>
)
);
}
}
for better comprehension, here a gif of my feature and his bug :
feature with bug
here my github repo with all code
Just try to make the context of HTML5Backend a singleton. You can check the codes at:
https://github.com/react-dnd/react-dnd/issues/186
https://github.com/react-dnd/react-dnd/issues/708
I faced with similar issue and found workaround by moving
<DragDropContextProvider></DragDropContextProvider>
to the topmost react component.
Structure before:
App
Component1
Component2
...
ComponentX with render(<DragDropContextProvider>...<DragDropContextProvider>)
Structure after:
App with render(<DragDropContextProvider>...<DragDropContextProvider>)
Component1
Component2
...
ComponentX
Supposing App is loaded only once and HTML5 backend is not replicated.
You can either create a new file and import DragDropContext where you need it:
import HTML5 from 'react-dnd-html5-backend';
import {DragDropContext} from 'react-dnd';
export default DragDropContext(HTML5);
Source: https://github.com/react-dnd/react-dnd/issues/708#issuecomment-309259695
Or using Hooks:
import { DndProvider, createDndContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import React, { useRef } from "react";
const RNDContext = createDndContext(HTML5Backend);
function useDNDProviderElement(props) {
const manager = useRef(RNDContext);
if (!props.children) return null;
return <DndProvider manager={manager.current.dragDropManager}>{props.children}</DndProvider>;
}
export default function DragAndDrop(props) {
const DNDElement = useDNDProviderElement(props);
return <React.Fragment>{DNDElement}</React.Fragment>;
}
Use:
import DragAndDrop from "../some/path/DragAndDrop";
export default function MyComp(props){
return <DragAndDrop>....<DragAndDrop/>
}
Source: https://github.com/react-dnd/react-dnd/issues/186#issuecomment-561631584
If you use BrowserRouter then move the DragDropContextProvider out of BrowserRouter:
Source: https://github.com/react-dnd/react-dnd/issues/186#issuecomment-453990887
DndProvider has a options prop in where you can set rootElement which bounds DnD to that specified context, and unfortunately it isn't documented well. This approach solved all my issues, as I had other component which was using DnD and they were out of my boundary and I wasn't able to make HTML5Backend Singleton.
I tried this approach with "react-dnd": "^14.0.2"
const myFirstId = 'first-DnD-Containier';
const mySecondId = 'second-DnD-Containier';
export const DndWrapper = React.memo((props) => {
const [context, setContext] = useState(null);
useEffect(() => {
setContext(document.getElementById(props.id))
},[props.id])
return context ? (
<DndProvider backend={HTML5Backend} options={{ rootElement: context}}>
{props.children}
</DndProvider>
) : null;
});
export default function App() {
return (
<div>
<div id={myFirstId}>
<DndWrapper id={myFirstId}>
<MyOtherComponents />
</DndWrapper>
</div>
<div id={mySecondId}>
<DndWrapper id={mySecondId}>
<MyOtherComponents />
</DndWrapper>
</div>
</div>
);
}
P.S. Make sure by the time you call document.getElementById the DOM exist with ids.
Move the DragDropContextProvider out of BrowserRouter.
import { BrowserRouter as Router } from 'react-router-dom';
import { DndProvider } from 'react-dnd';
<DndProvider>
<Router>
<App />
</Router>
</DndProvider>