Why runs the component 2 times with useEffect? - javascript

On the screen I get : Hello { "name" : "Jack" }
In console I get:
App rendering
useFetch useEffect
App rendering
Because what I have seen in the console , I think App.js must have ran 2 times and useFetch must have ran 1 time .
I think this is behind :
App.js calls useFetch and writes on console that App rendering . App.js does NOT wait useFetch to complete, so Hello undefined is on the screen . BUT we don't see undefined on the screen , because it is so fast and it goes away -> because of 2. step :
While App.js writes on console and displays undefined on screen , useFetch is running . When useFetch completes its running it gives back the correct object ( Jack )
App.js component runs again and writes again on console and displays the correct object ( Hello Jack )
But if this is true , then why runs App.js 2 times ?
Somebody said that my theory about this is false and useFetch gives back 2 times value . First it gives back null as object and second time the correct value , which is from fetch . So he said that useFetch does not wait fetch function to complete , so it gives back first time null and when fetch function completes then it gives back Jack .
But if he has right , then why is that ? Why does not wait useFetch to complete its functions to complete ?
Can somebody write step by step whats happening behind ?
App.js :
import { useFetch } from './useFetch';
function App() {
const { data } = useFetch({ url : "jack.json" })
console.log('App rendering')
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div> )
}
export default App;
useFetch.js :
import { useState, useEffect } from "react";
export const useFetch = (options) => {
const [data, setData] = useState(null);
useEffect(() => {
console.log("useFetch useEffect");
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
}, [])
return { data }
}
My Question inspired this :
https://youtu.be/dH6i3GurZW8?t=316

Your thinking is correct.
But if this is true , then why runs App.js 2 times ?
Because App is a react component and like all other react components it will re-render whenever its data dependencies change. In your case, App will re-render whenever the value of data changes.
Your custom useFetch hook uses the useEffect hook to fetch data. Effects in React are performed after react components have rendered. That's why the console logs the first "App rendering". Once the useFetch hook calls setData, the value of data is updated which triggers the second re-render of App.

Here is a step-by-step of what's happening:
function App() {
const { data } = useFetch({ url : "jack.json" })
console.log('App rendering')
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div> )
}
Before I begin, it may help to also understand how the react component lifecycle works:
From the start, it's just like calling a regular function...
If you refer to the diagram above, we are at the stage called render, because the body of a functional component is the equivalent of the render function in class components.
First, useFetch is called
const { data } = useFetch({ url : "jack.json" });
Inside useFetch
const [data, setData] = useState(null);
At this point, data is null
Next useEffect is called
useEffect(() => {
console.log("useFetch useEffect");
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
}, []);
useEffect creates an effect watcher. This is basically a function which is invoked every time one of its dependencies changes. However, it is not invoked until after the first time the component renders.
If you refer to the diagram above, the watcher will only be invoked for the first time at the point called componentDidMount.
Back inside App:
(image reposted to avoid scrolling)
console.log('App rendering')
We get a nice console.log output...
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div>
);
Here we return control back to react, and it will now attempt to take our html and mount it inside the dom.
Note that data is still null. Every other component react encounters will go through the same process described above.
So far, the steps leading up to this point can be summarized as Mounting (See the diagram above), and we have now reached the componentDidMount stage.
componentDidMount
React will now invoke our effect.
Note: If you had more than one effect, react will invoke all of them, and none will be skipped for this first round of effectual work that react does. Also they will all be invoked in the same order you declared them in.
useEffect watcher invoked
console.log("useFetch useEffect");
Another nice printout to console.log
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
fetch is invoked, but since we are not allowed to wait for promises inside effects, the fetch just runs.
Note: If your effect returns any function, react saves it to run it the next time an update/unmount occurs.
At this point, we just kinda idle until something interesting hap...oh wait fetch is done.
State Updates (setData(...))
(image reposted to avoid scrolling)
.then( json => setData(json))
setData will update the state of the component. If you refer to the diagram above once again, you'll see that when a state update occurs, the next step is to render.
Therefore, react will once again repeat the same steps as above, but it does not re-create the useEffect, or the useState again, because it has stored them from the previous render.
Sidenote: This is also how react is able to detect when you are calling a hook (useEffect, useState, etc) conditionally. If react detects a new hook after the first render is finished, it will warn you of this
App is rendered again
console.log('App rendering')
Next...
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div>
);
And we once again return the html content of our component (this time data is now whatever was sent to setData).
Updating
As you can tell, we are now in the Updating Phase. From now on, react will simply wait for props/state updates, and runs any effects that depend on them in the componentDidUpdate stage of the lifecycle
Sidenote: Your useEffect should depend on options.url so that it is re-run when the url changes
useEffect(() => {
console.log("useFetch useEffect");
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
}, [options?.url]);
Unmounting
If by some bad luck, your component is unmounted either by the parent, or by some strange magic, your component will be moved into the Unmounting phase. Once again, refer to the diagram above
In this phase, none of the effects are run. Instead, any functions returned by your "effect watcher", will be invoked to do cleanup.
Note: All the cleanup functions will be run in the order they were encountered (typically top-down)

