New to React.
I have a handler, as follows, that updates state of an array. The data is a set of animal pairs.
class Animal extends Component {
state = {
pairs: [
{ fromAnimal: 'Dog', toAnimal: 'Cat' },
{ fromAnimal: 'Lion', toAnimal: 'Tiger' },
{ fromAnimal: 'Rabbit', toAnimal: 'Bear' }
]
};
closePairHandler = (fromAnimal, toAnimal) => {
let newPairs = this.state.pairs.filter((pair) => {
return !(pair.fromAnimal === fromAnimal && pair.toAnimal === toAnimal);
});
console.log('pairs', newPairs); // This shows that the correct pair was removed from the array.
this.setState({ pairs: newPairs });
};
render() {
return (
<div>
{
this.state.pairs.map((pair, index) => {
return <SomeComponent key={index} pair={pair} closePair={(fromAnimal, toAnimal) => this.closePairHandler(fromAnimal, toAnimal)} />;
}
}
</div>
);
};
};
export default Animal;
This is a super simplified version of the code I have. BUT, when the closePairHandler is called to remove an animal pair (for example, Lion/Tiger). The console.log in the closePairHandler shows that the array has been updated successfully.
However, when the components render. It is removing the LAST component in the array and not the one that was selected. It's reducing the array size by 1, but not removing the correct item in the mapping (in render), althought the closePairHandler console.log is showing the array correctly updated before setting the state.
Can anyone explain to me what is going on here?
Thanks again!
You are not providing the key for your mapped data while rendering SomeComponent and hence react is not able to correctly identify what element got changed. You can use index as the key if you don't have a unique id in your pair object else you should use that for performance reasons
return (
<div>
{
this.state.pairs.map((pair, index) => {
return <SomeComponent key={index} pair={pair} closePair={(fromAnimal, toAnimal) => this.closePairHandler(fromAnimal, toAnimal)} />;
}
}
</div>
);
Related
I have a react component that stores a set of fruits in useState. I have a memoized function (visibleFruits) that filters fruits. I map visibleFruits to the dom.
The problem is, when i check a fruit, all visible fruits re-render.
I am expecting that only the selected one re-renders since it is the only one that is changing.
Is there a way for me to use this pattern but prevent all from re-rendering on check?
In real life, there is a complex function in visibleFruits useMemo. So I can't simply append the filter before the map.
Edit, here is updated edit:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
])
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible)
}, [fruits])
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
f.selected = bool
}
return f
})
})
}
return (
<div>
{visibleFruits.map((fruit) => {
return <FruitOption fruit={fruit} handleCheck={handleCheck} />
})}
</div>
)
}
const FruitOption = ({ fruit, handleCheck }) => {
console.log('** THIS RENDERS TWICE EVERY TIME USER SELECTS A FRUIT **')
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
)
}
export default Example
First, there's a problem with the handleCheck function (but it's not related to what you're asking about). Your code is modifying a fruit object directly (f.selected = bool), but you're not allowed to do that with React state, objects in state must not be directly modified, and rendering may not be correct if you break that rule. Instead, you need to copy the object and modify the copy (like you are with the array):
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
};
But that's not what you're asking about, just something else to fix. :-)
The reason you see the console.log executed twice after handleCheck is that the component has to be re-rendered (for the change), and there are two visible fruits, so you see two calls to your FruitOption component function. There are two reasons for this:
handleChange changes every time your Example component function is called, so FruitOption sees a new prop every time; and
FruitOption doesn't avoid re-rendering when its props don't change, so even once you've fixed #1, you'd still see two console.log calls; and
Separately, there's no key on the FruitOption elements, which can cause rendering issues. Always include a meaningful key when rendering elements in an array. (Don't just use the index, it's problematic; but your fruit objects have an id, which is perfect.)
To fix it:
Memoize handleChange so that it's not recreated every time, probably via useCallback, and
Use React.memo so that FruitOption doesn't get called if its props don't change (see the end of this answer for the class component equivalent), and
Add a meaningful key to the FruitOption elements in Example
Taking those and the handleChange fix above and putting them all together:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
]);
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible);
}, [fruits]);
const handleCheck = React.useCallback(
(bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
},
[] // *** No dependencies since all it uses is `setFruits`, which is
// stable for the lifetime of the component
);
return (
<div>
{visibleFruits.map((fruit) => {
// *** Note the key
return <FruitOption key={fruit.id} fruit={fruit} handleCheck={handleCheck} />
})}
</div>
);
}
// *** `React.memo` will compare the props and skip the call if they're the same, reusing
// the previous call's result.
const FruitOption = React.memo(({ fruit, handleCheck }) => {
console.log(`Rendering fruit ${fruit.id}`);
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
);
});
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
As you can see, with all that in place, only the changed fruit is re-rendered.
Re React.memo: For components with more complicated requirements, you can provide a function as a second argument that determines whether the two sets of props are "the same" for rendering purposes. By default, React.memo just does a shallow equality comparison, which is often sufficient.
Finally: For class components, the equivalent of React.memo without providing an equality callback is extending PureComponent instead of Component. If you want to make your check of the props more fine-grained, you can implement shouldComponentUpdate instead.
Suppose I have long list (let's assume there is no pagination yet) where each list item has input and ability to update own value (as a part of collection). Let's say code looks something like that:
const initItems = [
{ id: 0, label: "Hello world" },
...
{ id: 100, label: "Goodby" }
];
function List() {
const [items, setItems] = React.useState([...initItems]);
const handleChange = React.useCallback((e, id) => {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
label: e.target.value
}
}
return item;
}));
}, [items]);
return (
<ul>
{items.map(({ id, label }) => {
return (
<Item
id={id}
key={id}
label={label}
onChange={handleChange}
/>
)
})}
</ul>
)
}
// Where Item component is:
const Item = React.memo(({ onChange, label, id }) => {
console.log('Item render');
return (
<li>
<input type="text" value={label} onChange={e => onChange(e, id)} />
</li>
)
});
Looks pretty straightforward, right? While wrapping Item component with React.memo() what I wanted to achieve is to avoid re-render of each Item when some of the Item's gets updated. Well, I'm not sure it should works with this strategy, since each Item is a part of collection (items) and when I update any Item then items gets mapped and updated. What I did try - is to write custom areEqual method for Item component, where I do comparison of label value from props:
function areEqual(prev, next) {
return prev.label === next.label;
}
however with this approach the behaviour of updating items breaks down completely and updating next item reset previous updates and so on (I even could not observe any pattern to explain).
So the question: is it possible to avoid re-rendering of every item in such collection while having ability to update value of individual item?
Your problem here that you change callback on each render. So, you change callback, it changes onChange and this, in turn, runs rerender. To avoid it you can use updater function with setState.
const handleChange = React.useCallback((e, id) => {
// I made separate function so it would be easier to read
// You can just write `(items) =>` before your `items.map` and it will work
function updater(items) {
// we have freshest items here
return items.map((item) => {
if (item.id === id) {
return {
...item,
label: e.target.value,
};
}
return item;
});
}
// pass function
setItems(upadter);
// removed items from dependencies
}, []);
This way, your updater function will always get current value of state into parameters, and your props will update for actually updated item. Another solution would be to write custom updater that compares all values, but onChange. This is ok in short term, but this can become complex and cumbersome to maintain.
Here is live example: https://codesandbox.io/s/unruffled-johnson-ubz1l
I have two api requests that return JSON objects. They return an array of objects.
One API request that I make is fine and allows me to update the state with the response, but the other one (below) doesn't and I don't understand why.
API request to fetch genres list:
async getGenreList() {
const genresResults = await getGenres();
return genresResults;
}
The request:
export const getGenres = async () => {
try {
const response = await axios.get(
"https://api.themoviedb.org/3/genre/movie/list?api_key=<APIKEY>&language=en-US"
);
const { genres } = response.data;
return genres;
} catch (error) {
console.error(error);
}
};
The response is an array of 19 genre objects but this is just an example:
[
{id: 28, name: "Action"},
{id: 12, name: "Adventure"}
]
I then want to update the state like this and pass the response to genreOptions. But it tells me Error: Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead.
componentDidMount() {
this.getGenreList().then((response) => {
console.log(response)
this.setState({ genreOptions: response});
});
}
The below works when i update the state and map over it but I don't want to do that, i want to pass the whole response down so i can map over the data in my component as I need it there to do some data matching.
this.setState({ genreOptions: response.map((genreOption) => {
return genreOption.name
})});
This is the state:
this.state = {
results: [],
movieDetails: null,
genreOptions: [],
};
I want to pass the genreOptions here to genres then map over it in the MovieResults component.
<MovieResults>
{totalCount > 0 && <TotalCounter>{totalCount} results</TotalCounter>}
<MovieList movies={results || []} genres={genreOptions || []} />
</MovieResults>
Why can't I? Any ideas? I have done it for another similar request :S
UPDATE TO SHOW MOVIELIST COMPONENT
export default class MovieList extends React.Component {
render() {
const { movies, genres } = this.props;
const testFunction = (movieGenreIds) => {
const matchMovieGenresAndGenreIds = genres.map((genreId) => {
const matchedGenres = movieGenreIds.find((movieGenre) => {
return movieGenre.id === genreId
})
return matchedGenres // this returns the matching objects
})
const result = matchMovieGenresAndGenreIds.filter(Boolean).map((el)=> {
return el.name
})
return result
}
return (
<MoviesWrapper>
{movies.map((movie) => {
const {
title,
vote_average,
overview,
release_date,
poster_path,
genre_ids
} = movie;
return (
<MovieItem
title={title}
rating={vote_average}
overview={overview}
release={release_date}
poster={poster_path}
movieGenres={testFunction(genre_ids)}
/>
);
})}
</MoviesWrapper>
);
}
}
**** MOVIE ITEM COMPONENT***
export default class MovieItem extends React.Component {
render() {
const { title, overview, rating, release, poster, movieGenres } = this.props;
return (
// The MovieItemWrapper must be linked to the movie details popup
<MovieItemWrapper>
<LeftCont>
<img
className="movie-img"
src={`https://image.tmdb.org/t/p/w500${poster}`}
/>
</LeftCont>
<RightCont>
<div className="movie-title-container">
<h2 className="movie-title">{title}</h2>
<Rating>{rating}</Rating>
</div>
<div>{movieGenres}</div>
<p>{overview}</p>
<p>{release}</p>
</RightCont>
</MovieItemWrapper>
);
}
}
Please follow this steps to fix your code. I'll try yo explain what's happening along the way:
In your main component. Set the state to the value that you really want to pass to your child component. Remember that response will be an array of objects.
componentDidMount() {
this.getGenreList().then((response) => {
this.setState({genreOptions: response});
});
}
In your MovieList component. Please check your testFunction to respect data types. The following code will return you an array of strings containing the names of the genres that are included in the movies genres array.
const testFunction = (movieGenreIds) => {
return genres
.filter((genre) => {
return movieGenreIds.includes(genre.id);
})
.map((genre) => genre.name);
};
In your MovieItem component. (This is were the real problem was)
Instead of:
<div>{movieGenres}</div>
You may want to do something like this:
<div>{movieGenres.join(' ')}</div>
This converts your array into a string that can be rendered. Your error was due to the fact that you were passing there an array of objects that React couldn't render.
If you have any doubt, please let me know.
NOTE: I suggest you to use a type checker to avoid this kind of problems. And to be consistent with your variables naming conventions.
Update based on new information from chat:
In your ExpandableFilters component, you must fix the following piece of code to get the genre name (string). As explained in chat, you can't have objects as a result for a JSX expression ({}), but only primitives that can be coerced to strings, JSX elements or an array of JSX elements.
<GenreFilterCont marginTop>
{filtersShown && (
<ExpandableFiltersUl>
{this.props.movieGenres.map((genre, index) => {
return (
<ExpandableFiltersLi key={index}>
<Checkbox />
{genre.name}
</ExpandableFiltersLi>
);
})}
</ExpandableFiltersUl>
)}
</GenreFilterCont>
Please also note that I've added a key property. You should do it whenever you have a list of elements to render. For more about this I will refer you to the React Docs.
import React, {Component} from 'react';
import "./DisplayCard.css";
class DisplayCard extends Component {
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
return <div>{array[i].task}</div>
}
}
renderElements = (savedTasks) =>{
if (savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(savedTasks)
}
}
render() {
return (
<div className="DisplayCardContainer">
{this.renderElements(this.props.saved)}
</div>
)
}
}
export default DisplayCard;
Hey guys,
I am new to react, so this is my child component that takes state from its parent component. My goal is to re-render component every time the array this.props.saved is changed.
This component renders: <p>You have no saved tasks.</p> when the this.props.saved.length === 0 and it renders <div>{array[0].task}</div> when i enter the first task, but it keeps it at <div>{array[0].task}</div> after that. I do see that the state keeps changing and this.props.saved keeps getting bigger, but my component doesn't change anymore.
Here's your problem:
runArray = (array) => {
for (var i = 0; i<array.length; i++) {
//the first time we get here, it immediately ends the function!
return <div>{array[i].task}</div>
}
}
This loop only ever goes through once (at i=0) and then returns, exiting the runArray function and cancelling the rest of the loop. You probably wanted to return an array of elements, one for each of the tasks. I recommend using Array.map() for this, which takes an array and transforms each element, creating a new array:
runArray = (array) => {
return array.map(arrayElement => <div>arrayElement.task</div>);
}
This should do the trick. Note that React may complain about the fact that your elements lack the key property - see the documentation for more info: https://reactjs.org/docs/lists-and-keys.html
The problem is in your runArray function. Inside your loop, you are returning the first element and that's it. My guess is, you see only the first entry?
When you are trying to render all your tasks, I would suggest to map your tasks, e.g.
runArray = (array) => array.map(entry => <div>{entry.task}</div>)
It is because you write wrong the runArray function. You make a return in the for loop so it breaks after the first iteration. It will not iterate over the full array.
You need to transform your for loop to a map : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
runArray = (array) => {
return array.map(v => <div>{v.task}</div>)
}
Does it fix your issue ?
You have to update state of the component to trigger render function. Your render function is not triggered because you did not update the state when the props changed. There are many ways to update state when props updated. One method may be the following:
componentWillReceiveProps(nextProps){
if (nextProps.saved !== this.props.saved) {
this.setState({ saved: nextProps.saved })
}
}
Also change yoour render function to use state of the component as below:
renderElements = () =>{
if (this.state.savedTasks.length === 0) {
return <div className="noTasks"> <p>You have no saved tasks.</p> </div>
} else {
return this.runArray(this.state.savedTasks)
}
}
Use .map so that it renders your task correctly. You can remove runArray and rely entirely on props so you don't need to pass arguments across functions as it can get messy quickly. Here's a quick running example of how to create a parent component where you can add a task and pass them into a component so that it renders your data when props are changed, therefore making it reactive.
class App extends React.Component {
state = {
taskLabel: "",
tasks: [
{
id: 1,
label: "Do something"
},
{
id: 2,
label: "Learn sometihng"
}
]
};
handleInput = evt => {
this.setState({
[evt.target.name]: evt.target.value
});
};
handleSubmit = evt => {
evt.preventDefault();
this.setState(prevState => ({
taskLabel: "",
tasks: [
...prevState.tasks,
{
id: prevState.tasks.length + 1,
label: this.state.taskLabel
}
]
}));
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
name="taskLabel"
type="text"
placeholder="Task label"
value={this.state.taskLabel}
onChange={this.handleInput}
/>
<button>Create task</button>
</form>
<DisplayCard tasks={this.state.tasks} />
</div>
);
}
}
class DisplayCard extends React.Component {
renderTasks = () => {
if (this.props.tasks.length !== 0) {
return this.props.tasks.map(task => (
<div key={task.id}>{task.label}</div>
));
} else {
return <div>No tasks</div>;
}
};
render() {
return <div>{this.renderTasks()}</div>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I have three files: ShopsContainer.js ShopsComponent.js and ShopsItemComponent.js
ShopsContainer maintains an array of shop items in local state that gets passed down into ShopsComponent as props. ShopsComponent then maps through the items array that is being received as props and renders a ShopsItemComponent for each item in the array.
Within my ShopsContainer file, I have a method that removes a shop item from state using the following code:
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
When this happens, the correct item is removed from the items array in state, however, whatever the last ShopItem is that is in the DOM at the time of the removeShop call will get removed no matter if it is the correct item that should be removed or not. In other words, when removeShop gets called and the items array in state gets updated correctly, the wrong ShopItemComponent gets removed from the DOM.
What I would like to happen (or what I think should happen) is when removeShop gets called, that shop gets removed from the items array in state and ShopsContainer re-renders causing ShopsComponent to re-render with the updated props being received. And lastly ShopsComponent would map through the newly updated items array in props displaying a `ShopItemComponent for the correct items. Perhaps the problem has to do with the props being updated?
My code is as follows:
ShopsContainer.js
class ShopsContainer extends Component {
constructor() {
this.state = {
items: null
}
this.getAll();
this.removeShop = this.removeShop.bind(this);
}
getAll = () => {
// API request that fetches items and updates state
}
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
render() {
return (
<div>
{this.state.items ? <ShopComponent items={this.state.items} removeShop={this.removeShop} /> : <div><h1>Loading...</h1></div>}
</div>
);
}
}
ShopsComponent.js
class ShopsComponent extends Component {
constructor() {
this.handleRemove = this.handleRemove.bind(this);
}
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem, i) => {
return (<ShopItemComponent key={i} item={shopItem} removeShop={this.handleRemove} />);
})}
</React.Fragment>
);
}
}
Your code is working great, but you only has one mistake , your ShopComponent is assign index as a key for each ShopItemComponent and react is tracking those indexes to update the correct component, so you need to set key as a unique value between items, then I realize that shopAccount should be your id for each item.
The solution code is below.
class ShopsComponent extends Component {
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem) => <ShopItemComponent key={shopItem.shopAccount} item={shopItem} removeShop={this.handleRemove} />)}
</React.Fragment>
);
}
}
I hope you can find useful.
Note, when you are using a arrow function into your class, don't bind that method into the constructor, so remove it, because
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
is already binded.