I dont know how to hunt down my problem - React - reactjs

Im very new to React and im having an issue Im not sure how to troubleshoot. So im setting an array on the context when a http request fails in a custom hook
Here is my hook:
const useHttp = (requestObj: any, setData: Function) =>
{
const [isLoading, setIsLoading] = useState(false);
const ctx = useContext(GlobalContext);
const sendRequest = useCallback(() =>
{
setIsLoading(true);
fetch(requestObj.url, {
method: requestObj.method ? requestObj.method: 'GET',
headers: requestObj.headers ? requestObj.headers : {},
body: requestObj.body ? JSON.stringify(requestObj.body) : null
})
.then(res => res.json())
.then(data => {
setIsLoading(false);
setData(data);
})
.catch(err =>
{
setIsLoading(false);
ctx.setErrors([
(prevErrors: string[]) =>
{
//prevErrors.push(err.message)
let newArray = prevErrors.map((error) => {return error});
newArray.push(err.message);
return newArray;
}]
);
console.log('There was an error');
});
}, []);
return {
isLoading: isLoading,
sendRequest: sendRequest
}
}
Im using .map cos the spread operator for arrays isnt working. Im looking into it but its not important for this.
When there are errors I create a modal and then render it in my jsx. My problem is that for some reason my Modal is rendering twice. The second time it has no props and that blows up my program. I dont know why its rendering again and I dont know how to attack the problem. The stack has nothing with regards to what is causing it (that I can see). If a component is rendering again would the props not be the same as originally used? I have breakpoints in the spot where the modal is called and they arent getting hit again. So can anyone offer some advice for how I go about debugging this?
const App: FC = () => {
const [errors, setErrors] = useState([]);
let modal = null
if(errors.length > 0)
{
modal = (
<Modal
heading="Warning"
content={<div>{errors}</div>}
buttonList={
[
{label: "OK", clickHandler: ()=> {}, closesModal: true},
{label: "Cancel", clickHandler: ()=> {alert("cancelled")}, closesModal: false}
]
}
isOpen={true}/>
)
}
return (
<GlobalContext.Provider value={{errors: errors, setErrors: setErrors}}>
<ProviderV3 theme={defaultTheme}>
<Toolbar></Toolbar>
<Grid
margin='25px'
columns='50% 50%'
gap='10px'
maxWidth='100vw'>
<OwnerSearch />
<NewOwnerSearch />
</Grid>
</ProviderV3>
{modal}
</GlobalContext.Provider>
);
};
import { FC, useState } from 'react';
import {
ButtonGroup, Button, DialogContainer,
Dialog, Content, Heading, Divider
} from '#adobe/react-spectrum';
type Props = {
heading: string,
content : any,
buttonList: {label: string, clickHandler: Function, closesModal: boolean}[],
isOpen: boolean
}
const Modal: FC<Props> = ( props ) =>
{
const [open, setOpen] = useState(props.isOpen);
let buttons = props.buttonList.map((button, index) =>
{
return <Button key={index} variant="cta" onPress={() => close(button.clickHandler, button.closesModal)} autoFocus>
{button.label}
</Button>
});
const close = (clickHandler: Function | null, closesModal: boolean) =>
{
if(clickHandler != null)
{
clickHandler()
}
if(closesModal)
{
setOpen(false)
}
}
return (
<DialogContainer onDismiss={() => close(null, true)} >
{open &&
<Dialog>
<Heading>{props.heading}</Heading>
<Divider />
<Content>{props.content}</Content>
<ButtonGroup>
{buttons}
</ButtonGroup>
</Dialog>
}
</DialogContainer>
);
}
export default Modal;
Following Firefighters suggestion I get an error now:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at resolveDispatcher (react.development.js:1476)
at useContext (react.development.js:1484)
at useProvider (module.js:239)
at $bc3300334f45fd1ec62a173e70ad86$var$Provider (module.js:95)
at describeNativeComponentFrame (react-dom.development.js:946)
at describeFunctionComponentFrame (react-dom.development.js:1034)
at describeFiber (react-dom.development.js:1119)
at getStackByFiberInDevAndProd (react-dom.development.js:1138)
at createCapturedValue (react-dom.development.js:20023)
at throwException (react-dom.development.js:20351)

