Component will preserve state after unmounting it - reactjs

Component will preserve state even after unmounting it
I'm building a feedback form with Formik and want to move from class components to hooks but face mentioned difficulties.
function Feedback(props) {
const [fileInfo, setFileInfo] = useState("");
const [feedbackStatus, setFeedbackStatus] = useState("");
let timer = null;
useEffect(() => {
const status = props.feedbackResponse.status;
if (status) {
if (status >= 200 && status < 300) {
setFeedbackStatus("success");
timer = setTimeout(() => {
props.history.goBack();
}, 2500);
} else if (status === "pending") {
setFeedbackStatus("pending");
} else {
setFeedbackStatus("error");
}
}
return () => {
clearInterval(timer);
}
}, [props.feedbackResponse.status]);
// ...code ommited for brevity
}
This effect runs succesfully after my form submission while waiting for a server response. Feedback component is a react-router modal component, if it matters. However, if I re-open that modal, I see a success message instead of a new form. In my return I am conditionally rendering a success message if feedbackStatus === "success" or a form that, depending on a server response, might display an error message otherwise. My class component works fine with this code:
componentDidUpdate(prevProps) {
const status = this.props.feedbackResponse.status;
if (prevProps.feedbackResponse.status !== status) {
if (status >= 200 && status < 300) {
this.setState({feedbackStatus: "success"});
this.timer = setTimeout(() => {
this.props.history.goBack();
}, 2500);
} else if (status === "pending") {
this.setState({feedbackStatus: "pending"});
} else {
this.setState({feedbackStatus: "error"});
};
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
Expected output: reopening this modal component after a successful form submit should render a new form but it renders a previous' submit status. This leads me to think that I'm not unmounting my Feedback component at all but where's my mistake then?

You can use <Feedback key={someKey} />.
This will ensure that a new instance of Feedback component is made when you re-open it, thus your old success/failure messages will be erased from the state.

The above behaviour happens because the effect is run on initial render as well and in that case props.feedbackStatus might be preserved from the previous instances.
Since you only wish to execute the effect when the component updates, you would need to stop execution of useEffect on initial render which happens even when you pass values to the dependency array. You can do that using useRef
function Feedback(props) {
const [fileInfo, setFileInfo] = useState("");
const [feedbackStatus, setFeedbackStatus] = useState("");
const isInitialRender = useRef(true);
let timer = null;
useEffect(() => {
if(isInitialRender.current === true) {
isInitialRender.current = false;
} else {
const status = props.feedbackResponse.status;
if (status) {
if (status >= 200 && status < 300) {
setFeedbackStatus("success");
timer = setTimeout(() => {
props.history.goBack();
}, 2500);
} else if (status === "pending") {
setFeedbackStatus("pending");
} else {
setFeedbackStatus("error");
}
}
}
return () => {
clearInterval(timer);
}
}, [props.feedbackResponse.status]);
}

Related

change a static method for a functional component. I have been making class to functional but I have never done a static method yet

here is what the static method looks like, I want to change the class component to a functional component but functional components don't support it. I am still learning, any advice will be appreciated
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.security.validToken) {
setTimeout(() => {
const product_type = localStorage.getItem("types");
if (product_type === "imp") {
nextProps.history.push("/imported");
} else if (product_type === "branded") {
nextProps.history.push("/brands");
} else if (product_type === "imp,branded" || product_type === "branded,imp") {
nextProps.history.push("/imported-brands");
} else if (product_type === "local") {
nextProps.history.push("/local");
}
}, 1000);
}
Would be easier if you could share the whole component, or at least how/where you plan to manage your functional component's state. As said on this anwser:
This has a number of ways that it can be done, but the best way is situational.
Let's see how we could create a ProductsRouter functional component.
import React from 'react'
import { useHistory } from 'react-router'
export const ProductsRouter = ({ security }) => {
// our page state
const [productType, setProductType] = React.useState('')
// history object instance
const history = useHistory()
// our routing effect
React.useEffect(() => {
// if not authenticated, do nothing
if (!security.validToken) return
// update our state
setProductType(localStorage.getItem('types'))
// timeout function
const timer = setTimeout(() => {
if (productType === 'imp') {
history.push('/imported')
} else if (productType === 'branded') {
history.push('/brands')
} else if (productType === 'imp,branded' || productType === 'branded,imp') {
history.push('/imported-brands')
} else if (productType === 'local') {
history.push('/local')
}
}, 1000)
return () => clearTimeout(timer)
}, [])
return <pre>{JSON.stringify({ history }, null, 2)}</pre>
}
We could use a map to keep our code tidy and flexible. If there's no particular reason to have a 1000ms timeout, we could also call push instantly, as long as we have a validToken.
const typesMap = new Map([
['imp', '/imported'],
['branded', '/brands'],
['imp,branded', '/imported-brands'],
['branded,imp', '/imported-brands'],
['local', '/local'],
])
export const ProductsRouter = ({ security }) => {
const history = useHistory()
React.useEffect(() => {
if (!security.validToken) return
history.push(typesMap.get(localStorage.getItem('types')))
return () => {}
}, [security.validToken])
return <pre>{JSON.stringify({ history }, null, 2)}</pre>
}
Hope that helps! Cheers

How to make notification appear in 4 seconds, but avoid it if state has changed

I want to show notification message after 4 seconds when some state is invalid. But if during that 4 seconds it has changed and is valid now - I want to put condition in setTimeout that would check it. But the problem is that it still uses the first state value, not the changed one. One of my assumptions to fix it was making setState in the line before synchronous, but don't know how. Maybe any other ways to fix it?
useEffect(async () => {
try {
const snippetIndexResponse = await getSnippetIndex(
//some params
);
if (snippetIndexResponse !== -1) {
setSnippetIndex(snippetIndexResponse);
} else {
setSnippetIndex(null)
setTimeout(() => {
console.log(snippetIndex) <-- it logs only first state, instead wanted null
if(!snippetIndex) {
openNotificationWithIcon(
"error",
"Invalid snippet selection",
"Snippet slice shouldn't tear code blocks. Please, try again."
);
}
}, 4000)
}
} catch (err) {
setSnippetIndex(null);
openNotificationWithIcon("error", err.name, err.message);
}
}, [beginRow, endRow]);
First You can not call useEffect callback as async method.
Second for your purpose you can act as below:
let timeoutId = null;
useEffect(() => {
(async () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
try {
const snippetIndexResponse = await getSnippetIndex(
//some params
);
if (snippetIndexResponse !== -1) {
setSnippetIndex(snippetIndexResponse);
} else {
setSnippetIndex(null)
timeoutId = setTimeout(() => {
console.log(snippetIndex) <-- it logs only first state, instead wanted null
if(!snippetIndex) {
openNotificationWithIcon(
"error",
"Invalid snippet selection",
"Snippet slice shouldn't tear code blocks. Please, try again."
);
}
}, 4000)
}
} catch (err) {
setSnippetIndex(null);
openNotificationWithIcon("error", err.name, err.message);
}
})();
}, [beginRow, endRow]);
I think you could make the notification UI a component and pass the state in as a parameter. If the state changes the component will be destroyed and recreated.
And you can add the 4 second timer in useEffect() as well as cancel it in the 'return' of useEffect. If the timer fires, update some visibility flag.
Live typing this - so may contain some syntax errors...
cost myAlert = (isLoading) => {
const [isVisible, setIsVisible] = setIsVisible(false)
useEffect(()=>{
const timerId = setTimer, setIsVisible(true) when it goes off
return ()=>{cancelTimer(timerId)}
}, [isLoading, setIsVisible])
if (!isLoading && !isVisible) return null
if (isVisible) return (
<>
// your ui
</>
)
}
You may want to useCallback on setTimer so it won't cause useEffect to fire if isLoading changes -- but I think this should get you close to the solution.

