React hooks: why does useEffect need an exhaustive array of dependencies? - javascript

I've got the following use case in a React component.
It is a search user input that uses React Autosuggest. Its value is always an ID, so I only have the user ID as a prop. Therefore at first load to show the username value, I need to fetch it at first mount.
EDIT: I don't want to fetch the value again when it changes later, because I already have the value from my suggestions request.
type InputUserProps = {
userID?: string;
onChange: (userID: string) => void;
};
// Input User is a controlled input
const InputUser: React.FC<InputUserProps> = (props) => {
const [username, setUsername] = useState<string | null>(null);
useEffect(() => {
if (props.userID && !username) {
fetchUsername(props.userID).then((username) => setUsername(username));
}
}, []);
async function loadSuggestions(inputValue: string): Suggestion[] {
return fetchSuggestions(inputValue);
}
function selectSuggestion(suggestion: Suggestion): void {
props.onChange(suggestion.userID); // I don't want to rerun the effect from that change...
setUsername(suggestion.username); // ...because I set the username here
}
return (
<InputSuggest
value={props.userID}
label={username}
onSuggestionsRequested={loadSuggestions}
onSuggestionSelected={selectSuggestion}
/>
);
};
export default InputUser;
(I'm adding a simplified way this component is called)
const App: React.FC<AppProps> = (props) => {
// simplified, I use react-hook-form in the real code
const [userID, setUserID] = useState<string?>(any_initial_value_or_null);
return <InputUser userID={userID} onChange={(newValue)=>setUserID(newValue)} />
};
export default App;
It works, but I have the following warning on my useEffect hook
React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)
But if I do so, I will run it more than once as the username value is changed by the hook itself! As it works without all the dependencies, I'm wondering:
How can I solve my case cleanly?
What are those dependencies for and why is it advised to be exhaustive with them?

Looks like the userId should indeed be a dependency, since if it changes you want to run your query again.
I think you can drop the check for username and always fetch when, and only when the userId changes:
useEffect(() => {
if(props.userID) {
fetchUsername(props.userID)
.then((username) => setUsername(username))
}
}, [props.userID])
Generally speaking, you want to list all closure variables in your effect to avoid stale references when the effect is executed.
-- EDIT to address OP questions:
Since in your use case you know you only want to perform the action on the initial mount passing an empty dependency array is a valid approach.
Another option is to keep track of the fetched userID, e.g.
const fetchedIds = useRef(new Set())
whenever you fetch username for a new ID you can update the ref:
fetchedIds.current.add(newId)
and in your effect you can test:
if (props.userID && !fetchedIds.current.has(props.userID)) {
// do the fetch
}

What are those depdendencies?
useEffect takes an optional second argument which is an array of dependencies. Dependencies of the useEffect hook tell it to run the effect whenever one if its dependency changes.
If you don't pass an optional second argument to useEffect hook, it will execute every time the component re-renders. Empty dependency array specifies that you want to run the effect only once, after the initial render of the component. In this case, useEffect hook will almost behave like componentDidMount in class components.
why is it advised to be exhaustive on them ?
Effects see props and state from the render they were defined in. So when you use something from the scope of functional component that participates in react's data flow like props or state, inside the callback function of useEffect hook, that callback function function will close over that data and unless new effect is defined with the new values of props and state, your effect will see the stale props and state values.
Following code snippet demonstrates what could go wrong if you lie about the dependencies of useEffect hook.
In the following code snippet, there are two components, App and User. App component has three buttons and maintains the id of the user which is displayed by the User component. User id is passed as a prop from App to User component and User component fetches the user with the id, passed as prop, from the jsonplaceholder API.
Now the problem in the following code snippet is that it doesn't works correctly. Reason is that it lies about the dependency of the useEffect hook. useEffect hook depends on userID prop to fetch the user from the API but as i skipped adding userID as a dependency, useEffect hook doesn't executes every time userID prop changes.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, []);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Above code snippet shows one of the few problems that could arise if you lie about the dependencies and this is why you must not skip or lie about the dependencies of the useEffect hook or any other hook that has a dependency array, for example useCallback hook.'
To fix the previous code snippet, you just have to add userID as a dependency to the dependency array of useEffect hook so that if executes whenever userID prop changes and new user is fetched with the id equal to userID prop.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, [userID]);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In your case, if you skip adding props.userID in the dependency array of useEffect hook, your effect will not fetch the new data when the prop userID changes.
To learn more about the negative impacts of omitting the dependencies of useEffect hook, see:
Don’t Lie to React About Dependencies
How can I solve my case cleanly ?
Since your effect depends on the prop value userID, you should include it in the dependency array to always fetch the new data whenever the userID changes.
Adding props.userID as a dependency to useEffect hook will trigger the effect every time props.userID changes but the problem is that you are unnecessarily using username inside the useEffect. You should remove it because that is not needed since username value doesn't and shouldn't decide when new user data should be fetched. You just want the effect to run whenever props.userID changes.
You could also decouple the action from the state update by using the useReducer hook to manage and update the state.
Edit
Since you only want to run the effect even when userID is used by useEffect hook, in your case it is ok to have an empty array as the second argument and ignore the eslint warning. You could also not omit any dependencies of the useEffect hook and use some condition in useEffect hook that evaluates to false after the effect runs for the first time and updates the state.
Personally i would suggest you to try to change how your components are structured so that you don't have to deal with this kind of problem in the first place.