Try putting the open state inside the App component and remove it from the Modal component:
const [errors, setErrors] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
if(errors.length > 0) setIsModalOpen(true);
}, [errors])
<Modal
...
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
/>

Related

How to ensure useState works when mocking custom react hook

I have a component which imports a custom hook. I want to mock returned values of this hook but ensure the useState still works when I fire and event.
component.tsx
export const Component = () => {
const { expanded, text, handleClick, listOfCards } = useComponent();
return (
<div>
<button id="component" aria-controls="content" aria-expanded={expanded}>
{text}
</button>
{expanded && (
<div role="region" aria-labelledby="component" id="content">
{listOfCards.map((card) => (
<p>{card.name}</p>
))}
</div>
)}
</div>
);
};
useComponent.tsx
const useComponent = () => {
const [expanded, setExpanded] = useState(false);
const { listOfCards } = useAnotherCustomHook();
const { translate } = useTranslationTool();
return {
text: translate("id123"),
expanded,
handleClick: () => setExpanded(!expanded),
listOfCards,
};
};
component.test.tsx
jest.mock("./component.hook");
const mockuseComponent = useComponent as jest.Mock<any>;
test("Checks correct attributes are used, and onClick is called when button is clicked", () => {
mockuseComponent.mockImplementation(() => ({
text: "Click to expand",
listOfCards: [{ name: "name1" }, { name: "name2" }],
}));
render(<Component />);
const button = screen.getByRole("button", { name: "Click to expand" });
expect(button).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(button);
expect(button).toHaveAttribute('aria-expanded', 'true');
});
With the above test aria-expanded doesnt get set to true after we fire the event because im mocking the whole hook. So my question is, is there a way to only mock part of the hook and keep the useState functionality?

React TypeError is not a function with Onboarding implementation

I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.

Testing-library: removing element with waitForElementToBeRemoved doesn't get jest coverage

Here's a Toast component that is displayed when a button is clicked and disappears after x seconds.
For testing waitFor is used to change showToast state to true when API call is successful, then waitForElementToBeRemoved is used to check if the toast component has been removed from the screen.
Tests are passing, so the assumption is that showToast became false. But when I check the jest coverage, that line hide={() => setShowToast(false)} is still shown as uncovered.
What would be needed to cover that line using testing-library?
Toast component:
const Toast = props => {
const { message, color, iconName, show, hide, background, timeoutDuration, ...rest } = props;
useEffect(() => {
if (show) {
const timeout = setTimeout(() => {
hide();
}, timeoutDuration * 1000 + 1000);
return () => clearTimeout(timeout);
}
}, [show, timeoutDuration]);
return (
<Box>
<Container {...rest} show={show} timeoutDuration={timeoutDuration}>
<StyledToast py={1} px={2} background={background} bgColor={colors[color]} role="alert">
<StyledIcon name={iconName} color={color} />
<StyledP color={color} fontSize={[14, 16]}>
{message}
</StyledP>
</StyledToast>
</Container>
</Box>
);
};
Component that uses Toast:
const [showToast, setShowToast] = useState(false);
{showToast && (
<Toast
message="Store Settings successfully updated!"
color="green"
iconName="check-circle"
background={true}
show={showToast}
timeoutDuration={10}
zIndex={10}
hide={() => setShowToast(false)}
/>
)}
Test:
import '#testing-library/jest-dom';
import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from '#testing-library/preact';
test('Clicking OK displays Toast and it disappears', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: {}] } })
})
);
const CheckBox = screen.getByTestId('some-id');
fireEvent.click(CheckBox);
const SaveButton = screen.getByText('Save');
fireEvent.click(SaveButton);
expect(screen.getByText('OK')).toBeTruthy();
const OKButton = screen.getByText('OK');
fireEvent.click(OKButton);
await waitFor(() => {
expect(screen.getByText('Store Settings successfully updated!')).toBeInTheDocument();
}, { timeout: 11000 });
waitForElementToBeRemoved(screen.getByText('Store Settings successfully updated!')).then(() =>
expect(screen.queryByText('Store Settings successfully updated!')).not.toBeInTheDocument()
);
});
Try
await waitForElementToBeRemoved(...)
since waitForElementToBeRemoved is an async function call