setSomeState() called from a catch() function doesn't update

If have a problem with react code where, for some reason, everything works as expected when I update the UI from the MyApp.promise().then(<here>) but not in my MyApp.promise().then().catch(<here>)
I know the code is actually executed up to the point I actually call setData which is my useState() returned function
A call to that function in then() works just fine, not in catch()
the exception that eventually triggers catch() works fine since the catch() is executed as expected
I added a console.log() inside my component, and I see that it's no longer re-drawn when the updates comes from catch()
I guess my question is : what would be special in a catch() function so react wouldn't behave ?
This is the code for my application hook that handles upgrade status updates :
const useUpdateStatus = () => {
const [data,setData] = useState({status: STATUS.IDLE,changelog:null,tasks:[]})
const updateData = (d) => {
// We call setData with an anonymous function so we can merge previous
// data with new data
setData((prev) => {
console.log({ ...prev, ...d })
return { ...prev, ...d }
})
};
// Only once, we set the timer to periodically update status
useEffect(() => {
setInterval(() => {
MyApp.get('/system/upgrade')
.then((upgrade) => {
// If anything is not "pending", it means we are upgrading
for (var t of upgrade.tasks) {
if (t.status !== "pending") {
updateData({ status: STATUS.INSTALLING})
}
}
// updateData will call setData with the full status
// This works as intended, UI is updated on each step
updateData({ tasks: upgrade.tasks, changelog: upgrade.changelog})
})
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => {
for (var t of prev.tasks) {
if (t['id'] === "restarting") {
t['status'] = 'running'
}
else if (t['status'] == "running") {
t['status'] = 'finished'
}
}
// The expected data is logged here
console.log(prev)
return prev
})
})
}, 1000);
},[])
return data
}
This is the presentation layer :
// Using the hook :
const { changelog, tasks, status } = useUpdateStatus()
// Somewhere int he page :
<UpdateProgress tasks={tasks}/>
// The actual components :
const UpdateProgress = (props) => {
return(
<div style={{display: "flex", width: "100%"}}>
{ props.tasks.map(s => {
return(
<UpdateTask key={s.name} task={s}/>
)
})}
</div>
)
}
const UpdateTask = (props) => {
const colors = {
"pending":"LightGray",
"running":"SteelBlue",
"finished":"Green",
"failed":"red"
}
return(
<div style={{ textAlign: "center", flex: "1" }}>
<Check fill={colors[props.task.status]} width="50px" height="50px"/><br/>
<p style={props.task.status==="running" ? {fontWeight: 'bold'} : { fontWeight: 'normal'}}>{props.task.name}</p>
</div>
)
}
React performs an Object.is comparison to check is a re-render is needed after a state update call. Since you are mutating the state in catch block, react is falsely notified that the state hasn't changed and hence a re-render in not triggered
You can update your state like below to make it work
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => {
for (var t of prev.tasks) {
if (t['id'] === "restarting") {
t['status'] = 'running'
}
else if (t['status'] == "running") {
t['status'] = 'finished'
}
}
// The expected data is logged here
console.log(prev)
return {...prev}
})
})
However a better way to update state is to do it in an immutable manner
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => ({
...prev,
tasks: prev.tasks.map((task) => {
if (task.id === "restarting") {
return { ...task, status: 'running'}
}
else if (task.id === "running") {
return { ...task, status: 'finished'}
}
return task
})
}))
})