If you are sure that before mounting the InputUser component, all dependent props have been filled and they have the right value, then add eslint-disable-next-line react-hooks/exhaustive-deps right before }, []) line, but If depended props have no value at first component mounting, So you have to add them in useEffect dependencies.

Related

Can't update parent state from child using functional components

I am having an issue with my React app. I am trying to set the state of the parent component based on the child component's value. I can see in the dev tools and log window that the child's value is being received by the parent; however, the setState is not working as it should. I have tried creating a separate function just to set the values; hoping for it to act as a middleware but no luck.
I have been through about a couple of StackOverflow threads but not many cater for functional components. I found the following codegrepper snippet for reference but it does not help either.
link: https://www.codegrepper.com/code-examples/javascript/react+function+component+state
Most of the threads deal with how to get the value to the parent component; however, my issue is more "setting the state" specific.
import React, { useEffect, useState } from "react";
import Character from "../component/Character";
import Filter from "../component/Filter";
import Pagination from "../component/Pagination";
import axios from "axios";
import "./Home.css";
const Home = (props) => {
const [API, setAPI] = useState(`https://someapi.com/api/character/?gender=&status=&name=`);
const [characterData, setCharacterData] = useState([]);
const [pagination, setPagination] = useState(0);
const makeNetworkRequest = (data) => {
setAPI(data);
setTimeout(() => {
axios.get(data).then(resp => {
setPagination(resp.data.info)
setCharacterData(resp.data.results)
})
}, 1000)
}
const handleFormCallBack = (childData) => {
setAPI(childData);
makeNetworkRequest(API);
console.log(`Parent handler data ${childData}`)
console.log(`Parent handler API ${API}`)
}
useEffect(() => {
makeNetworkRequest(API)
}, [characterData.length]);
const mappedCharacters = characterData.length > 0 ? characterData.map((character) => <Character key={character.id} id={character.id} alive={character.status} /* status={<DeadOrAlive deadoralive={character.status} /> }*/ gender={character.gender} name={character.name} image={character.image} />) : <h4>Loading...</h4>
return (
<div className="home-container">
<h3>Home</h3>
<Filter parentCallBack={handleFormCallBack} />
<div className="characters-container">
{mappedCharacters}
</div>
{/* <Pagination pages={pagination.pages}/> */}
</div>
)
}
export default Home;
In the code above I am using a callback function on the parent named "handleFormCallBack", mentioned again below to get the information from the child filter component. When I log the value, the following results are being generated.
const handleFormCallBack = (childData) => {
setAPI(childData);
makeNetworkRequest(API);
console.log(`Parent handler data ${childData}`)
// Parent handler data https://someapi.com/api/character/?gender=&status=&name=charactername
console.log(`Parent handler API ${API}`)
// Parent handler API https://someapi.com/api/character/?gender=&status=&name=
}
I am not sure what I am doing wrong but any sort of help would be much appreciated.
Kind Regards
useState works pretty much like setState and it is not synchronous, so when you set the new value using setAPI(childData); react is still changing the state and before it actually does so both of your console.log() statements are being executed.
Solution - after setting the new value you need to track if it has changed, so use a useEffect hook for the endpoint url and then when it changes do what you want.
useEffect(() =< {
// do anything you want to here when the API value changes. you can also add if conditions inside here.
}, [API])
Just to check what I have explained, after calling setAPI(childData); add a setTimeout like
setTimeout(() => {
// you will get new values here. this is just to make my point clear
console.log(Parent handler data ${childData})
console.log(Parent handler API ${API})
}, 5000);