Related

React component does not re-render under Jest on state change

Component:
const MyComponent = props => {
const {price} = props;
const result1 = useResult(price);
return (
<div>...</div>
)
}
Custom Hook:
export const useResult = (price) => {
const [result, setResult] = useState([]);
useEffect(() => {
const data = [{price: price}]
setResult(data);
}, [price]);
return result;
};
Jest test:
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor(() => {
expect(...).toBeInTheDocument();
});
});
What it does happen with the above code is that MyComponent, when running the test, renders only once instead of two (when the application runs). After the initial render where result1 is an empty array, useEffect of useResult is running and since there is a state change due to setResult(data), I should expect MyComponent to be re-rendered. However, that's not the case and result1 still equals to [] whereas it should equal to [{price:300}].
Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the component that calls them.
Any explanation/thoughts for the above?
UPDATE
The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push in order to add an element to an array that was a state variable...
Well, it seems that you are asking a very specific thing about testing a custom hook. In that case, I also had some issues in the past testing custom hooks through #testing-library and a different package was created (and recently incorporated into the #testing-library) that provides the renderHook() function for testing custom hooks. I suggest you to test that.
Original Package (do not use it. Use directly the TL one)
Docs about the renderHook() call inside the TL docs
You can read more about it in this blog post from Kent C. Dodds.
I also suggest you create a "state change" to test your component and test the hook with the renderHook().
Here is a simple codesandbox with some tests for a component similar to your case.
Original Answer
Essentially, your test is not waiting for the component to perform the side effects. There are 2 ways of waiting for that:
Using waitFor()
import { waitFor, screen } from '#testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
await waitFor(() =>
expect(screen.getByText('your-text-goes-here')).toBeInTheDocument()
)
});
Using the findBy* query from RTL, that returns a Promise (read the Docs here) and is a combination from the waitFor and getBy* query (read docs here)
import { screen } from '#testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
expect(await screen.findByText('your-text-goes-here')).toBeInTheDocument();
});
Step 1: the code being tested
If, as mentioned in the comments of the question, the operation inside the effect is synchronous, then using useEffect for setting this state based on the props is undesirable in all cases. Not only for testing.
The component will render, update the DOM and immediately need to re render the following frame because it's state was updated. It causes a flash effect for the user and needlessly slows the app down.
If the operation is cheap, it's way more efficient to just execute it on every render.
If the operation can be more expensive, you can wrap it in useMemo to ensure it only happens when there's changes to the inputs.
export const useResult = (price) => {
return useMemo(
// I assume this is a stub for a expensive operation.
() => [{price: price}],
[price]
);
};
If, for some obscure reason, you do need to do this in an effect anyway (you probably don't but there's edge cases), you can use a layoutEffect instead. It will be processed synchronously and avoid the flashing frame. Still wouldn't recommend it but it's a slight improvement over a regular effect.
Step 2: Testing
If you changed the component to not use an effect, it should now be correct from the first render, and you don't have the problem anymore. Avoiding having a problem in the first place is also a valid solution :D
If you do find the need to flush something synchronously in a test, there's now the flushSync function which does just that.
Perhaps it would also flush the state update in the effect, causing your test to work with no other changes. I guess it should, as new updates triggered by effects while flushing should continue to be processed before returning.
flushSync(() => {
render(
<MyComponent price={300}/>)
)
})
In any case there's no point doing this if you can instead improve the component to fix the additional render introduced by setting state in an effect.
you can do:
The test will have to be async: it('should ...', async() => { ....
await screen.findByText('whatever');
This is async so it will wait to find whatever and fail if it can't find it
or you can do
await waitFor (() => {
const whatever = screen.getByText('whatever');
expect(whatever).toBeInTheDocument();
})
You are not waiting for the component to be rerendered
import { waitFor, screen } from 'testing-library/react'
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor (() => {
// check that props.price is shown
screen.debug() // check what's renderered
expect(screen.getByText(300)).toBeInTheDocument();
});
});

React - Updating data in dispatch from useEffect

