how update the value of useEffect() hook on button click - javascript

I have a functional component and I have created a button inside it. I am also using a "Use_effect()" hook. My main is to re-render the functional component, update the use_effect() hook when the button is clicked.
const Emp_list = (props) => {
useEffect(() => {
props.getList(props.state.emp);
}, []);
return (
<div>
{props.state.emp.map((val ) =>
{val.feature_code}
{val.group_code}
<button onClick = {() => props.removeEmpFromList(val.feature_code)} > Remove </button>
<EmpForm empList={props.state.emp}
onChangeText = {props.onChangeText}
/>
</div>
<button onClick= {() => props.getdata (props.state)}>Get Names</button>
<p>
</div>
);
};
export default Emp_list;
removeEmpFromList = (i) => {
const remaining = this.state.emp( c => c.feature_code !== i)
this.setState({
emp: [...remaining]
})
}
When I click the Remove button , it will basically remove the employee from the list. The function removeEmpFromList will update the state.
The functional component EmpForm basically shows the list of all employees.
So I want to re-render the page so that, it updates the state value in useEffect() hook. So when EmpForm is called again on re-rending it shows the updated list.

You didn't provide the code for removeEmpFromList() ... but probably it updates the state by mutation therefor component gets the same object ref - compared shallowly - no difference, no reason to rerender.
Modify removeEmpFromList() method to create a new object for emp - f.e. using .filter.
If not above then passing entire state is the source of problem (the same reason as above).
Simply pass only emp as prop or use functions in setState() (to return a new object for the entire state) this way.

I figured it out! Thanks for the help guys.
So, it was not re-rendering because initally, useEffect() second parameter was [] , if you change it to props.state then it will update the changes made to the state and re-render the component automatically.
useEffect(() => {
props.getList(props.state.emp);
}, [props.state.emp]);

Related

Re-rendering on key-value pair object components

I want to avoid re-render of my child component <ChildComponent/> whenever I update my state using a onClick in <ChildComponent/>.
I have my callback function in <ParentComponent/> which updates one of the values for the key-value pair object.
In the parent component
const _keyValueObject = useMemo(() => utilityFunction(array, object), [array, object])
const [keyValueObject, setKeyValueObject] = useState<SomeTransport>(_keyValueObject)
const handleStateChange = useCallback((id: number) => {
setKeyValueObject(keyValueObject => {
const temp = { ... keyValueObject }
keyValueObject[id].isChecked = ! keyValueObject[id].isChecked
return temp
})
}, [])
return(
<Container>
{!! keyValueObject &&
Object.values(keyValueObject).map(value => (
<ValueItem
key={value.id}
category={value}
handleStateChange ={handleStateChange}
/>
))}
</Container>
)
In child component ValueItem
const clickHandler = useCallback(
event => {
event.preventDefault()
event.stopPropagation()
handleStateChange(value.id)
},
[handleStateChange, value.id],
)
return (
<Container>
<CheckBox checked={value.isChecked} onClick={clickHandler}>
{value.isChecked && <Icon as={CheckboxCheckedIcon as AnyStyledComponent} />}
</CheckBox>
<CategoryItem key={value.id}>{value.title}</CategoryItem>
</Container>
)
export default ValueItem
In child component if I use export default memo(ValueItem), then the checkbox does not get updated on the click.
What I need now is to not re-render every child component, but keeping in mind that the checkbox works. Any suggestions?
Spreading (const temp = { ... keyValueObject }) doesn't deep clone the object as you might think. So while keyValueObject will have a new reference, it's object values will not be cloned, so will have the same reference, so memo will think nothing changes when comparing the category prop.
Solution: make sure you create a new value for the keyValueObject's id which you want to update. Example: setKeyValueObject(keyValueObject => ({...keyValueObject, [id]: {...keyValueObject[id], isChecked: !keyValueObject[id].isChecked})). Now keyValueObject[id] is a new object/reference, so memo will see that and render your component. It will not render the other children since their references stay the same.
Working Codesandbox
Explanation
What you need to do is wrap the child with React.memo. This way you ensure that Child is memoized and doesn't re-render unnecessarily. However, that is not enough.
In parent, handleStateChange is getting a new reference on every render, therefore it makes the parent render. If the parent renders, all the children will re-render. Wrapping the handleStateChange with useCallback makes sure react component remembers the reference to the function. And memo remembers the result for Child.
Useful resource

React functional components in Array.map are always rerendering when getting passed a function as props

I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState and passes it as props to the childs. Additionally the onClick function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
... useCallback to make react see that the onClick function always stays the same
... React.Fragment to make react find a component again because otherwise a child would not have a unique id or sth similar
The child component is exported as:
export default React.memo(TheChildComponent, compareEquality) with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality is never executed and therefore I know that compareEquality is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick function and by doing that updated the isSelected state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick function every render, because useCallback isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo, this should then prevent the children from re-rendering except when isSelected changes. (Your second argument to React.memo should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map).
Turns out the only problem was neither useCallback, useMemo or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.

React - Failing to give the same state to 2 elements with the same id (card and modal)

I am rendering some cards and upon clicking a card, I have the corresponding modal. Both the card and its modal have a heart icon and I want to mark both as "favorite" when either of them is clicked. I have a "favoriteBeers" array, where I want to push the favorite beers. I also have another piece of state named "favorite" and this one is boolean. The issue is that this state seems to be reversed (it is false when it's supposed to be true and the other way around). Also, only one item seems to be in the favorites array, no matter how may items I try to set as favorite.
I have lifted the array state on the root component, this is the piece of code that lies there:
const [favoriteBeers, setFavoriteBeers] = useState([]);
const handleSetFavorite = id => {
setFavoriteBeers([...favoriteBeers, beers.find(beer => beer.id === id)]);
};
const handleRemoveFavorite = id => {
setFavoriteBeers(favoriteBeers.filter(beer => beer.id !== id));
};
I also have one component for the card, and one for its modal. I have an identical function in both components:
const [isFavorite, setIsFavorite] = useState(false);
if (!isFavorite) {
setIsFavorite(true);
handleSetFavorite(id);
} else {
setIsFavorite(false);
handleRemoveFavorite(id);
}
};
//the icon that calls the function
<IconButton aria-label='add to favorites'>
{!isFavorite ? (
<FavoriteBorderIcon
onClickCapture={e => handleIconClick(e, beer.id)}
/>
) : (
<FavoriteIcon onClickCapture={e => handleIconClick(e, beer.id)} />
)}
</IconButton>
I have also prepared a codesandbox codesandbox with the components, thanks in advance
One problem is that in <Home> your isFavorite prop is undefined since <App> isn't passing such a thing. Also you're not using this prop value (currently undefined) to initialize the <BeerCard> or <BeerCardExpanded> components.
Second problem - the code below is only updating a components own isFavorite state and calling handleSetFavorite, since each component has it's own local isFavorite state.
if (!isFavorite) {
setIsFavorite(true);
handleSetFavorite(id);
}
For example when <BeerCard> flips it's isFavorite state, <BeerCardExpanded> doesn't. So I've removed these local states and directly used props.isFavorite instead.
Here is updated sandbox
I've added this method in <Home>:
const isFavorite = (beer, favoriteBeers) => {
return favoriteBeers.includes(beer);
};
Which is used to pass proper boolean value for the same for isFavorite prop of both <BeerCard> (isFavorite={isFavorite(beer, favoriteBeers)})
And <BeerCardExpanded> (isFavorite={isFavorite(isClicked, favoriteBeers)}) components.
I've used rest params syntax ...props just to avoid renaming isFavorite prop and have minimal changes. You can improvise the whole thing.

Re-render component once on both With and Without Update of Props - REACT HOOKS

Is it possible to force re-render component once with or without any changes in props..
const SearchGenresResult = ({ Id }) => .....
useEffect(() => {
callUrl(url);
setTheNewInputValue(Id);
}, [Id]);
Explanation :
5 images with Id as 1, 2, 3, 4 and 5
On click of any images, the id is passed as a prop.
On receiving the Id, details of that image is displayed on a modal.
All is working except clicking same image two times.
Though the id passed as a prop but the useEffect is not
triggered as the value of previous and current prop remains same. Hence
the modal is not displayed.
How to solve this issue..
Tried using const forceUpdate = useState()[1] but no luck..
Well instead of passing id as a string you can pass an object with id as a key and add that as a reference to useEffect dependency.
In such a case whenever you setState for id in parent, a new reference of the object is created and hence the useEffect check breaks causing it to run again
const SearchGenresResult = ({ stateId }) => .....
useEffect(() => {
callUrl(url);
setTheNewInputValue(stateId.Id);
}, [stateId]);
...
}
and in Parent
...
const [stateId, setStateId] = useState({});
..
function fn(someId) {
setStateId({id: someId});
}
...
<SearchGenresResult stateId={stateId} />
However if you passing the sameId to child, you shouldn't actually be needed a re-render

Parent component unnecessarily re-rendering child on parent state change

I am creating a simple Magic The Gathering search engine. The vision is to have a list of search results, and when a search result is clicked the main display renders extended information about the card selected.
You can see it here
The top level App component contains the state of what card is to be displayed and the ScrollView component maintains the state of the card selected for only the highlighting of the selected card in the list. I propagate down the setDisplayCard handler so that when a card is clicked in the list, I can set the display card as a callback.
function App(props) {
const [displayCard, setDisplayCard] = useState(null)
return (
<div className="App">
<SearchDisplay handleCardSelect={setDisplayCard}/>
<CardDisplay card={displayCard} />
</div>
);
}
function SearchDisplay({handleCardSelect}) {
const [cards, setCards] = useState([]);
useEffect(() => {
(async () => {
const cards = await testCardSearch();
setCards(cards);
})();
}, []);
async function handleSearch(searchTerm) {
const searchCards = await cardSearch({name: searchTerm});
setCards(searchCards)
};
return (
<StyledDiv>
<SearchBar
handleSubmit={handleSearch}
/>
<ScrollView
handleCardSelect={handleCardSelect}
cards={cards}
/>
</StyledDiv>
);
}
function ScrollView({cards, handleCardSelect}) {
const [selected, setSelected] = useState(null);
return (
<ViewContainer>
{cards.map((card, idx) =>
<li
key={idx}
style={selected === idx ? {backgroundColor: "red"} : {backgroundColor: "blue"}}
onClick={() => {
setSelected(idx);
handleCardSelect(card);
}}
>
<Card card={card} />
</li>
)}
</ViewContainer>
);
}
The issue I am having is that calling setDisplayCard re-renders my ScrollView and eliminates its local state of the card that was selected so I am unable to highlight the active card in the list. Based on my understanding of react, I don't see why ScrollView re-renders as it does not depend on the state of displayCard. And I am not sure what approach to take to fix it. When I click on a card in the list, I expect it to highlight red.
A child component's render method will always be called, once its parent's render method is invoked. The same goes for if its props or state change.
Since you're using functional components, you could use the React.memo HOC to prevent unnecessary component re-renders.
React.memo acts similar to a PureComponent and will shallowly compare ScrollView's old props to the new props and only trigger a re-render if they're unequal:
export default React.memo(ScrollView);
React.memo also has a second argument, which gives you control over the comparison:
function areEqual(prevProps, nextProps) {
// only update if a card was added or removed
return prevProps.cards.length === nextProps.cards.length;
}
export default React.memo(ScrollView, areEqual);
If you were to use class-based components, you could use the shouldComponentUpdate life cycle method as well.
By default (stateless) components re-render under 3 conditions
It's props have changed
It's state has changed
It's parent re-renders
This behavior can be changed using either shouldComponentUpdate for components or memo for stateless-components.
// If this function returns true, the component won't rerender
areEqual((prevProps, nextProps) => prevProps.cards === nextProps.card)
export default React.memo(ScrollView, areEqual);
However I don't think this is your problem. You are using an array Index idx as your element key which can often lead to unexpected behavior.
Try to remove key={idx} and check if this fixes your issue.
So your App component is supposed to hold the state of the card the user clicked? Right now your App component is stateless. It's a functional component. Try converting it to a class component with an initial, and maintained, state.
What is the logic of your setDisplayCard()?
I've heard that in React 16? there is something like 'useState()' and 'hooks', but I'm not familiar with it.
This person seemed to be having a similar problem,
React functional component using state

Categories

Resources