useEffect causing it to call the method to get posts way too many times. I only want to get the posts when my query changes

I am trying to call the reddit API. The post titles are showing up, but I want them to rerender when my query changes. I just want to know how to call a method when a piece of my state changes(aka my query). I’m using useEffect from react to do it but that calls it whenever anything changes in the component, causing it to call the method to get posts way to many times. I only want to get the posts when my query changes.
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
function Results()
{
const query = useSelector(state => state.query);
const results = useSelector(state => state.results);
const dispatch = useDispatch();
let fetchResults = () =>
{
let postTitles = [];
let postSrcs = [];
fetch('https://www.reddit.com/r/' + query + '.json')
.then(response => response.json())
.then(body => {
for (let i = 0; i < body.data.children.length; ++i) {
if (body.data.children[i].data.post_hint === 'image')
{
let img_url = body.data.children[i].data.url_overridden_by_dest;
postSrcs.push(img_url);
}
let title = body.data.children[i].data.title;
postTitles.push(title);
}
dispatch({type: "QUERY_RESULTS", payload: postTitles})
}).catch((err) => {
console.log(err)
});
}
useEffect(() => {
fetchResults();
console.log("use effect triggered")
})
return (
<>
<h1>Query: {query}</h1>
{ !results
? <h1>No Results</h1>
: results.map(p => <h6> {p} </h6>)
}
</>
)
}
export default Results;
For example in the console log that tells me when use effect is triggered. and when i search for a post the use effect triggered is stacking up.
useEffect has a differents mode. You can check how to use in official document https://reactjs.org/docs/hooks-reference.html#useeffect
So the main you must know is 3 things
useEffect is the last render in react. So first render a components and read other code when it finish useEffect run.
useEffect may run code only one time adding []. for example
useEffect ( () => {
...code
}, [])
This code will run only one time.
useEffect may run code watching variables adding variables into []. For example
useEffect ( () => {
...code
}, [ count, name , ... ])
This code will run first time and later would run if count or name change
To achieve that you need to prevent useEffect to be called on any changes, and only once the query changes.
NOTE: Since you're using dispatch within fetchResults, it's better to make sure that dispatch is ready before calling fetchResults.
Your useEffect may look like the following to achieve that:
useEffect(() => {
// To prevent call fetchResults if dispatch only is changed
if (query) {
fetchResults();
console.log("use effect triggered");
}
}, [dispatch, query]);
Hooks like useEffect are used in function components. The Class component comparison to useEffect are the methods componentDidMount, componentDidUpdate, and componentWillUnmount.
useEffect will run when the component renders, which might be more times than you think.
So useEffect takes a second parameter
The second param is an array of variables that the component will check to make sure changed before re-rendering. You could put whatever bits of props and state you want in here to check against.
In your case add [query] as a second para:
useEffect(() => {
fetchResults();
console.log("use effect triggered")
},[query])
https://css-tricks.com/run-useeffect-only-once/

Error "Maximum update depth exceeded. This can happen when a component calls setState inside useEffect"

