Setting hook starts infinite loop and app hangs - reactjs

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.

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?

Setting a parent's state to a child's state setter

I have the following React example, where I'm setting a state to the value of another state setter.
import React, {
Dispatch,
SetStateAction,
useEffect,
useState,
} from "react";
import ReactDOM from "react-dom";
const A = ({}) => {
const [setStage, setStageSetter] = useState<Dispatch<SetStateAction<number>>();
useEffect(() => {
console.log('A.setStage', setStage);
if (setStage) {
setStage(2);
}
}, [setStage, setStageSetter]);
return <B setStageSetter={setStageSetter} />;
};
const B = ({ setStageSetter }) => {
const [stage, setStage] = useState<number>(1);
useEffect(() => {
console.log('B.setStage', setStage);
setStageSetter(setStage);
}, [setStage, setStageSetter]);
return <p>{stage}</p>;
};
ReactDOM.render(<A />, mountNode);
However, when I run the above, it does not output what I'd expect, which is for the above to read 2 in the DOM. It seems to set the value to 1, and then to undefined Why is this?
A solution to the above is the following answer:
const { useEffect, useState } = React;
const B = ({ setStageSetter }) => {
const [stage, setStage] = useState(1);
useEffect(() => {
setStageSetter(() => setStage);
}, [setStage, setStageSetter]);
return <p>{stage}</p>;
};
const A = () => {
const [setStage, setStageSetter] = useState();
useEffect(() => {
if (setStage) {
setStage(2);
}
}, [setStage]);
return <B setStageSetter={setStageSetter} />;
};
ReactDOM.render(<A />, mountNode);
The functional difference is from setStageSetter(setStage); to setStageSetter(() => setStage);
Whilst this does set the parent's state to the setter of the child, and I can call a method in the parent and set the child's state, I don't understand why the callback is required. Why is this? Is this "bad practice"?
When the setStageSetter call was invoked with a different parameter, it seems to have achieved the desired objective. Please see the code snippet below for a working demo.
Code Snippet
const {
Dispatch,
SetStateAction,
useEffect,
useState
} = React;
const B = ({ setStageSetter }) => {
const [stage, setStage] = useState(1);
// <Number>
useEffect(() => {
console.log('B stage: ', stage, ' setStage', setStage);
// calling the props method with a "function" and not the setter as-is
setStageSetter(() => setStage);
}, [setStage, setStageSetter]);
return <p>{stage}</p>;
};
const A = ({}) => {
const [setStageA, setStageSetter] = useState(()=>{});
// <Dispatch<SetStateAction<Number>>
useEffect(() => {
console.log('A setStageA', setStageA);
if (setStageA) { // to realize the change on screen, delay by 1.5 seconds
setTimeout(() => setStageA(2), 1500);
// setStageA(2); // this works too, but change is not immediately observable
}
}, [setStageA, setStageSetter]);
return <B setStageSetter={setStageSetter} />;
};
ReactDOM.render(
<div>
DEMO
<A />
</div>,
document.getElementById("rd")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="rd" />
Explanation
Inline comments added to the snippet above.

How to create delayed effect with setTimeout and useEffect?

Ref: https://stackblitz.com/edit/react-ts-8ykcuf?file=index.tsx
I created a small example to replicate the issue I am facing.
I am trying to create a delayed effect with setTimeout inside useEffect. I can see from console.log that setTimeout has already triggered and I expect the DOM to be updated, but actually the DOM is not rendered until the next human interaction.
The side effect in the sample example is to simulate a bot appending new message after user has entered a new message.
import React, { useEffect, useState } from 'react';
import { render } from 'react-dom';
interface Chat {
messages: string[];
poster: string;
}
const App = () => {
const [activeChat, setActiveChat] = useState<Chat>({
poster: 'Adam',
messages: ['one', 'two', 'three'],
});
const [message, setMessage] = useState('');
const [isBotChat, setIsBotChat] = useState(false);
useEffect(() => {
if (isBotChat) {
setIsBotChat(false);
setTimeout(() => {
activeChat.messages.push('dsadsadsada');
console.log('setTimeout completed');
}, 500);
}
}, [isBotChat]);
const handleSubmit = (e) => {
e.preventDefault();
if (message !== '') {
activeChat.messages.push(message);
setMessage('');
setIsBotChat(true);
}
};
return (
<div>
<h1>Active Chat</h1>
<div>
{activeChat?.messages.map((m, index) => (
<div key={index}>{m}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.currentTarget.value)}
/>
<button type="submit">Send Message</button>
</form>
</div>
);
};
render(<App />, document.getElementById('root'));
To set your state you need to use setActiveChat, in this case something like:
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, 'dsadsadsada']
}))
The set state function React provides can accept a function, which we'll use in this case to avoid race conditions. previous is the previous value of activeChat (We can't rely on activeChat itself being up to date yet since the current render may be out of sync with the state) Then we expand the existing state and add the new property.
In your comments you mention only changing properties, and I'm afraid it's really not recommended to change anything in state directly, there are several in depth explanations of that here (StackOverflow question), and here (Documentation).
Full example (StackBlitz):
import React, { useEffect, useState } from 'react';
import { render } from 'react-dom';
import './style.css';
interface Chat {
messages: string[];
poster: string;
}
const App = () => {
const [activeChat, setActiveChat] = useState<Chat>({
poster: 'Adam',
messages: ['one', 'two', 'three'],
});
const [message, setMessage] = useState('');
const [isBotChat, setIsBotChat] = useState(false);
useEffect(() => {
if (isBotChat) {
setIsBotChat(false);
setTimeout(() => {
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, 'dsadsadsada']
}))
console.log('setTimeout completed');
}, 500);
}
}, [isBotChat]);
const handleSubmit = (e) => {
e.preventDefault();
if (message !== '') {
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, message]
}))
setMessage('');
setTimeout(() => {
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, 'dsadsadsada']
}))
console.log('setTimeout completed');
}, 500);
}
};
return (
<div>
<h1>Active Chat</h1>
<div>
{activeChat?.messages.map((m, index) => (
<div key={index}>{m}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.currentTarget.value)}
/>
<button type="submit">Send Message</button>
</form>
</div>
);
};
render(<App />, document.getElementById('root'));