React Uncaught Invariant Violation: Element type is invalid: expected a string

I am trying to use hooks and React context/provider to show modal in my app. But when I try to show it I am getting this error:
Here is the piece of code:
My Provider
function ModalProvider({ children }) {
const [ModalContent, setModalContent] = useState(null);
const [modalData, setModalData] = useState(null);
const [open, setOpen] = useState(false);
const showModal = useCallback(
({ content, data }) => {
setModalContent(content);
setModalData(data);
setOpen(true);
},
[setModalContent, setModalData, setOpen],
);
const hideModal = useCallback(
() => {
setModalContent(null);
setModalData(null);
setOpen(false);
},
[setModalContent, setModalData, setOpen],
);
const value = useMemo(
() => ({
ModalContent,
open,
modalData,
showModal,
hideModal,
}),
[ModalContent, modalData, open, showModal, hideModal],
);
return (
<ModalContext.Provider value={value}>
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && <ModalContent />}
</Modal>
{children}
</ModalContext.Provider>
);
ModalProvider.propTypes = {
children: PropTypes.node,
};
export default memo(ModalProvider);
Implementation
const Modal = () => {
return (
<div><h1>Test Modal</h1></div>
);
}
const Home = ({
logout,
}) => {
const { showModal, hideModal } = useModalDispatcher();
return (
<div>
<Button onClick={() => showModal({ content: Modal })} size="small">SHOW</Button>
<Button onClick={() => hideModal()} size="small">HIDE</Button>
</div>
);
};
Home.displayName = 'Home';
Home.propTypes = {
logout: PropTypes.func.isRequired,
};
export default Home;
The only way that this is working is when I use the prop "type" something like ModalContent.type I do not really know why is that.
The problem here is that you saving the component as a function when using the setModalContent function.
That is so because state can be initialized and updated with a function that returns the initial state or the updated state, you need to supply a function that in turn returns the function you want to put in state.
So, in order to get this working you could wrap the execution of you setModalContent inside the showModal function with an anonymous function, like this:
const showModal = useCallback(
({ content, data }) => {
setModalContent(() => content);
setModalData(data);
setOpen(true);
},
[setModalContent, setModalData, setOpen],
);
Hope it helps!
Can you try changing this, in your ModalProvider component
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && <ModalContent />}
</Modal>
To this
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && {ModalContent}}
</Modal>

Setting hook starts infinite loop and app hangs

This could be related to my problem, but I tried using the answer with no luck. It is like my react component start an infinite loop (the app hangs) when setting the hook setShow(!show):
There is a sandbox here were the problem is shown:
https://codesandbox.io/s/collapse-gmqpp
import React, { useState, useEffect } from "react";
const CrawlJobs = () => {
const [mediaList, setMediaList] = useState([]);
const [show, setShow] = useState(false);
useEffect(() => {
const fetchMediaData = async () => {
try {
setMediaList([{ id: 1, name: "Facebook" }, { id: 2, name: "Twitter" }]);
} catch (error) {}
};
fetchMediaData();
}, [mediaList]);
const toggle = () => {
setShow(!show);
};
return (
<div>
{mediaList.map(media => (
<div>
<div onClick={() => toggle()}>Show</div>
{show && <h1>Toggled context</h1>}
</div>
))}
</div>
);
};
export default CrawlJobs;
In this hook, you're updating mediaList and watching for changes on the same too.
useEffect(() => {
const fetchMediaData = async () => {
try {
setMediaList([{ id: 1, name: "Facebook" }, { id: 2, name: "Twitter" }]);
} catch (error) {}
};
fetchMediaData();
}, [mediaList]);
That's the cause of the infinite loop. Please use a callback using useCallback or completely remove the dependency array there.

Resources