I'm facing this error (repeats thousands of times until page crashes) whenever my Cart component (also shown below) is called:
index.js:1 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
in Cart (created by Context.Consumer)
in Route (at Routes.js:24)
in Switch (at Routes.js:23)
in Router (created by BrowserRouter)
in BrowserRouter (at Routes.js:22)
in Routes (at src/index.js:5)
My Cart component:
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import Layout from "./Layout";
import { getCart } from "./cartHelpers";
import Card from "./Card";
import Checkout from "./Checkout";
const Cart = () => {
const [items, setItems] = useState([]);
useEffect(() => {
setItems(getCart());
}, [items]);
const showItems = items => {
return (
<div>
<h2>Your cart has {`${items.length}`} items</h2>
<hr />
{items.map((product, i) => (
<Card
key={i}
product={product}
showAddToCartButton={false}
cartUpdate={true}
showRemoveProductButton={true}
/>
))}
</div>
);
};
const noItemsMessage = () => (
<h2>
Your cart is empty. <br /> <Link to="/shop">Continue shopping</Link>
</h2>
);
return (
<Layout
className="container-fluid"
>
<div className="row">
<div className="col-6">
{items.length > 0 ? showItems(items) : noItemsMessage()}
</div>
<div className="col-6">
<h2 className="mb-4">Your cart summary</h2>
<hr />
<Checkout products={items} />
</div>
</div>
</Layout>
);
};
export default Cart;
useEffect is calling getCart() (shown below):
export const getCart = () => {
if (typeof window !== "undefined") {
if (localStorage.getItem("cart")) {
return JSON.parse(localStorage.getItem("cart"));
}
}
return [];
};
I intend for getCart to grab the cart from the localStorage and populate that in the state variable items or return an empty array [].
From my understanding, useEffect changes the page whenever the state changes and when there is any item in the dependency array it will be based on that.
Well I cant figure out why this error is happening.
I really tried understanding this error can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render and here's what I learned so far trying to solve this:
items, the dependency, doesn't change much. Testing adding 1 item or 0 items results in thousands of the same error
Removing useEffect stops the error entirely but defeats the purpose of tying it to the items state variable
How can I stop this error?
I just want this component to work without freezing and throwing thousands of this error upon render.
The reason is with your useEffect dependencies:
useEffect(() => {
setItems(getCart());
}, [items]);
The problem is that you are passing [items] and calling useEffect while items changed. And inside useEffect you are changing items.
Make sure if you return each time new object or array from getItems() react don't know that your objects are the same and calling effect again and again.
Solution
Remove items from dependencies
useEffect(() => {
setItems(getCart());
}, []);
Or if you need to access current state while changing it:
useEffect(() => {
setItems(currentItems => getCart(currentItems));
}, []);
Problem is here:
useEffect(() => {
setItems(getCart());
}, [items]);
You have added a dependency with items. So that means whenever items changes, run setItems(getCart());
But when you set your items, that means the items change which in turn triggers the useEffect again and hence the infinite loop.
Solution:
useEffect(() => {
setItems(getCart());
}, []);
Remove the dependency. This means the useEffect will run as soon as the component is mounted and executed once only. If you want this to run for different reasons, you will need to add that to the dependency array.
You should call setItems after getCard Promiss return or you can also use callback instead of promiss.
First you need need to take result and after that call setItems. It will resolve you problem.

React Warning: Cannot update a component from inside the function body of a different component

