I'm using multiple useEffect hooks to perform the componentDidMount and componentDidUpdate functionalities, however, looks like when component loads, all my useEffect fires initially...
const testComp = () => {
const stateUserId = useSelector(state => { return state.userId; });
const [userId, setUserId] = useState(stateUserId);
const [active, setActive] = useState(false);
const [accountId, setAccountId] = useState();
useEffect(() => {
console.log('component did mount...');
}, [userId]);
useEffect(() => {
console.log('should trigger when userId changes...');
}, [userId]);
useEffect(() => {
console.log('should trigger when active changes...');
}, [active]);
useEffect(() => {
console.log('should trigger when accountId changes...');
}, [accounted]);
return (<div>...</div);
}
when my component mounts, I see all those console log there
component did mount...
should trigger when userId changes...
should trigger when active changes...
should trigger when accountId changes...
How could I only let my first useEffect fires, but the other three fires when the dependency changes only?
useEffect is not a direct replacement of componentDidMount and componentDidUpdate. Effect will run after each render, which is why you are seeing all those console logs. According to the React documentation, the second parameter of useEffect means
you can choose to fire them (effects) only when certain values have changed.
After the initial render, if the component is rendered again, only effects watch the corresponding value changes are triggered.
One way to achieve what you want is by creating additional variables to host initial values and do comparisons in the useEffect when you need to.
const testComp = () => {
const initUserId = useSelector(state => { return state.userId; });
const stateUserId = initUserId;
const [userId, setUserId] = useState(stateUserId);
useEffect(() => {
console.log('component did mount...');
}, [userId]);
useEffect(() => {
if (userId !== initUserId) {
console.log('should trigger when userId changes...');
}
}, [userId]);
return <div>...</div>
}
Any useEffect hook will always be fired at component mount and when some of the variables in its dependency array changes. If you want to perform an action just when the variable changes you must check the incoming value first and do the required validations.
Also your first useEffect must have an empty dependency array to fire it just when the component mounts, because as it is it will also be called when userId changes.
you can use custom useEffect:
const useCustomEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
and use it in your component:
useCustomEffect(() => {
console.log('trigger when userId changes...');
}, [userId]);
Related
I use a lot of firestore snapshots in my react native application. I am also using React hooks. The code looks something like this:
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// When the component initially loads, add all the loaded data to state.
// When data changes on firestore, we receive that update here in this
// callback and then update the UI based on current state
});;
}, []);
At first I assumed useState would be the best hook to store and update the UI. However, based on the way my useEffect hook is set up with an empty dependency array, when the snapshot callback gets fired with updated data and I try to modify the current state with the new changes, the current state is undefined. I believe this is because of a closure. I am able to get around it using useRef with a forceUpdate() like so:
const dataRef = useRef(initialData);
const [, updateState] = React.useState();
const forceUpdate = useCallback(() => updateState({}), []);
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// if snapshot data is added
dataRef.current.push(newData)
forceUpdate()
// if snapshot data is updated
dataRef.current.find(e => some condition) = updatedData
forceUpdate()
});;
}, []);
return(
// JSX that uses dataRef.current directly
)
My question is am I doing this correct by using useRef along with a forceUpdate instead of useState in a different way? It doesn't seem right that I'm having to update a useRef hook and call forceUpdate() all over my app. When trying useState I tried adding the state variable to the dependency array but ended up with an infinite loop. I only want the snapshot function to be initialized once and the stateful data in the component to be updated over time as things change on the backend (which fires in the onSnapshot callback).
It would be better if you combine useEffect and useState. UseEffect will setup and detach the listener, useState can just be responsible for the data you need.
const [data, setData] = useState([]);
useEffect(() => {
const unsubscribe = someFirestoreAPICall().onSnapshot(snap => {
const data = snap.docs.map(doc => doc.data())
this.setData(data)
});
//remember to unsubscribe from your realtime listener on unmount or you will create a memory leak
return () => unsubscribe()
}, []);
Then you can just reference "data" from the useState hook in your app.
A simple useEffect worked for me, i don't need to create a helper function or anything of sorts,
useEffect(() => {
const colRef = collection(db, "data")
//real time update
onSnapshot(colRef, (snapshot) => {
snapshot.docs.forEach((doc) => {
setTestData((prev) => [...prev, doc.data()])
// console.log("onsnapshot", doc.data());
})
})
}, [])
I found that inside of the onSnapshot() method I was unable to access state(e.g. if I console.log(state) I would get an empty value.
Creating a helper function worked for, but I'm not sure if this is hack-y solution or not but something like:
[state, setState] = useState([])
stateHelperFunction = () => {
//update state here
setState()
}
firestoreAPICall.onSnapshot(snapshot => {
stateHelperFunction(doc.data())
})
use can get the currentState using callback on set hook
const [state, setState] = useState([]);
firestoreAPICall.onSnapshot(snapshot => {
setState(prevState => { prevState.push(doc.data()) return prevState; })
})
prevState will have Current State Value
Earlier I had a Class component, so I didn't face any issues while using lifecycle methods, but after converting to useEffect hooks, I am facing the initial render issue which I don't want to happen.
Class
componentDidMount() {
this.setState({
patchVal:this.props.patchTaskVal,
startTime:this.props.patchStartTime,
setEndTime:this.props.patchEndTime
})
}
componentDidUpdate(prevProps) {
if (prevProps.patchTaskVal !== this.props.patchTaskVal) {
this.callValidation()
}
if (prevProps.closeTask !== this.props.closeTask) {
this.setState({
showValue:false,
progressValue:[],
startTime:new Date(),
setEndTime:""
})
}
if (prevProps.patchStartTime !== this.props.patchStartTime || prevProps.endTime !== this.props.endTime && this.props.endTime !== "") {
this.setState({
startTime:this.props.patchStartTime,
setEndTime:parseInt(this.props.endTime)
})
}
}
Functional
const [patchTaskVal, setPatchTaskVal]=useState(/*initial value */)
const [startTime, setStartTime]=useState()
const [endTime, setEndTime] = useState()
**// I want only this useEffect to run on the initial render**
useEffect(() => {
setPatchTaskVal(props.patchTaskVal)
...//set other states
}, [])
useEffect(() => {
callValidation()
}, [props.patchTaskVal])
useEffect(() => {
//setShowValue...
}, [props.closeTask])
useEffect(() => {
if (props.endTime != "") {
// set states...
}
}, [props.patchStartTime,props.endTime])
Here I am facing an issue where all the useEffects are running on the initial render, Please suggest a solution for this so that only the first useEffect will run on the initial render and all other useEffects will run according to its dependency prop values.
You basically need a ref which will tell you whether this is the first render on not. Refs values persist over rerenders. You can start with a truthy value and toggle it to false after the first render (using a useEffect with an empty array[]). Based on that you can run your desired code.
You can also put the whole thing in a custom hook:
import { useEffect, useRef } from "react";
const useOnUpdate = (callback, deps) => {
const isFirst = useRef(true);
useEffect(() => {
if (!isFirst.current) {
callback();
}
}, deps);
useEffect(() => {
isFirst.current = false;
}, []);
};
export default useOnUpdate;
You can call this hook in your component like :
useOnUpdate(() => {
console.log(prop);
}, [prop]);
In the hook:
After the initial render, both useEffects run. But when the first effect runs the value of the isFirst.current is true. So the callback is not called. The second useEffect also runs and sets isFirst.current to false.
Now in subsequent renders only the first useEffect run (when dependencies change), and isFirst.current is false now so callback is executed.
The order of the two useEffects is very important here. Otherwise, in the useEffect with deps, isFirst.current will be true even after the first render.
Link
If you compare the functional and the class component you can notice that there is one part missing - previous props.
Functional component does not have previous props in scope, but you can save them yourself with a small trick: save them to reference so it will not impact you render cycle.
Since now you have the previous props and the current props you can apply the same logic you did for class component.
import React, { useRef, useEffect, useState } from "react";
default function App() {
const [input, setInput] = useState("");
const [commitInput, setCommitInput] = useState("");
return (
<>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={() => setCommitInput(input)}>apply</button>
<Child test={commitInput} />
</>
);
}
function Child(props) {
const prev = useRef(props.test);
useEffect(() => {
if (prev.current !== props.test) {
alert("only when changes");
}
}, [props.test]);
return <div>{props.test}</div>;
}
try this...
let init = true;
useEffect( ()=>{
if(init) {
setPatchTaskVal(props.patchTaskVal)
init = false;
...//set other states}
}, [])
useEffect( ()=> {
!init && callValidation()
},[props.patchTaskVal])
useEffect( ()=>{
//!init && setShowValue...
},[props.closeTask])
useEffect( ()=>{
if(props.endTime!="" && !init){
// set states...
}
},[props.patchStartTime,props.endTime])
Hope my understanding is right about your question.
Why not just add a if statement to check the state is not undefined or default value
useEffect( ()=> {
if (props.patchTaskVal) {
callValidation()
}
},[props.patchTaskVal])
useEffect( ()=>{
if (props.closeTask) {
//setShowValue...
}
},[props.closeTask])
useEffect( ()=>{
if(props.patchStartTime){
// set states...
}
if(props.endTime){
// set states...
}
},[props.patchStartTime,props.endTime]
And according your class component,
this.setState({
patchVal:this.props.patchTaskVal,
startTime:this.props.patchStartTime,
setEndTime:this.props.patchEndTime
})
The function component should map props to component's state. Like this
const [patchTaskVal, setPatchTaskVal]=useState(props.patchTaskVal)
const [startTime, setStartTime]=useState(props.patchStartTime)
const [endTime, setEndTime] = useState(props.patchEndTime)
I'm creating a Counter component and passing count as a prop from the parent component.
When the Counter component is mounted, I'm registering a focus event handler on the window object.
Whenever the window gets focused I'm trying to print the count prop value in the console. But I'm not getting the latest count prop value instead I'm getting the initial count value.
Is it happening because whenever there is a change in any prop, new props object gets created in memory and passed to the child component, and as I'm not attaching my event handler whenever there is a change in the count prop, I'm getting the initial prop ( stale prop ) value because displayCount function is not attached to focus event again and still pointing to the old reference of the props object?
Please correct me if I'm wrong.
Using a ref will work. But I'm interested in knowing what exactly is the reason behind this behavior.
//CounterDisplay.js
import React, { useEffect, useRef } from 'react';
const CounterDisplay = (props) => {
const { count } = props;
const countRef = useRef(count);
useEffect(() => {
window.addEventListener('focus', displayCount);
return(() => {
window.removeEventListener('focus', displayCount);
});
},[]);
useEffect(() => {
countRef.current = count;
},[count]);
const displayCount = () => {
console.log(" count prop -> "+ props.count);
console.log(" countRef -> "+ countRef.current);
}
return (
<h1>{count}</h1>
);
}
export default CounterDisplay;
You're missing a dependency in the dependency array of your first useEffect. You can try to restructure your code to something like this, and see if that works. In the current version, your useEffect does not update when props changes, since you provide an empty dependency array.
import React, { useEffect, useRef } from 'react';
const CounterDisplay = (props) => {
const { count } = props;
const countRef = useRef(count);
useEffect(() => {
const displayCount = () => {
console.log(" count prop -> "+ count);
console.log(" countRef -> "+ countRef.current);
}
window.addEventListener('focus', displayCount);
return(() => {
window.removeEventListener('focus', displayCount);
});
},[count]);
useEffect(() => {
countRef.current = count;
},[count]);
return (
<h1>{count}</h1>
);
}
Your useEffect needs to render after every update of your count value. So try using [count]
I use ReactJs, jest and react testing library. I have this code:
const App = ({data}) => {
const [state, setState] = useState(); // after useEffect runs state should be false
console.log('test state', state)
useEffect(() => {
setState(!data)
}, [])
return <p>'data is' {data},<p/>
}
<App data={12} />
After the useEffect will run the state should be false. Now i want to test the component using jest and react testing library.
describe('App', () => {
beforeEach(() => {
const setValue = jest.fn(x => {});
React.useState = jest.fn()
.mockImplementationOnce(x => [x, setValue])
})
test('It should render the correct value', async() => {
const v = utils.queryByText(12)
expect(v).toBeInTheDocument()
})
})
})
When i run the test in console.log('test state', state) i get for first time console.log('test state', undefined) and after that console.log('test state', false), but i need to get only the false value, because due the fact that for the first time the value is undefined the test fails. How to do this?
You need to wait until the component did mount and then run your expectation. In react testing library, there is a special method for it. waitFor.
from React testing library documentation:
When in need to wait for any period of time you can use waitFor, to wait for your expectations to pass.
import {waitFor} from '#testing-library/react'
describe('App', () => {
// rest of the codes ...
test('It should render the correct value', async() => {
const v = utils.queryByText(12)
await waitFor(() => {
expect(v).toBeInTheDocument()
})
})
})
})
You are getting two console logs because this line initialises your state (undefined):
const [state, setState] = useState();
After that the useEffect hook runs onComponentDidMount and changes the state, which causes the component to rerender (second log statement).
You could use something like this, if you want to init the state from the props:
const [state, setState] = useState(!data);
Update:
This works on my machine:
Maybe you have some typos?
I was starting to build some of my new components with the new and shiny React Hooks. But I was using a lot of async api calls in my components where I also show a loading spinner while the data is fetching. So as far as I understood the concept this should be correct:
const InsideCompontent = props => {
const [loading, setLoading] = useState(false);
useEffect(() => {
...
fetchData()
...
},[])
function fetchData() {
setFetching(true);
apiCall().then(() => {
setFetching(false)
})
}
}
So this is just my initial idea of how this might work. Just a small example.
But what happens if the parent component has now a condition changed that this component gets unmounted before the async call is finished.
Is there somehow a check where I can check if the component is still mounted before I call the setFetching(false) in the api callback?
Or am I missing something here ?
Here is working example :
https://codesandbox.io/s/1o0pm2j5yq
EDIT:
There was no really issue here. You can try it out here:
https://codesandbox.io/s/1o0pm2j5yq
The error was from something else, so with hooks you don't need to check if the component is mounted or not before doing a state change.
Another reason why to use it :)
You can use the useRef hook to store any mutable value you like, so you could use this to toggle a variable isMounted to false when the component is unmounted, and check if this variable is true before you try to update the state.
Example
const { useState, useRef, useEffect } = React;
function apiCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Foo");
}, 2000);
});
}
const InsideCompontent = props => {
const [state, setState] = useState({ isLoading: true, data: null });
const isMounted = useRef(true);
useEffect(() => {
apiCall().then(data => {
if (isMounted.current) {
setState({ isLoading: false, data });
}
});
return () => {
isMounted.current = false
};
}, []);
if (state.isLoading) return <div>Loading...</div>
return <div>{state.data}</div>;
};
function App() {
const [isMounted, setIsMounted] = useState(true);
useEffect(() => {
setTimeout(() => {
setIsMounted(false);
}, 1000);
}, []);
return isMounted ? <InsideCompontent /> : null;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Here's a Hook for fetching data that we use internally. It also allows manipulating the data once it's fetched and will throw out data if another call is made prior to a call finishing.
https://www.npmjs.com/package/use-data-hook
(You can also just include the code if you don't want an entire package)
^ Also this converts to JavaScript by simply removing the types.
It is loosely inspired by this article, but with more capabilities, so if you don't need the data-manipulation you can always use the solution in that article.
Assuming that this is the error you've encountered:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
React complains and hints you at the same time. If component has to be unmounted but there is an outstanding network request, it should be cancelled. Returning a function from within useEffect is a mechanism for performing any sort of cleanup required (docs).
Building on your example with setTimeout:
const [fetching, setFetching] = useState(true);
useEffect(() => {
const timerId = setTimeout(() => {
setFetching(false);
}, 4000);
return () => clearTimeout(timerId)
})
In case component unmounts before the callback fires, timer is cleared and setFetching won't be invoked.