How I do it right now
I have a list of items. Right now, when the user presses button X, shouldShowItem is toggled. shouldShowItem ultimately lies in redux and is passed down into Item as a prop. it's either true or false. When it changes, toggleDisplay is called and changes state in my hook:
useEffect(() => {
toggleDisplay(!display); //this is just a useState hook call
}, [shouldShowItem]); //PS: I'm aware that I don't need this extra step here, but my actual code is a bit more complicated, so I just simplified it here.
My Problem is, that I have one single shouldShowItem property in redux, not one shouldShowItem for each item. I don't want to move this property into the redux-state for each and every item.
Problem:
The problem with my construction however is that shouldShowItem is being saved, which means that if I toggle it at time X for item Y, and then my Item Z also re-renders as a result of an unrelated event, it will re-render with an updated shouldShowItem state, - although that state change was intended for Item X.
Essentially, I am saving the state of shouldShowItem in redux while I just need a toggle, that I can dispatch once, that works on the current Item, and then isn't read / needed anymore. I want to basically dispatch a toggle, - I don't care about the state of each item within redux, I just care that it's toggled once.
Suggestions?
Edit: More Code
Item:
const Item = ({shouldShowItem, itemText, updateCurrentItem})=>
{
const [display, toggleDisplay] = useState(false);
useEffect(() => {
if (isSelected) {
toggleDisplay(!display);
}
}, [shouldShowItem]);
return (
<div
onClick={() => {
toggleDisplay(!display);
updateCurrentItem(item._id);
}}
>
{display && <span>{itemText}</span>}
</div>
);
}
Mapping through item list:
allItems.map((item, i) => {
return (
<Item
key={i}
shouldShowItem={this.props.shouldShowItem}
itemText={item.text}
updateCurrentItem={this.props.updateCurrentItem}
);
});
The props here come from redux, so shouldShowItem is a boolean value that lies in redux, and updateCurrentItem is an action creator. And well, in redux i simply just toggle the shouldShowItem true & false whenever the toggle action is dispatched. (The toggle action that sets & unsets true/false of shouldShowItem is in some other component and works fine)
instead of a boolean shouldShowItem, why not convert it into an object of ids with boolean values:
const [showItem, setShowItem] = useState({id_1: false, id_2: false})
const toggleDisplay = id => {
setShowItem({...showItem, [id]: !showItem[id]})
updateCurrentItem(id)
}
allItems.map((item, i) => {
return (
<Item
key={i}
shouldShowItem={showItem[item._id]}
itemText={item.text}
updateCurrentItem={toggleDisplay}
);
});
const Item = ({shouldShowItem, itemText, updateCurrentItem}) => {
return (
<div
onClick={() => toggleDisplay(item._id)}
>
{shouldShowItem && <span>{itemText}</span>}
</div>
)
}
I have a react component that uses hooks for state. I have set the initial state for home to {location:null, canCharge: 'yes'}.
I then have a couple of subcomponents that call setHome() to update the pieces of the state they are responsible for.
One sets the location, and the other sets the canCharge property of the home state.
The setter for the ChargeRadioGroup works as expected, only updating the canCharge property and has no effect on the value of location.
The PlacesAutoComplete set however seems to have captured the initial state of home, and after setting a breakpoint inside, I see that it always is called with home: {location:null, canCharge:'yes'}.
I realize I could break this single state into two separate states, one for location and one for canCharge, but I'd like to understand why this is happening instead of implementing a workaround.
export default function VerticalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [home, setHome] = useState({
location: null,
canCharge: "yes"
});
const [work, setWork] = useState({
location: null,
canCharge: "yes"
});
const steps = getSteps();
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepLabel>Where do you live?</StepLabel>
<StepContent>
<Box className={classes.stepContent}>
<PlacesAutocomplete
className={classes.formElement}
name={"Home"}
onPlaceSelected={location => setHome({ ...home, location })}
googleApiKey={"<API_KEY>"}
/>
<ChargeRadioGroup
className={classes.formElement}
label="Can you charge your car here?"
value={home.canCharge}
onChange={event =>
setHome({ ...home, canCharge: event.target.value })
}
/>
The code for the PlacesAutoComplete component can be seen here
I'm guessing this has something to do with the way that this component calls it's onPlaceSelected prop, but I can't figure out exactly what's going on, or how to fix it:
useEffect(() => {
if (!loaded) return;
const config = {
types,
bounds,
fields
};
if (componentRestrictions) {
config.componentRestrictions = componentRestrictions;
}
autocomplete = new window.google.maps.places.Autocomplete(
inputRef.current,
config
);
event = autocomplete.addListener("place_changed", onSelected);
return () => event && event.remove();
}, [loaded]);
const onSelected = () => {
if (onPlaceSelected && autocomplete) {
onPlaceSelected(autocomplete.getPlace());
}
};
Updating my original answer.
Instead of this:
onPlaceSelected={location => setHome({ ...home, location })}
This:
onPlaceSelected={newlocation => setHome( (prevState) => (
{ ...prevState, location:newlocation }
))}
The set state functions can take a value, and object or a function that receives the old state and returns the new state. Because setting state is sometimes asynchronous, object state with members getting set with different calls may result in captured variables overwriting new state.
More details at this link: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
I made a todo app and I wanted to improvise it by showing all completed task if "Show completed task" is checked
Checking "Show completed task" works fine, but unchecking it doesn't execute the "else statement".
Originally what wanted is if its unchecked it should give me all the state, completed or not. The code from 'else statement' is just for debugging. It should at least give me the array[0] and array[2], tried console logging it, and it throws an undefined.
PS: tried to console log something upon unchecking, unchecked is working.
filterCompleted = (e) => {
if(e.target.checked){
this.setState({todos: [...this.state.todos.filter(todo => {return todo.completed === true})]})
}
else{
//code below just for debugging but this does not execute
this.setState({todos: [...this.state.todos.filter(todo => {return todo.completed === false})]})
}
}
This will only ever work first time, beyond that the state will only ever hold all completed tasks or all uncompleted tasks.
You should introduce a new state e.g.
constructor(prop) {
this.state = {
todos: [],
filterCompleted: false
}
}
...
filterCompleted = e => this.setState({ filterCompleted: e.target.checked })
Then when rendering the list you can simply just exclude items that aren't completed e.g.
const items = this.state
.todos
.filter(x => !this.state.filterCompleted || x.completed)
.map(x => <TodoItem {...x} />)
The Problem lies in the following line
this.setState({todos: [...this.state.todos.filter(todo => {return todo.completed === true})]})
You are just filtering the items without channging them. You have to create a new array and set the value of the item to true/false. Something like this:
let newTodos = this.state.todos
newTodos[index].checked = true
this.setState({todos: newTodos})
index being some string/int that identifies the checkbox itself.
I think you have to leave the props and set a new state variable filterCompleted and during render filter all todoitems:
// when checked:
this.setState({ filterCompleted: true/false })
// during render (with hooks better with useMemo...):
todosToReder = this.state.filterCompleted ? this.state.todos.filter(el => el.completed) : this.state.todos
I'm working on a React project, and I have a section with "Saved Games".
The "Saved Games" section maps the "Saved Games" state.
This is what it looks like:
let SavedGamesList = <h1>Loading...</h1>;
if (this.props.savedGamesState.length < 1) {
SavedGamesList = <StyledNotSavedGames>Such empty</StyledNotSavedGames>;
}
if (this.props.savedGamesState.length >= 1) {
SavedGamesList = this.props.savedGamesState.map(game => (
<GameCard
key={game.game}
title={game.title}
hoursViewed={game.hours_viewed}
saved={true}
/>
));
}
When I try to delete a game, it deletes a random one not the one I clicked, or multiple games at once.
This is what the "GameCard" (Where the delete button is) looks like:
deleteGame = () => {
let gameName = this.props.title;
this.props.deleteGame(gameName); //This is the Redux dispatch
console.log(this.props.savedGamesState);
};
And this is how I try to change the state in the Reducer:
case actionTypes.DELETE_GAME:
let updatedGames = [
...state.savedGames.splice(
state.savedGames.findIndex(e => e.title === action.payload),
1
)
];
return {
...state,
savedGames: updatedGames
};
Edit: Dispatch to props:
deleteGame: (res) => dispatch({type: actionType.DELETE_GAME, payload: res})
I also noticed that the last game in the list can't be deleted, the state updated but the component doesn't re-render so it's not disappearing.
Something in the reducer probably is wrong, what do you think?
I think your problem is that the return value of splice is the array of removed games,
try something like that (note you can also use the filter method):
case actionTypes.DELETE_GAME:{
let updatedGames = [
...state.savedGames
];
updatedGames.splice(
updatedGames.findIndex(e => e.title === action.payload),
1
)
return {
...state,
savedGames: updatedGames
};
}
also I think it is better for you to use the key of the game to remove it and not the title unless the title is unique
I have a functional component using Hooks:
function Component(props) {
const [ items, setItems ] = useState([]);
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, []);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ])
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
The component fetches some items on mount and saves them to state.
The component receives an itemId prop (from React Router).
Whenever the props.itemId changes, I want this to trigger an effect, in this case logging it to console.
The problem is that, since the effect is also dependent on items, the effect will also run whenever items changes, for instance when the items are re-fetched by pressing the button.
This can be fixed by storing the previous props.itemId in a separate state variable and comparing the two, but this seems like a hack and adds boilerplate. Using Component classes this is solved by comparing current and previous props in componentDidUpdate, but this is not possible using functional components, which is a requirement for using Hooks.
What is the best way to trigger an effect dependent on multiple parameters, only when one of the parameters change?
PS. Hooks are kind of a new thing, and I think we all are trying our best to figure out how to properly work with them, so if my way of thinking about this seems wrong or awkward to you, please point it out.
The React Team says that the best way to get prev values is to use useRef: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
function Component(props) {
const [ items, setItems ] = useState([]);
const prevItemIdRef = useRef();
useEffect(() => {
prevItemIdRef.current = props.itemId;
});
const prevItemId = prevItemIdRef.current;
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, []);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(() => {
if(prevItemId !== props.itemId) {
console.log('diff itemId');
}
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ])
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
I think that this could help in your case.
Note: if you don't need the previous value, another approach is to write one useEffect more for props.itemId
React.useEffect(() => {
console.log('track changes for itemId');
}, [props.itemId]);
An easy way out is to write a custom hook to help us with that
// Desired hook
function useCompare (val) {
const prevVal = usePrevious(val)
return prevVal !== val
}
// Helper hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
and then use it in useEffect
function Component(props) {
const hasItemIdChanged = useCompare(props.itemId);
useEffect(() => {
console.log('item id changed');
}, [hasItemIdChanged])
return <></>
}
⚠️ NOTE: This answer is currently incorrect and could lead to unexpected bugs / side-effects. The useCallback variable would need to be a dependency of the useEffect hook, therefore leading to the same problem as OP was facing.
I will address it asap
Recently ran into this on a project, and our solution was to move the contents of the useEffect to a callback (memoized in this case) - and adjust the dependencies of both. With your provided code it looks something like this:
function Component(props) {
const [ items, setItems ] = useState([]);
const onItemIdChange = useCallback(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [items, props.itemId]);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(onItemIdChange, [ props.itemId ]);
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
So the useEffect just has the ID prop as its dependency, and the callback both the items and the ID.
In fact you could remove the ID dependency from the callback and pass it as a parameter to the onItemIdChange callback:
const onItemIdChange = useCallback((id) => {
if (items) {
const item = items.find(item => item.id === id);
console.log("Item changed to " item.name);
}
}, [items]);
useEffect(() => {
onItemIdChange(props.itemId)
}, [ props.itemId ])
I am a react hooks beginner so this might not be right but I ended up defining a custom hook for this sort of scenario:
const useEffectWhen = (effect, deps, whenDeps) => {
const whenRef = useRef(whenDeps || []);
const initial = whenRef.current === whenDeps;
const whenDepsChanged = initial || !whenRef.current.every((w, i) => w === whenDeps[i]);
whenRef.current = whenDeps;
const nullDeps = deps.map(() => null);
return useEffect(
whenDepsChanged ? effect : () => {},
whenDepsChanged ? deps : nullDeps
);
}
It watches a second array of dependencies (which can be fewer than the useEffect dependencies) for changes & produces the original useEffect if any of these change.
Here's how you could use (and reuse) it in your example instead of useEffect:
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffectWhen(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ], [props.itemId])
Here's a simplified example of it in action, useEffectWhen will only show up in the console when the id changes, as opposed to useEffect which logs when items or id changes.
This will work without any eslint warnings, but that's mostly because it confuses the eslint rule for exhaustive-deps! You can include useEffectWhen in the eslint rule if you want to make sure you have the deps you need. You'll need this in your package.json:
"eslintConfig": {
"extends": "react-app",
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useEffectWhen"
}
]
}
},
and optionally this in your .env file for react-scripts to pick it up:
EXTEND_ESLINT=true
2022 answer
I know that this is an old question, but it's still worth an answer. Effects have received a lot of attention this year, and it's now clearer what you should / shouldn't do with effects than it was back then.
The new React docs, which are still in beta, cover the topic in length.
So, before answering the 'how', you must first answer the 'why', because there aren't actually many genuine use cases for running an effect only when certain dependencies change.
You might not need an effect
'You might not need an effect' is actually the title of the page that demonstrates that you might be using an effect for the wrong reasons. And the example you gave us is actually discussed in 'adjusting some state when a prop changes'
Your code can be simply rewritten without the second effect:
function Component(props) {
const [ items, setItems ] = useState([]);
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, [handleFetchItems]);
// Find the item that matches in the list
const item = items.find(item => item.id === props.itemId);
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
Indeed, you don't need an effect just to identify the item from the list. React already re-renders when items changes, so item can be re-computed in the component directly.
Note: if you really want to log to the console when the item changes, you can still add this code to your component:
useEffect(() => {
console.log("Item changed to ", item?.name);
}, [item])
Note how item is the only dependency of the effect, so it only runs when item changes. But this is not a good use case for an effect again.
Instead, if you want to do something in response to an event that occurs in your application, you should do this 👇
Separating Events from Effects
And yes, 'Separating Events from Effects' is also the title of a page in the new React docs! In the example you gave us, you want to write to the console whenever item changes. But I'm not naive and I know you might want to do more than this in a practice.
If you want to do something when something else changes, then you can use the new useEvent hook to create an event handler for it (or at least, use its polyfill since it hasn't been released yet). The event in question here is the itemId prop changing.
You can then change your second effect to:
// Handle a change of item ID
const onItemIdChange = useEvent((itemId) => {
const item = items?.find(item => item.id === itemId);
console.log("Item changed to " item.name);
});
// Trigger onItemIdChange when item ID changes
useEffect(() => {
onItemIdChange(props.itemId);
}, [props.itemId]);
The advantage of this method is that useEvent returns a stable function, so you don't need to add it as a dependency to your effect (it never changes). Also, the function you pass to useEvent has no dependency array: it can access all the properties, states or variables inside your component and it is always fresh (no stale values).
I still think that based on the example you gave us, you don't really need an effect. But for other genuine cases, at least you know how to extract events from your effects now and thus you can avoid hacking the dependency array.
Based on the previous answers here and inspired by react-use's useCustomCompareEffect implementation, I went on writing the useGranularEffect hook to solve a similar issue:
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useGranularEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items ], [ props.itemId ])
implemented as (TypeScript):
export const useGranularEffect = (
effect: EffectCallback,
primaryDeps: DependencyList,
secondaryDeps: DependencyList
) => {
const ref = useRef<DependencyList>();
if (!ref.current || !primaryDeps.every((w, i) => Object.is(w, ref.current[i]))) {
ref.current = [...primaryDeps, ...secondaryDeps];
}
// eslint-disable-next-line react-hooks/exhaustive-deps
return useEffect(effect, ref.current);
};
Try it on codesandbox
The signature of useGranularEffect is the same as useEffect, except that the list of dependencies has been split into two:
primary dependencies: the effect only runs when these dependencies change
secondary dependencies: all the other dependencies used in the effect
In my opinion, it makes the case of running the effect only when certain dependencies change easier to read.
Notes:
Unfortunately, there is not linting rule to help you ensure that the two arrays of dependencies are exhaustive, so it is your responsibility to make sure you're not missing any
It is safe to ignore the linting warning inside the implementation of useGranularEffect because effect is not an actual dependency (it's the effect function itself) and ref.current contains the list of all dependencies (primary + secondary, which the linter cannot guess)
I'm using Object.is to compare dependencies so that it's consistent with the behaviour of useEffect, but feel free to use your own compare function or, better, to add a comparer as argument
UPDATE: useGranularEffect has now be published into the granular-hooks package. So just:
npm install granular-hooks
then
import { useGranularEffect } from 'granular-hooks'
I just tried this myself and it seems to me that you don't need to put things in the useEffect dependency list in order to have their updated versions. Meaning you can just solely put in props.itemId and still use items within the effect.
I created a snippet here to attempt to prove/illustrate this. Let me know if something is wrong.
const Child = React.memo(props => {
const [items, setItems] = React.useState([]);
const fetchItems = () => {
setTimeout(() => {
setItems((old) => {
const newItems = [];
for (let i = 0; i < old.length + 1; i++) {
newItems.push(i);
}
return newItems;
})
}, 1000);
}
React.useEffect(() => {
console.log('OLD (logs on both buttons) id:', props.id, 'items:', items.length);
}, [props.id, items]);
React.useEffect(() => {
console.log('NEW (logs on only the red button) id:', props.id, 'items:', items.length);
}, [props.id]);
return (
<div
onClick={fetchItems}
style={{
width: "200px",
height: "100px",
marginTop: "12px",
backgroundColor: 'orange',
textAlign: "center"
}}
>
Click me to add a new item!
</div>
);
});
const Example = () => {
const [id, setId] = React.useState(0);
const updateId = React.useCallback(() => {
setId(old => old + 1);
}, []);
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Child
id={id}
/>
<div
onClick={updateId}
style={{
width: "200px",
height: "100px",
marginTop: "12px",
backgroundColor: 'red',
textAlign: "center"
}}
>Click me to update the id</div>
</div>
);
};
ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
From the example provided, your effect does not depend on items and itemId, but one of the items from the collection.
Yes, you need items and itemId to get that item, but it does not mean you have to specify them in the dependency array.
To make sure it is executed only when the target item changes, you should pass that item to dependency array using the same lookup logic.
useEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items.find(item => item.id === props.itemId) ])