I am using Redux with Class Components in React. Having the below two states in Redux store.
{ spinner: false, refresh: false }
In Parent Components, I have a dispatch function to change this states.
class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
this.props.onShowSpinner();
this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
In Child Component, I am trying to reload the parent component like below.
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
if (somecondition) {
// doing some redux store update
props.reloadApp();
}
}
render() {
return <button />;
}
}
I am getting error as below.
Warning: Cannot update a component from inside the function body of a
different component.
How to remove this warning? What I am doing wrong here?
For me I was dispatching to my redux store in a React Hook. I had to dispatch in a useEffect to properly sync with the React render cycle:
export const useOrderbookSubscription = marketId => {
const { data, error, loading } = useSubscription(ORDERBOOK_SUBSCRIPTION, {
variables: {
marketId,
},
})
const formattedData = useMemo(() => {
// DISPATCHING HERE CAUSED THE WARNING
}, [data])
// DISPATCHING HERE CAUSED THE WARNING TOO
// Note: Dispatching to the store has to be done in a useEffect so that React
// can sync the update with the render cycle otherwise it causes the message:
// `Warning: Cannot update a component from inside the function body of a different component.`
useEffect(() => {
orderbookStore.dispatch(setOrderbookData(formattedData))
}, [formattedData])
return { data: formattedData, error, loading }
}
If your code calls a function in a parent component upon a condition being met like this:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
Try wrapping the condition in a useEffect:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
useEffect(() => {
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
}, [data, handleNoUsersLoaded]);
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
It seems that you have latest build of React#16.13.x. You can find more details about it here. It is specified that you should not setState of another component from other component.
from the docs:
It is supported to call setState during render, but only for the same component. If you call setState during a render on a different component, you will now see a warning:
Warning: Cannot update a component from inside the function body of a different component.
This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the setState call into useEffect.
Coming to the actual question.
I think there is no need of getDerivedStateFromProps in the child component body. If you want to trigger the bound event. Then you can call it via the onClick of the Child component as i can see it is a <button/>.
class Child extends React.Component {
constructor(props){
super(props);
this.updateState = this.updateState.bind(this);
}
updateState() { // call this onClick to trigger the update
if (somecondition) {
// doing some redux store update
this.props.reloadApp();
}
}
render() {
return <button onClick={this.updateState} />;
}
}
Same error but different scenario
tl;dr wrapping state update in setTimeout fixes it.
This scenarios was causing the issue which IMO is a valid use case.
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setSomeState(someNewValue);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
fix
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setTimeout(() => {
setSomeState(someNewValue);
}, 0);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
In my case I had missed the arrow function ()=>{}
Instead of onDismiss={()=>{/*do something*/}}
I had it as onDismiss={/*do something*/}
I had same issue after upgrading react and react native, i just solved that issue by putting my props.navigation.setOptions to in useEffect. If someone is facing same problen that i had i just want to suggest him put your state changing or whatever inside useEffect
Commented some lines of code, but this issue is solvable :) This warnings occur because you are synchronously calling reloadApp inside other class, defer the call to componentDidMount().
import React from "react";
export default class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
// this.props.onShowSpinner();
// this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
// if (somecondition) {
// doing some redux store update
props.reloadApp();
// }
}
componentDidMount(props) {
if (props) {
props.reloadApp();
}
}
render() {
return <h1>This is a child.</h1>;
}
}
I got this error using redux to hold swiperIndex with react-native-swiper
Fixed it by putting changeSwiperIndex into a timeout
I got the following for a react native project while calling navigation between screens.
Warning: Cannot update a component from inside the function body of a different component.
I thought it was because I was using TouchableOpacity. This is not an issue of using Pressable, Button, or TouchableOpacity. When I got the error message my code for calling the ChatRoom screen from the home screen was the following:
const HomeScreen = ({navigation}) => {
return (<View> <Button title = {'Chats'} onPress = { navigation.navigate('ChatRoom')} <View>) }
The resulting behavior was that the code gave out that warning and I couldn't go back to the previous HomeScreen and reuse the button to navigate to the ChatRoom. The solution to that was doing the onPress in an inline anonymous function.
onPress{ () => navigation.navigate('ChatRoom')}
instead of the previous
onPress{ navigation.navigate('ChatRoom')}
so now as expected behavior, I can go from Home to ChatRoom and back again with a reusable button.
PS: 1st answer ever in StackOverflow. Still learning community etiquette. Let me know what I can improve in answering better. Thanx
If you want to invoke some function passed as props automatically from child component then best place is componentDidMount lifecycle methods in case of class components or useEffect hooks in case of functional components as at this point component is fully created and also mounted.
I was running into this problem writing a filter component with a few text boxes that allows the user to limit the items in a list within another component. I was tracking my filtered items in Redux state. This solution is essentially that of #Rajnikant; with some sample code.
I received the warning because of following. Note the props.setFilteredItems in the render function.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- Danger! Updates Redux during a render!
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I ran this code with React 16.12.0, I received the warning listed in the topic of this thread in my browser console. Based on the stack trace, the offending line was my props.setFilteredItems invocation within the render function. So I simply enclosed the filter invocations and state change in a useEffect as below.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
useEffect(() => {
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- OK now; effect runs outside of render.
}, [nameFilter, cityFilter]);
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I first added the useEffect I blew the top off the stack since every invocation of useEffect caused state change. I had to add an array of skipping effects so that the effect only ran when the filter fields themselves changed.
I suggest looking at video below. As the warning in the OP's question suggests, there's a change detection issue with the parent (Parent) attempting to update one child's (Child 2) attribute prematurely as the result of another sibling child's (Child 1) callback to the parent. For me, Child 2 was prematurely/incorrectly calling the passed in Parent callback thus throwing the warning.
Note, this commuincation workflow is only an option. I personally prefer exchange and update of data between components via a shared Redux store. However, sometimes it's overkill. The video suggests a clean alternative where the children are 'dumb' and only converse via props mand callbacks.
Also note, If the callback is invoked on an Child 1 'event' like a button click it'll work since, by then, the children have been updated. No need for timeouts, useEffects, etc. UseState will suffice for this narrow scenario.
Here's the link (thanks Masoud):
https://www.youtube.com/watch?v=Qf68sssXPtM
In react native, if you change the state yourself in the code using a hot-reload I found out I get this error, but using a button to change the state made the error go away.
However wrapping my useEffect content in a :
setTimeout(() => {
//....
}, 0);
Worked even for hot-reloading but I don't want a stupid setTimeout for no reason so I removed it and found out changing it via code works just fine!
I was updating state in multiple child components simultaneously which was causing unexpected behavior. replacing useState with useRef hook worked for me.
Try to use setTimeout,when I call props.showNotification without setTimeout, this error appear, maybe everything run inTime in life circle, UI cannot update.
const showNotifyTimeout = setTimeout(() => {
this.props.showNotification();
clearTimeout(showNotifyTimeout);
}, 100);