I am trying updating data in dispatch in useEffect but showing warning in console
React Hook useEffect has missing dependencies: 'dispatch', 'id', and 'state.selectedHotel'. Either include them or remove the dependency array react-hooks/exhaustive-deps
code
import { GlobalContext } from "../../../context/globalContext";
const HotelDetail = () => {
const [state, dispatch] = useContext(GlobalContext);
const { id } = useParams();
useEffect(() => {
const hotelData = async () => {
try {
let response = await ServiceGetAllHotels();
let hotel = response.hotels.filter(hotel => {
return hotel.hotelUserName === id;
});
dispatch({
type: "UPDATE",
payload: { selectedHotel: hotel[0] }
});
}catch(){}
};
}, [])
};
But warning message disappear when I add this (below code)
useEffect(() => {
.....
}, [dispatch, state.selectedHotel, id])
I dont understand why this error/warning , why error disappear when I add this ? Please help Can I go with this code?
Its not an error but a warning that can save you from bugs because of useEffect hook not running when it was supposed to.
useEffect hook, by default, executes after:
the initial render
each time a component is re-rendered
Sometimes we don't want this default behavior; passing a second optional argument to useEffect hook changes the default execution behavior of useEffect hook. Second argument to useEffect hook is known as its dependency array that tells React when to execute the useEffect hook.
Run "useEffect" once, after the initial render
We can achieve this by passing an empty array as the second argument to the useEffect hook:
useEffect(() => {
// code
}, []);
This effect will only execute once, similar to componentDidMount in class components.
Run "useEffect" everytime any of its dependency changes
When the code inside the useEffect depends on the state or a prop, you sometimes want useEffect to execute every time that piece of state or prop changes.
How can we tell React to run the effect every time a particular state or prop changes? By adding that state or prop in the dependency array of the useEffect hook.
Example:
Imagine a Post component that receives post id as a prop and it fetches the comments related to that post.
You might write the following code to fetch the comments:
useEffect(() => {
fetch(`/${props.postId}`)
.then(res => res.json())
.then(comments => setComments(comments))
.catch(...)
}, []);
Problem with the above code:
When the Post component is rendered for the first time, useEffect hook will execute, fetching the comments using the id of the post passed in as the argument.
But what if the post id changes or the post id is not available during the first render of the Post component?
If post id prop changes, Post component will re-render BUT the post comments will not be fetched because useEffect hook will only execute once, after the initial render.
How can you solve this problem?
By adding post id prop in the dependency array of the useEffect hook.
useEffect(() => {
fetch(`/${props.postId}`)
.then(res => res.json())
.then(comments => setComments(comments))
.catch(...)
}, [props.postId]);
Now every time post id changes, useEffect will be executed, fetching the comments related to the post.
This is the kind of problem you can run into by missing the dependencies of the useEffect hook and React is warning you about it.
You should not omit any dependencies of the useEffect hook or other hooks like: useMemo or useCallback. Not omitting them will save you from such warnings from React but more importantly, it will save you from bugs.
Infinite loop of state update and re-render
One thing to keep in mind when adding dependencies in the dependency array of the useEffect is that if your are not careful, your code can get stuck in an infinite cycle of:
useEffect --> state update ---> re-render --> useEffect ....
Consider the following example:
useEffect(() => {
const newState = state.map(...);
setState(data);
}, [state, setState]);
In the above example, if we remove the state from the dependency array, we will get a warning about missing dependencies and if we add state in the array, we will get an infinite cycle of state update and re-render.
What can we do?
One way is to skip the state as a dependency of the useState hook and disable the warning using the following:
// eslint-disable-next-line react-hooks/exhaustive-deps
Above solution will work but it's not ideal.
Ideal solution is to change your code in such a way that allows you to remove the dependency that is causing the problem. In this case, we can simply use the functional form of the setState which takes a callback function as shown below:
useEffect(() => {
setState(currState => currState.map(...));
}, [setState]);
Now we don't need to add state in the dependency array - problem solved!
Summary
Don't omit the dependencies of the useEffect hook
Be mindful of the infinite cycle of state update and re-render. If you face this problem, try to change your code in such a way that you can safely remove the dependency that is causing the infinite cycle
The useEffect hook accepts two arguments. The first one is a classic callback and the second one is an array of so called "dependencies".
The hook is designed to execute the callback immediately after component has been mount (after elements have been successfully added to the real DOM and references are available) and then on every render if at least one of the values in the dependencies array has changed.
So, if you pass an empty array, your callback will be executed only once during the full lifecycle of your component.
It makes sense if you think about it from a memory point of view. Each time that the component function is executed, a new callback is created storing references to the current execution context variables. If those variables change and a new callback is not created, then the old callback would still use the old values.
This is why "missing dependencies" is marked as a warning (not an error), code could perfectly work with missing dependencies, sometimes it could be also intentional. Even if you can always add all dependencies and then perform internal checks. It is a good practice to pass all your dependencies so your callback is always up to date.

React Update Profile for user through a form