I try to use useState but dont works weirdly

I used the useState of react to deal with "hooks" (is the name of it?)
const [users, setUsers] = useState(null);
In the next piece of code I use the setUsers but dosnt do it...
getUsersApi(queryString.stringify(params)).then(response => {
console.log(params.page)
// eslint-disable-next-line eqeqeq
if(params.page == "1"){
console.log(response)//line33
if(isEmpty(response)){
setUsers([]);
}else{
setUsers(response);
console.log(users);//line38
}
}else{
if(!response){
setBtnLoading(0);
}else{
setUsers({...users, ...response});
setBtnLoading(false);
}
}
})
I inserted a console.log inside of it and apparently pass through there, but dosnt set users...
Here is the function getUsersApi() in case you need it.
export function getUsersApi(paramsUrl){
console.log(paramsUrl)
const url = `${API_HOST}/users?${paramsUrl}`
const params = {
headers:{
Authorization: `Bearer${getTokenApi()}`,
},
};
return fetch(url, params).then(response => {
return response.json();
}).then(result => {
return result;
}).catch(err => {
return err;
});
}
Here is the function isEmpty() in case you need it.
function isEmpty(value) {
if (value == null) {
return true;
}
if (isArrayLike(value) &&
(isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||
isBuffer(value) || isTypedArray(value) || isArguments(value))) {
return !value.length;
}
var tag = getTag(value);
if (tag == mapTag || tag == setTag) {
return !value.size;
}
if (isPrototype(value)) {
return !baseKeys(value).length;
}
for (var key in value) {
if (hasOwnProperty.call(value, key)) {
return false;
}
}
return true;
}
Thanks a lot guys!!
If you want to know if the state has been updated, be sure to set a useEffect hook with the state inside the dependency array.
import React, { useEffect, useState } from 'react';
useEffect(() => {
console.log("Users: ", users)
}, [users])
If this gets logged, users have been sucessfully updated.
If this does NOT gets logged, you're not entering the else which calls setUsers

