This question already has answers here:
Wrong React hooks behaviour with event listener
(9 answers)
Closed 17 days ago.
I am trying to show multiple message using provider and hooks . But I am not able to show multiple message .One one message is show at one time don't know why ?
here is my code
https://codesandbox.io/s/new-mountain-cnkye5?file=/src/App.tsx:274-562
React.useEffect(() => {
setTimeout(() => {
utilContext.addMessage("error 2 sec");
}, 300);
setTimeout(() => {
utilContext.addMessage("error 5 mili sec");
}, 2000);
setTimeout(() => {
utilContext.addMessage("error 1 sec");
}, 1000);
}, []);
I am also using map function to render all message.
return (
<>
{messages.map((msg, index) => (
<div key={`Toast-Message-${index}`}>
{msg.msg}
<button
onClick={(event) => {
alert("000");
}}
>
close
</button>
</div>
))}
<ConfirmationDialogContext.Provider value={value}>
{children}
</ConfirmationDialogContext.Provider>
</>
);
Expected output : It will show 3 messages after some time.
Change your
const addMessage = (message: string, status: "success" | "error") => {
setmessages([...messages, { msg: message, type: status, duration: 5000 }]);
};
to
const addMessage = (message: string, status: "success" | "error") => {
setmessages((currentMessages) => [
...currentMessages,
{ msg: message, type: status, duration: 5000 }
]);
};
This is because you call the 3 addMessage in the same time, and so the messages variable has the same value in all three calls.
Read Updating state based on the previous state for more info on this syntax
You context is changing every time you add a message, and in your useEffect dependencies you don't have the context as a dep. which means that you're only adding messages to the first context instance, while rendering the latest one all the time.
BUT, if you add the context to your useEffect dependencies you will get an infinite loop.
One (Bad) solution would be to track every setTimeout with useRef like this:
export default function App() {
const utilContext = useConfirmationDialog();
const m1Ref = React.useRef(false);
const m2Ref = React.useRef(false);
const m3Ref = React.useRef(false);
React.useEffect(() => {
console.log("useEffect");
const s = [];
if (m1Ref.current === false) {
const s1 = setTimeout(() => {
utilContext.addMessage("error 300 msec");
}, 300);
s.push(s1);
m1Ref.current = true;
}
if (m2Ref.current === false) {
const s2 = setTimeout(() => {
m2Ref.current = true;
utilContext.addMessage("error 2 sec");
}, 2000);
s.push(s2);
}
if (m3Ref.current === false) {
const s3 = setTimeout(() => {
m3Ref.current = true;
utilContext.addMessage("error 1 sec");
}, 1000);
s.push(s3);
}
return () => {
s.forEach((x) => clearTimeout(x));
};
}, [utilContext]);
return (
<Typography>
MUI example. Please put the code to reproduce the issue in src/App.tsx
</Typography>
);
}
I think that you should move the delay of presenting the messages out of the sender into the context, like this:
const addMessage = (delay: number, message: string, status: "success" | "error") => {
setTimeout(() => {
setmessages((currentMessages) => [
...currentMessages,
{ msg: message, type: status, duration: 5000 }
]);
}, delay);
};
Related
Situation:
Im trying to write a custom hook that allows me to fetch data and launches a setInterval which polls status updates every two seconds. Once all data is processed, the interval is cleared to stop polling updates.
Problem:
My problem is that the function passed to setInterval only has the initial state (empty array) although it has already been updated. Either it is resetting back to the initial state or the function has an old reference.
Code:
Link to Codesandbox: https://codesandbox.io/s/pollingstate-9wziw7?file=/src/Hooks.tsx
Hook:
export type Invoice = {id: number, status: string};
export async function executeRequest<T>(callback: () => T, afterRequest: (response: {error: string | null, data: T}) => void) {
const response = callback();
afterRequest({error: null, data: response});
}
export function useInvoiceProcessing(formData: Invoice[]): [result: Invoice[], executeRequest: () => Promise<void>] {
const timer: MutableRefObject<NodeJS.Timer | undefined> = useRef();
const [result, setResult] = useState<Invoice[]>(formData);
// Mock what the API would return
const firstRequestResult: Invoice[] = [
{id: 1, status: "Success"},
{id: 2, status: "Pending"}, // This one needs to be polled again
{id: 3, status: "Failed"}
];
const firstPoll: Invoice = {id: 2, status: "Pending"};
const secondPoll: Invoice = {id: 2, status: "Success"};
// The function that triggers when the user clicks on "Execute Request"
async function handleFirstRequest() {
await executeRequest(() => firstRequestResult, response => {
if (!response.error) {
setResult(response.data)
if (response.data.some(invoice => invoice.status === "Pending")) {
// Initialize the timer to poll every 2 seconds
timer.current = setInterval(() => updateStatus(), 2000);
}
} else {
// setError
}
})
}
let isFirstPoll = true; // Helper variable to simulate a first poll
async function updateStatus() {
// Result has the initial formData values (NotUploaded) but should already have the values from the first request
console.log(result);
const newResult = [...result];
let index = 0;
for (const invoice of newResult) {
if (invoice.status === "Pending") {
await executeRequest(() => isFirstPoll ? firstPoll : secondPoll, response => {
if (!response.error) {
newResult[index] = response.data;
} else {
// Handle error
}
});
}
index++;
}
setResult(newResult);
isFirstPoll = false;
const areInvoicesPending = newResult.some(invoice => invoice.status === "Pending");
if (!areInvoicesPending) {
console.log("Manual clear")
clearInterval(timer.current);
}
};
useEffect(() => {
return () => {
console.log("Unmount clear")
clearInterval(timer.current);
}
}, [])
return [result, handleFirstRequest];
Usage:
const [result, executeRequest] = useInvoiceProcessing([
{ id: 1, status: "NotUploaded" },
{ id: 2, status: "NotUploaded" },
{ id: 3, status: "NotUploaded" }
]);
async function handleRequest() {
console.log("Start request");
executeRequest();
}
return (
<div className="App">
<button onClick={handleRequest}>Execute Request</button>
<p>
{result.map((invoice) => (
<Fragment key={invoice.id}>
{invoice.status}
<br />
</Fragment>
))}
</p>
</div>
);
EDIT1
I have one possible solution. This post helped me in the right direction: React hooks functions have old version of a state var
The closure that updateStatus uses is outdated. To solve that, I saved updateStatus in a useRef (updateStatus also needs useCallback). Although not necessary, I had to store result in a useRef as well but I'm not sure yet why.
const updateStatusRef = useRef(updateStatus);
useEffect(()=>{
updateStatusRef.current = updateStatus;
}, [updateStatus]);
Here's a working example: https://codesandbox.io/s/pollingstate-forked-njw4ct?file=/src/Hooks.tsx
I am running the query every 1 minute to check the progress.
let timer = null
const [inProgress, setInProgress] = useState(false)
const [
checkProgress,
{ loading2, data2 }
] = useLazyQuery(CHECK_PROGRESS, {
fetchPolicy: 'cache-and-network',
onCompleted: (data) => {
if(data.progress.completed) {
setInProgress(false)
// some code
} else {
// some code
}
}
}
)
useEffect(() => {
if (inProgress) {
timer = setInterval(() => {
checkProgress()
}, 1000 * 60)
} else {
clearInterval(timer)
}
return () => {
clearInterval(timer)
}
}, [inProgress])
onCompleted doesn't trigger once data.progress.completed is changed from false to true.
But the query is still running every 1 minute.
Any idea to fix this?
The issue was on the backend. I was setting null for non-null field.
I found the error after adding onError callback.
onError: (error) => {
console.log(error)
}
I'm getting this error when triggering a setState inside of a custom React hook. I'm not sure of how to fix it, can anyone show me what I'm doing wrong. It is getting the error when it hits handleSetReportState() line. How should I be setting the report state from inside the hook?
custom useinterval poll hook
export function usePoll(callback: IntervalFunction, delay: number) {
const savedCallback = useRef<IntervalFunction | null>()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
function tick() {
if (savedCallback.current !== null) {
savedCallback.current()
}
}
const id = setInterval(tick, delay)
return () => clearInterval(id)
}, [delay])
}
React FC
const BankLink: React.FC = ({ report: _report }) => {
const [report, setReport] = React.useState(_report)
if ([...Statues].includes(report.status)) {
usePoll(async () => {
const initialStatus = _report.status
const { result } = await apiPost(`/links/search` });
const currentReport = result.results.filter((item: { id: string; }) => item.id === _report.id)
if (currentReport[0].status !== initialStatus) {
handleSetReportState(currentReport[0])
console.log('status changed')
} else {
console.log('status unchanged')
}
}, 5000)
}
... rest
This is because you put usePoll in if condition, see https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
You can put the condition into the callback
usePoll(async () => {
if ([...Statues].includes(report.status)) {
const initialStatus = _report.status
const { result } = await apiPost(`/links/search` });
const currentReport = result.results.filter((item: { id: string; }) => item.id === _report.id)
if (currentReport[0].status !== initialStatus) {
handleSetReportState(currentReport[0])
console.log('status changed')
} else {
console.log('status unchanged')
}
}
}, 5000)
And if the delay will affect report.status, use ref to store report.status and read from ref value in the callback.
So I have a Form in React and, I have a username field and email field. After the user stops typing for 800ms I am checking whether the username/email (based on which field the user is typing) is available (not take by someone else).
I end up having two very similar useEffect functions doing the same thing for username and email, just differ in the URL at which they are sending a request and the variable they are watching.
I am here sharing the snippet of the code. I am using useReducer here.
Initial State :
const initialState = {
username: {
value: '',
hasErrors: false,
errorMessage: '',
isUnique: false,
checkCount: 0,
},
email: {
value: '',
hasErrors: false,
errorMessage: '',
isUnique: false,
checkCount: 0,
},
submitCount: 0,
}
Reducer Function:
//using immer
function myReducer(draft, action) {
switch (action.type) {
case 'usernameAfterDelay':
draft.username.checkCount++;
return;
case 'usernameUniqueResults':
if (action.value) {
draft.username.hasErrors = true;
draft.username.isUnique = false;
draft.username.message = 'That username is already taken.';
} else {
draft.username.isUnique = true;
}
return;
case 'emailAfterDelay':
draft.email.checkCount++;
return;
case 'emailUniqueResults':
if (action.value) {
draft.email.hasErrors = true;
draft.email.isUnique = false;
draft.email.message = 'That email is already being used.';
} else {
draft.email.isUnique = true;
}
return;
default:
return;
}
}
const [state, dispatch] = useImmerReducer(ourReducer, initialState);
My useEffect functions that are very similar
useEffect for applying to debounce to user typing
useEffect(() => {
if (state.username.value) {
const delay = setTimeout(() => {
dispatch({ type: 'usernameAfterDelay' });
}, 800);
return () => clearTimeout(delay);
}
}, [state.username.value]);
useEffect(() => {
if (state.email.value) {
const delay = setTimeout(() => {
dispatch({ type: 'emailAfterDelay' });
}, 800);
return () => clearTimeout(delay);
}
}, [state.email.value]);
useEffect for actually making the api call
useEffect(() => {
if (state.username.checkCount) {
const ourRequest = Axios.CancelToken.source();
async function fetchResults() {
try {
const response = await Axios.post(
'/doesUsernameExist',
{ username: state.username.value },
{ cancelToken: ourRequest.token }
);
dispatch({ type: 'usernameUniqueResults', value: response.data });
} catch (e) {
console.log('There was a problem or the request was cancelled.');
}
}
fetchResults();
return () => ourRequest.cancel();
}
}, [state.username.checkCount]);
useEffect(() => {
if (state.email.checkCount) {
const ourRequest = Axios.CancelToken.source();
async function fetchResults() {
try {
const response = await Axios.post(
'/doesEmailExist',
{ email: state.email.value },
{ cancelToken: ourRequest.token }
);
dispatch({ type: 'emailUniqueResults', value: response.data });
} catch (e) {
console.log('There was a problem or the request was cancelled.');
}
}
fetchResults();
return () => ourRequest.cancel();
}
}, [state.email.checkCount]);
JSX is as follows
<div>
<label htmlFor="username-register" className="text-muted mb-1">
<small>Username</small>
</label>
<input
id="username-register"
onChange={(e) =>
dispatch({
type: 'usernameImmediately',
value: e.target.value,
})
}
value={state.username.value}
/>
<CSSTransition
in={state.username.hasErrors}
timeout={330}
classNames="liveValidateMessage"
unmountOnExit
>
<div className="alert alert-danger small liveValidateMessage">
{state.username.message}
</div>
</CSSTransition>
<div>
<div>
<label htmlFor="email-register" className="text-muted mb-1">
<small>Email</small>
</label>
<input
id="email-register"
onChange={(e) =>
dispatch({
type: 'emailImmediately',
value: e.target.value,
})
}
/>
<CSSTransition
in={state.email.hasErrors}
timeout={330}
classNames="liveValidateMessage"
unmountOnExit
>
<div className="alert alert-danger small liveValidateMessage">
{state.email.message}
</div>
</CSSTransition>
</div>
As you can see there is a lot of repetitive code over here, I want to know is there any better way to handle things.
How about making a custom hook that will store the state and also check if the email is valid? Also you could probably combine your API call into a single function and then combine the useEffect into a single one.
Why are you using all these in useEffect when it's better used in your onchanged events?
let timer = []; //this is a global variable, need to be outside your component so the value does not get reset on every render.
const MyComponent = () => {
const [ isUsernameOk, setIsUsernameOk ] = useState(false)
......
const getResponse = (url, body) => await Axios.post(
url, body,
{ cancelToken: ourRequest.token }
); //u can put everything in one line.
const onChange = () => {
const { name, value } = e.target
dispatch({ type: 'usernameImmediately', value: e.target.value })
clearTimeout(timer[name]);
timer[name] = setTimeout(dispatch({ type: 'usernameAfterDelay' }),899)
// OR
timer[name] == setTimeout(async () => {
const response = await getResponse('/emailCheck', { email: test#email.com })
//do whatever u want with response
//do your dispatch or,
setIsUsernameOk(true)
})
}
return (
...............
{ isUserNameOk && <span> Username is available </span>
)
}
You do not need to use useEffect for whatever you are doing. Unnecessarily using useEffect is ineffective and may unnecessarily causing looping errors ( and also unnecessary re-rendering )
AND
This is a single check? Do you really need to go through all the redux codes to complicate your components? Is local state insufficient?
Using React and MongoDB/Mongoose, I'm trying to select a random item from the database, that meets the true parameter - basically, a user clicks a button to select a random fiction, or nonfiction writing prompt. Right now, when I console.log(fictionPrompts), nothing is returned, and just the loading piece from my if function displays. Am I missing something with my .filter()? This was working properly when I just had the randomizer function to get any prompt from the database, without trying to filter for a specific type.
From Mongoose:
const promptSchema = new mongoose.Schema({
isFiction: {
type: Boolean,
required: true
},
...
Part of React Component:
const Prompts = props => {
const [prompts, setPrompts] = useState([])
const [currentPrompt, setCurrentPrompt] = useState({})
const getFictionPrompts = () => {
axios(`${apiUrl}/prompts`)
.then(res => setPrompts(res.data.prompts))
.then(() => {
props.alert({
message: 'You\'ve received a prompt',
variant: 'success'
})
})
.catch(() => {
props.alert({
message: 'Something went wrong',
variant: 'danger'
})
})
const fictionPrompts = prompts.filter(prompt => (prompt.isFiction === true))
const newPromptIndex = Math.floor(Math.random() * fictionPrompts.length)
setCurrentPrompt(fictionPrompts[newPromptIndex])
}
let promptsJsx = ''
if (!currentPrompt) {
promptsJsx = 'Loading...'
} else {
promptsJsx = currentPrompt.text
}
return (
<Layout>
<button className='btn btn-primary prompt-button' onClick={getFictionPrompts}>Get A Fiction Prompt!</button>
<button className='btn btn-primary prompt-button' onClick={getNonFictionPrompts}>Get A Non-Fiction Prompt!</button>
<p>{promptsJsx}</p>
</Layout>
)
}
The reason that you are having issues here is that axios still hasn't returned the data when you are trying to set currentPrompt.
If you update your code to the following it should work:
const getFictionPrompts = () => {
axios(`${apiUrl}/prompts`)
.then(res => {
setPrompts(res.data.prompts);
// use the data here
const fictionPrompts = res.data.prompts.filter(
prompt => prompt.isFiction === true
);
const newPromptIndex = Math.floor(
Math.random() * fictionPrompts.length
);
setCurrentPrompt(fictionPrompts[newPromptIndex]);
})
.then(() => {
props.alert({
message: "You've received a prompt",
variant: "success"
});
})
.catch(() => {
props.alert({
message: "Something went wrong",
variant: "danger"
});
});
};