I'm trying to build a Update profile form where a user can change his details and onSubmit the new data will be sent to PUT/users end point to update the details in the database. The form will have the old values pre filled (gotten through a GET request to the same /users endpoint). To access this endpoint we also need to send a basic auth header with email and password. Now I'm making two fetch requests one to get the existing details and one to PUT the new details. My first GET request is successfully made and I can see the data prefilled but my second POST request doesn't work. What am I doing wrong?
Here is how my code looks. https://codesandbox.io/s/dawn-breeze-itinq?file=/src/App.js
const getUsers = async () => {
let myHeaders = new Headers();
myHeaders.set('Authorization', 'Basic ' + credentials);
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow',
};
try {
const response = await fetch(`${APIlink}/users`, requestOptions);
const result = await response.json();
setData(result);
} catch (err) {
setErr('Incorrect Password. Please Retry.');
}
};
useEffect(() => {
getUsers();
}, []);
You useEffect gets called on Every render as you have not mentioned any dependency array. So what happens in your case is,
your component renders --> useEffect gets called --> make api call --> you set state --> component re-renders --> useEffect gets called --> make api call and this cycle continues forever .
useEffect(() => {
....
}); => this useEffect will trigger on first render and every re-render
useEffect(() => {
...
}, []); => this will trigger only for the first time when the component is mounted
useEffect(() => {
...
}, [value]); => triggers on first render and whenever the value changes
If you are familiar with lifecycle methods in class based components, you usually do API calls in componentDidMount lifecycle method. This method is called only once after the first render. More on this: https://reactjs.org/docs/react-component.html#componentdidmount
In your code, you are using useEffect which is more or less like the componentDidUpdate lifecycle method, which is called on every render. this is why,
The page loads and I can see an endless string of GET requests in the network tab.
More on this: https://reactjs.org/docs/react-component.html#componentdidupdate
The solution would be to make your useEffect hook behave like a componentDidMount lifecycle method, which is essentially telling the useEffect hook to run only once.
If we examine a hook, it is made up of two components:
Callback function (required)
Dependency list of props (optional)
which looks like this:
useEffect(Callback, [Dependencies]);
An example would be:
useEffect(() => {
console.log(props.color)
}, [props.color]);
So essentially we are saying that whenever the prop color is changed, we want to log the value of color.
Seeing as how this is, if we pass an empty array of dependencies, the useEffect hook will only run once and that is what you should do. Do note that if a dependencies array is not passed, it will still behave like componentDidUpdate lifecycle method i.e it will be executed on every render.
Solution if you just skipped to this part is:
Pass an empty dependencies list in your useEffect hook.
More on hooks:
https://reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Do React hooks need to return a value?

I've recently started to build out custom hooks in my React application and have been following the documentation on the React website. However, the hooks which I am building require no return value as they set up data for Redux on initialization.
Example:
// custom hook
export const useSetup() {
useEffect(() => {
if (data) fetch().then(data => dispatch(setInit(data)))
}, [dispatch])
}
// functional component
export function Details() {
useSetup()
I can't find documentation explicitly stating that a hook needs to return anything. However, I cannot find an example of a hook not returning something. Can someone advise on if this approach is correct?
Yes, your approach is correct. React hooks are not required to return anything. The React documentation states that:
We don’t have to return a named function from the effect. We called it
cleanup here to clarify its purpose, but you could return an arrow
function or call it something different.
The return value of a function that is passed as an argument to a hook has a special use in the lifecycle of the React component it belongs to. Essentially, that return value is expected to be a function and executes before the component with the hook re-renders or is unmounted. React documentation call this kind of hook an "effect with cleanup."
The React documentation uses the example below to show what a useEffect hook looks like:
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
As you can see, the anonymous function that is used as an argument to useEffect does not have a return statement.
You can verify this by changing the function a little bit to log the return value:
const count = 0;
const a = () => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
}
console.log(a());
This prints undefined.
You can also use console.log on the useEffect function to see that it also returns undefined.
If you changed the hook to this:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
return () => {
console.log('cleanup');
}
});
You would see the "cleanup" message every time the component re-renders or is unmounted. You would have to trigger the re-render by updating the state of the component in some way.

React / Axios send infinite number of requests

I'm fetching data from my backend api when component mount and can do it successfuly but my React app keeps sending requests to the server causing it to slow down. I used useEffect hook, but I'm getting the same result without using the hook.
useEffect(() => {
axios.get('http://127.0.0.1:8000/food_category/')
.then(response => {
setFoodCategory(response.data);
console.log(response.data);
})});
What am I doing wrong?
If you give no dependencies to the useEffect hook, it will execute every time your component renders (which will happen infinitely because you set the state after getting data and thus your component rerenders).
Check out the second argument of useEffect in the docs to learn more about it.
An empty dependencies array indicates the useEffect will act as a mount and only executes once.
useEffect(() => {
// Do mount stuff here such as executing your request.
}, []);

Categories