How to set initial state value for useState Hook in jest and enzyme?

I know this question is already asked here: How to set initial state for useState Hook in jest and enzyme?
const [state, setState] = useState([]);
And I totally agree with Jimmy's Answer to mock the useState function from test file but I have some extended version of this question, "What if I have multiple useState statements into the hooks, How can I test them and assign the respective custom values to them?"
I have some JSX rendering with the condition of hook's state values and depending on the values of that state the JSX is rendering.
How Can I test those JSX by getting them into the wrapper of my test case code?
Upon the answer you linked, you can return different values for each call of a mock function:
let myMock = jest.fn();
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
In my opinion this is still brittle. You may modify the component and add another state later, and you would get confusing results.
Another way to test a React component is to test it like a user would by clicking things and setting values on inputs. This would fire the event handlers of the component, and React would update the state just as in real configuration. You may not be able to do shallow rendering though, or you may need to mock the child components.
If you prefer shallow rendering, maybe read initial state values from props like this:
function FooComponent({initialStateValue}) {
const [state, setState] = useState(initialStateValue ?? []);
}
If you don't really need to test state but the effects state has on children, you should (as some comments mention) just then test the side effect and treat the state as an opaque implementation detail. This will work if your are not using shallow rendering or not doing some kind of async initialization like fetching data in an effect or something otherwise it could require more work.
If you cant do the above, you might consider removing state completely out of the component and make it completely functional. That way any state you need, you can just inject it into the component. You could wrap all of the hooks into a 'controller' type hook that encapsulates the behavior of a component or domain. Here is an example of how you might do that with a <Todos />. This code is 0% tested, its just written to show a concept.
const useTodos = (state = {}) => {
const [todos, setTodos] = useState(state.todos);
const id = useRef(Date.now());
const addTodo = useCallback((task) => {
setTodos((current) => [...current, { id: id.current++, completed: false, task }]);
}, []);
const removeTodo = useCallback((id) => {
setTodos((current) => current.filter((t) => t.id !== id));
}, []);
const completeTodo = useCallback((id) => {
setTodos((current) => current.map((t) => {
let next = t;
if (t.id === id) {
next = { ...t, completed: true };
}
return next;
}))
}, []);
return { todos, addTodo, removeTodo, completeTodo };
};
const Todos = (props) => {
const { todos, onAdd, onRemove, onComplete } = props;
const onSubmit = (e) => {
e.preventDefault();
onAdd({ task: e.currentTarget.elements['todo'].value });
}
return (
<div classname="todos">
<ul className="todos-list">
{todos.map((todo) => (
<Todo key={todo.id} onRemove={onRemove} onComplete={onComplete} />
)}
</ul>
<form className="todos-form" onSubmit={onSubmit}>
<input name="todo" />
<button>Add</button>
</form>
</div>
);
};
So now the parent component injects <Todos /> with todos and callbacks. This is useful for testing, SSR, ect. Your component can just take a list of todos and some handlers, and more importantly you can trivially test both. For <Todos /> you can pass mocks and known state and for useTodos you would just call the methods and make sure the state reflects what is expected.
You might be thinking This moves the problem up a level and it does, but you can now test the component/logic and increase test coverage. The glue layer would require minimal if any testing around this component, unless you really wanted to make sure props are passed into the component.

Categories

Resources