I have this very simple component called Editor:
export default function Editor() {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [currentData, setCurrentData] = useState({});
useEffect(() => {
fetch("http://localhost:4000/pages")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setCurrentData(result);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [])
if (error) {
return (
<LoadingError value={error.message} />
);
} else if (!isLoaded) {
return (
<Loading />
)
} else {
console.log(currentData);
}
return ( <div></div> );
}
Notice the last else condition, where my currentData is logged into the console.
What happens when this component is rendered is that my console.log() fires twice! The first time my currentData object is empty. The second time, the object properly contains all the data from my API.
However, as my console.log() should fire only when there is no error (the first if condition), and only when the data is loaded (the second else if condition), how is it possible that I end up with empty object in the console the first time? What am I doing wrong?
Your useEffect only fires once. You're seeing the console.log output twice because you're doing updateState twice. Change the order to see the expected behaviour you're looking for
This is what's happening right now:
setIsLoaded(true); // Causes 1st rerender and so console.log executes with []
setCurrentData(result); // Causes second rerender and console.log has data.
What you want:
setCurrentData(result); // Causes 1st rerender but does not reach the else statement
setIsLoaded(true); // Reaches the else statement and currentData is already set
This will make sure your currentData is set before isLoaded is set to true
As of now, React does not batch update state calls in the above scenario, but this will change once React v18 is released.
Why does this React's useEffect() fires twice after loaded?
It doesn't.
The function Editor runs again because you call setIsLoaded and setCurrentData once the promise in the effect (which runs only once) resolves.
I have a feeling that React only batches state updates for certain handlers, like onClick and others, but I have feeling that when state updates occur in the callback of a promise, they dont get batched, so you will have two renders happening in your case.
edit - which is probably what #Harkunwar's answer is alluding to, in that the order in which you update your state matters
Related
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();
});
});
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)
I am using a hook component and several state variables. I have read about using useEffect() with params to get a kind of callback after updating a state. Example:
export const hookComponent = () => {
const [var, setVar] = useState(null);
useEffect(() => {
//do things
}, [var])
}
In this example, useEffect() would be executed on every setVar() call. In my case, I do not want to execute useEffect() everytime, but only on specific occasions.
I would like to give the setVar() some kind of information which I can use in useEffect() like setVar(newValue, true).
Note: I do not want to store this information in var.
Is there a way to do this?
Like Nizar said, simple conditional check on 'var' in useEffect
If expensive calc you can
const expensiveValue = useMemo(() => {
// other logic here if needed
// could even be simple return var=='x'?true:false, although this would be easier to do in the useEffect hook?
return computeExpensiveValue(var);
},[var]);
useEffect(() => {
//do things
//expensiveValue only changes when you want it to from the memo
}, [expensiveValue])
Thank you sambomartin and Nizar for your input.
For everyone looking for an answer:
After some further research I found 3 possible solutions:
Use a class component. If you really are dependent on that state update to be completed switch to a class component, which allows you to give the setState() a callback as a second param.
Use the useRef hook to determine where your state update is comming from. You can use this information in the useEffect() method.
Get independent from the state. I used this solution and externalized my callback function with the drawback of giving it every parameter on every call, although they are present in the component the states are saved.
As far as I know, the useEffect only triggers if the dependency value changes, not simply by executing setValue.
I offer you three solutions, the first one, close to what you want but without using useEffect hook, the second one is an extension of the first one, that may be required if you need control over the previous state, and the third, more general, like comments say, though it won't be triggered if the state is the same, even if you execute setValue.
First solution: Wrap your set value with another function that definitely controls what may happen after or before the new state:
export default function MyComponent() {
const [state, setState] = useState(null);
const handleChangeSetState = (nextState, flag) => {
if (flag) {
specialUseCaseCb();
}
setState(nextState);
};
return <div>{/* ... */}</div>;
}
Second solution: Wrap your set value with another function, like in the solution 1, and ask for the previous or next state within setState inner callback:
export default function MyComponent2() {
const [state, setState] = useState(0);
const handleChangeSetState = (increment, flag) =>
setState((prevState) => {
const nextState = prevState + increment;
// you may need prevState or nextState for checking your use case
if (flag) {
specialUseCaseCb();
}
return nextState;
});
return <div>{/* ... */}</div>;
}
Third solution: use useEffect hook to follow changes, remember though that setState won't re-trigger useEffect hook if the state is the same:
export default function MyComponent3() {
const [state, setState] = useState("");
// notice that this will only be triggered if state changes
useEffect(() => {
if (state !== "my-special-use-case") return;
specialUseCaseCb();
}, [state]);
return <div>{/* ... */}</div>;
}
I want to update useState array values by calling a function that maps through an array (called from the database) and the useState array will be updated for each item in the (database array) so I have tried the following approach:
const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState([]);
// *** get from the database ***** //
useEffect(()=> {
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
.get()
.then((snapshot) => {
setSnapshots(snapshot.docs)
}
) ;
}, []);
// *** get from the database ***** //
// *** update items value ***** //
return <div className="cart__items__item">
{snapshots && snapshots.map((doc)=>(
setItems([...items, doc.data().id]),
console.log(items)
))
}
</div>
// *** update items value ***** //
but the following error appears:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I have tried to console.log the result to see the check the issue and the Items array was logged in the console continuously I have tried to include the code in a useEffect but it did not work as well .
Never call a state setter at the top level of your component function. With function components, the key thing to remember is that when you change state, your function will get called again with the updated state. If your code has a state change at the top level of the function (as yours does in the question), every time the function runs, you change state, causing the function to run, causing another state change, and so on, and so on. In your code:
const initialArray = []; // *** 1
const [Items, setItems] = useState(initialArray) // *** 2
initialArray.push("pushed item")
setItems(initialArray) // *** 3
Creates a new array every time
Only uses the first one to set the initial value of Items when the component is created
Sets new array in state, causing the function to be called again
Instead, you should be setting state only in response to some change or event, such as a click handler, or some other state changing, etc.
Also note that you must not directly modify an object (including an array) that you have in state. Your code doesn't technically do that (since there's a new initialArray every time), but it looks like what you meant to do. To add to an array in state, you copy the array and add the new entry at the end.
An example of the above:
function Example() {
const [items, setItems] = useState([]);
const clickHandler = e => {
setItems([...items, e.currentTarget.value]);
};
return <div>
<div>
{items.map(item => <div key={item}>{item}</div>)}
</div>
<input type="button" value="A" onClick={clickHandler} />
<input type="button" value="B" onClick={clickHandler} />
<input type="button" value="C" onClick={clickHandler} />
</div>;
}
(Slightly odd UI just to keep the code example simple.)
Note that conventionally Items would be called items.
Re your update:
That code calls setItems at the top level of the function, so it has the problem above. Instead, you do that work in the useEffect querying the database.
There's no reason to call setItems repeatedly during the map operation.
The code should handle the component unmounting while the DB operation is outstanding
The code should actually render something in the map in the JSX
The code should handle errors (rejections)
E.g., something like this:
const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState(); // *** If you're going to use `undefined`
// as the initial state of `snapshots`,
// you probably want to do the same with
// `items`
useEffect(()=> {
let cancelled = false;
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
.get()
.then((snapshot) => {
// *** Don't try to set state if we've been unmounted in the meantime
if (!cancelled) {
setSnapshots(snapshot.docs);
// *** Create `items` **once** when you get the snapshots
setItems(snapshot.docs.map(doc => doc.data().id));
}
})
// *** You need to catch and handle rejections
.catch(error => {
// ...handle/report error...
});
return () => {
// *** The component has been unmounted. If you can proactively cancel
// the outstanding DB operation here, that would be best practice.
// This sets a flag so that it definitely doesn't try to update an
// unmounted component, either because A) You can't cancel the DB
// operation, and/or B) You can, but the cancellation occurred *just*
// at the wrong time to prevent the promise fulfillment callback from
// being queued. (E.g., you need it even if you can cancel.)
cancelled = true;
};
}, []);
// *** Use `items` here
return <div className="cart__items__item">
{items && items.map(id => <div>{id}</div>)/* *** Or whatever renders ID */}
</div>;
Note that that code assumes that doc.data().id is a synchronous operation.
What you are seeing here is standard react lifecycle behavior. Your component will be mounted and then render (run alle the code inside your component). After rendering the first time it "listens" to changes to the values that you handle in your component and re-renders if any changes are detected.
Your case:
const initialArray = [];
const [Items, setItems] = useState(initialArray)
initialArray.push("pushed item")
setItems(initialArray)
I see 2 bad things here:
You modify the array which you use as the initial value of your state and keep using that same array to update your state.
You push a new item and update the state on each render when calling setItems(initialArray)
Lets focus on the second one, since thats the one that causes your issues. If you want to avoid the endless render cycle then you should move your setItems() call to a method which doesn't run on each render. Before functional components this would be done in the componentDidMount() function. In a functional component this is done using the useEffect hook:
useEffect(() => {
// Your code here
}, [])
Notice the empty array supplied to useEffect. This array lists which dependencies should cause this useEffect to run. If you leave it empty it will only run once and act like the old componentDidMount() function.
So to solve your issue of endless rendering, you need to move setItems() into the useEffect hook.
In the example below, I'm getting in a loop when I call the onError prop in fetchItems(). I don't understand why, when called, it triggers hooks depending on it. How can I fix this? Thanks!
const Component = ({onError}) => {
const [items, setItems] = useState([]);
const itemsRef = useRef(items);
const fetchItems = useCallback(() => {
const [first] = itemsRef.current;
fetchNewItemsSince(first || 0).then((newItems) => {
setItems((oldItems) => [...oldItems, ...newItems]);
}).catch(onError);
}, [onError]);
// Update ref to dispose closure on `items` state
useEffect(() => {
itemsRef.current = items;
}, [items]);
// Call once on mount
useEffect(() => {
fetchItems();
}, [fetchItems]);
// Make an interval
useEffect(() => {
const id = setInterval(fetchItems, ONE_MINUTE);
return () => {
clearInterval(id);
};
}, [fetchItems]);
};
Try setting the initial state with a function
Const [foo, setFoo] = useState(() => ‘foo’)
Your useCallback for that state instance probably runs once, correct me if I’m wrong, so if you set a function for useState it will only run once, consider is a component did mount but no update.
Maybe this is practice but I have never called something like [onError] without setting an error state, because that’s what I think recognizes it.
So React is this great thing that renders certain components etc. UseEffect is great, I personally don’t use it to change the state, that for me is usually done in the JSX.
I would do like a onClick handler with the UseState method and watch for changes there. Instead your running a function that runs a setState event and watches for an event.
Let me know if that works or not if you need any explanation.
As I said in the comments, onError is triggering a re-render on the parent and therefore the children will also render again.
Someone suggested removing onError from the useCallback array of dependencies. Although it might work, it is considered a bad practice because can lead to memory-leak. Have you tried to remove the useCallback wrap around your function?