Skip first useEffect when there are multiple useEffects

To restrict useEffect from running on the first render we can do:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
According to example here: https://stackoverflow.com/a/53351556/3102993
But what if my component has multiple useEffects, each of which handle a different useState change? I've tried using the isFirstRun.current logic in the other useEffect but since one returns, the other one still runs on the initial render.
Some context:
const Comp = () => {
const [ amount, setAmount ] = useState(props.Item ? Item.Val : 0);
const [ type, setType ] = useState(props.Item ? Item.Type : "Type1");
useEffect(() => {
props.OnAmountChange(amount);
}, [amount]);
useEffect(() => {
props.OnTypeChange(type);
}, [type]);
return {
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
}
}
The reason I've used separate useEffects for each is because if I do the following, it doesn't update the amount.
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
As far as I understand, you need to control the execution of useEffect logic on the first mount and consecutive rerenders. You want to skip the first useEffect. Effects run after the render of the components.
So if you are using this solution:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
useEffect (() => {
// second useEffect
if(!isFirstRun) {
console.log("Effect was run");
}
});
So in this case, once isFirstRun ref is set to false, for all the consecutive effects the value of isFirstRun becomes false and hence all will run.
What you can do is, use something like a useMount custom Hook which can tell you whether it is the first render or a consecutive rerender. Here is the example code:
const {useState} = React
function useMounted() {
const [isMounted, setIsMounted] = useState(false)
React.useEffect(() => {
setIsMounted(true)
}, [])
return isMounted
}
function App() {
const [valueFirst, setValueFirst] = useState(0)
const [valueSecond, setValueSecond] = useState(0)
const isMounted = useMounted()
//1st effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueFirst ran")
}
}, [valueFirst])
//2nd effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueSecond ran")
}
}, [valueSecond])
return ( <
div >
<
span > {
valueFirst
} < /span> <
button onClick = {
() => {
setValueFirst((c) => c + 1)
}
} >
Trigger valueFirstEffect < /button> <
span > {
valueSecond
} < /span> <
button onClick = {
() => {
setValueSecond((c) => c + 1)
}
} >
Trigger valueSecondEffect < /button>
<
/div>
)
}
ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I hope it helps !!
You can use a single useEffect to do both effects in, you just implemented the logic incorrectly.
Your original attempt:
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
The issue here is the if/elseif, treat these as independent effects instead:
useEffect(() => {
if (amount !== 0) props.onAmountChange(amount);
if (type !== "Type1") props.onTypeChange(type);
}, [amount, type])
In this method if the value is different than the original value, it will call the on change. This has a bug however in that if the user ever switches the value back to the default it won't work. So I would suggest implementing the entire bit of code like this instead:
const Comp = () => {
const [ amount, setAmount ] = useState(null);
const [ type, setType ] = useState(null);
useEffect(() => {
if (amount !== null) {
props.onAmountChange(amount);
} else {
props.onAmountChange(0);
}
}, [amount]);
useEffect(() => {
if (type !== null) {
props.onTypeChange(type);
} else {
props.onTypeChange("Type1");
}
}, [type]);
return (
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
)
}
By using null as the initial state, you can delay calling the props methods until the user sets a value in the Radio that changes the states.
If you are using multiple useEffects that check for isFirstRun, make sure only the last one (on bottom) is setting isFirstRun to false. React goes through useEffects in order!
creds to #Dror Bar comment from react-hooks: skip first run in useEffect

Resources