I dont know how to hunt down my problem - React

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}
/>

Cant pass array of objects to setPeople function in React

I have another file of data.js that i'm calling it in my index.js. I know I will get data if I pass it in useState directly like this below.
const [people, setPeople] = useState(data);
But I want to keep the above state null at the start rather than passing data.
const [people, setPeople] = useState([]);
But rather than accessing data this way I want to directly pass it in my setPeople. Is it possible to do that? Something like this. So that it can be accessed in people and can be iterated.
setPeople([...people,{data}])
Index.js
import React, { useState, useReducer } from 'react';
import Modal from './Modal';
import { data } from '../../../data';
// reducer function
const Index = () => {
const [name,setName]= useState('');
const [modal, showModal] = useState(false);
const [people, setPeople] = useState([]);
const handleSubmit = ((e)=>
{
e.preventDefault();
setPeople([...people,{data}])
})
return (
<div>
<form onSubmit={handleSubmit}>
<input type='text' value={name} onChange={((e)=>setName(e.target.value))} />
<button type='submit'>Add</button>
</form>
{
people.map((person,index)=>{
return(
<div key={index}>
<h2>{person.name}</h2>
</div>
)
})
}
</div>
)
};
export default Index;
data.js
export const data = [
{ id: 1, name: 'john' },
{ id: 2, name: 'peter' },
{ id: 3, name: 'susan' },
{ id: 4, name: 'anna' },
];
You can pass this additional data to setPeople in the same way which you pass current people - use spread syntax.
setPeople([ ...people, ...data ])
Also if you want to depend on current people state you should pass callback which always provide you correct value which sometimes can't not happen if you depend on people.
setPeople(( prevPeople ) => [ ...prevPeople, ...data ])
UPDATE
const Index = () => {
const [name,setName]= useState('');
const [modal, showModal] = useState(false);
const [people, setPeople] = useState([]);
useEffect( () => {
setState(data) // this will be added only once when component mounted
}, [])
const handleSubmit = ((e)=>{
e.preventDefault();
setPeople(( prevPeople ) => [ ...prevPeople, { id: ?, name: "Only one new person" } ])
})
return (
<div>
{ /* your jsx */ }
</div>
)
};
UPDATE 2
const handleSubmit = () => {
const peoplesIds = people.map( p => p.id )
const notIncludedData = data.filter( obj => ! peoplesIds.includes( obj.id ) )
setPeople( prev => [ ...prev, ...notIncludedData, { id: 100, name: "new guy" } ] )
}
You can try this at handleSubmit
setPeople([...people,...data])

Resources