I have a simple React app. On the 'home' page you can search movies from an API and add a movie to a list of favorited. I'm using Context to store which movies are on the list and pass it to the 'favorites' page where those items are rendered. It works well up to a point.
Once on the 'favorites' page, when I remove a movie, I would like the page to then show the updated elements. Instead, I have the elements I already had there plus the elements from the updated list.
So let's say my favorited movies were 'spiderman', 'batman' and 'dracula'. when I remove 'dracula' from the list, I suddenly have the cards of 'spiderman', 'batman, 'dracula', 'spiderman'(again) and 'batman'(again).
When I reload the 'favorites' page, it all works as intended. I just would like for it to be updated correctly upon removing the movie.
Any advice?
Here is the code for the Home page, Favorite page, DataContext and the Card component
import React, { createContext, useState, useEffect } from "react";
export const DataContext = createContext();
function DataContextProvider({ children }) {
const [favorited, setFavorited] = useState([]);
useEffect(() => {
const savedMovies = localStorage.getItem("movies");
if (savedMovies) {
setFavorited(JSON.parse(savedMovies));
}
}, []);
useEffect(() => {
localStorage.setItem("movies", JSON.stringify(favorited));
}, [favorited]);
function addToFavorites(id) {
setFavorited((prev) => [...prev, id]);
}
function removeFromFavorited(id) {
const filtered = favorited.filter(el => el != id)
setFavorited(filtered)
}
return (
<DataContext.Provider value={{ favorited, addToFavorites, removeFromFavorited}}>
{children}
</DataContext.Provider>
);
}
export default DataContextProvider;
function Favorites(props) {
const ctx = useContext(DataContext);
const [favoriteMovies, setFavoriteMovies] = useState([]);
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
for (let i = 0; i < savedMovies.length; i++) {
axios
.get(
`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
)
.then((res) => {
setFavoriteMovies((prev) => [...prev, res.data]);
});
}
}, [ctx.favorited]);
return (
<>
<Navbar />
<main>
<div className="favorites-container">
{favoriteMovies.map((movie) => {
return <Card key={movie.id} movie={movie} />;
})}
</div>
</main>
</>
);
}
function Home(props) {
const [moviesData, setMoviesData] = useState([]);
const [numOfMovies, setNumOfMovies] = useState(10);
const [search, setSearch] = useState(getDayOfWeek());
const [spinner, setSpinner] = useState(true);
const [goodToBad, setGoodToBad] = useState(null);
function getDayOfWeek() {
const date = new Date().getDay();
let day = "";
switch (date) {
case 0:
day = "Sunday";
break;
case 1:
day = "Monday";
break;
case 2:
day = "Tuesday";
break;
case 3:
day = "Wednesday";
break;
case 4:
day = "Thursday";
break;
case 5:
day = "Friday";
break;
case 6:
day = "Saturday";
break;
}
return day;
}
function bestToWorst() {
setGoodToBad(true);
}
function worstToBest() {
setGoodToBad(false);
}
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
axios
.get(
`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${search}`
)
.then((res) => {
setMoviesData(res.data.results);
//console.log(res.data.results)
setSpinner(false);
setGoodToBad(null);
});
}, [search]);
return (
<>
<Navbar />
<main>
<form>
<input
type="text"
placeholder="Search here"
id="search-input"
onChange={(e) => {
setSearch(e.target.value);
setNumOfMovies(10);
}}
/>
{/* <input type="submit" value="Search" /> */}
</form>
<div className="sorting-btns">
<button id="top" onClick={bestToWorst}>
<BsArrowUp />
</button>
<button id="bottom" onClick={worstToBest}>
<BsArrowDown />
</button>
</div>
{spinner ? <Loader /> : ""}
<div>
<div className="results">
{!moviesData.length && <p>No results found</p>}
{moviesData
.slice(0, numOfMovies)
.sort((a,b) => {
if(goodToBad) {
return b.vote_average - a.vote_average
} else if (goodToBad === false){
return a.vote_average - b.vote_average
}
})
.map((movie) => (
<Card key={movie.id} movie={movie} />
))}
</div>
</div>
{numOfMovies < moviesData.length && (
<button className="more-btn" onClick={() => setNumOfMovies((prevNum) => prevNum + 6)}>
Show More
</button>
)}
</main>
</>
);
}
export default Home;
function Card(props) {
const ctx = useContext(DataContext);
return (
<div
className={
ctx.favorited.includes(props.movie.id)
? "favorited movie-card"
: "movie-card"
}
>
<div className="movie-img">
<img
alt="movie poster"
src={
props.movie.poster_path
? `https://image.tmdb.org/t/p/w200/${props.movie.poster_path}`
: "./generic-title.png"
}
/>
</div>
<h2>{props.movie.original_title}</h2>
<p>{props.movie.vote_average}/10</p>
<button
className="add-btn"
onClick={() => ctx.addToFavorites(props.movie.id)}
>
Add
</button>
<button
className="remove-btn"
onClick={() => ctx.removeFromFavorited(props.movie.id)}
>
Remove
</button>
</div>
);
}
export default Card;
As mentioned before a lot of things cold be improved (you might want to check some react tutorial beginners related to best practices).
Anyway the main issue your app seems to be your callback after you get the response from the API (so this part):
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
for (let i = 0; i < savedMovies.length; i++) {
axios
.get(
`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
)
.then((res) => {
setFavoriteMovies((prev) => [...prev, res.data]);
});
}
here you are calling setFavoriteMovies((prev) => [...prev, res.data]); but you actually never reset your favoriteMovies list.
So in your example favoriteMovies is ['spiderman', 'batman', 'dracula']. Then the useEffect callback executes with the array unchanged.
So you are making the requests just for 'spiderman' and 'batman' but your favoriteMovies array is ['spiderman', 'batman', 'dracula'] when the callback is entered (and this is why you end up appending those values to the existing ones and in the end your favoriteMovies == ['spiderman', 'batman', 'dracula', 'spiderman', 'batman'] in your example).
How to fix?
Quick fix would that might be obvious would be to reset the favoriteMovies at the beggining of useEffect. But that would be a extremly bad ideea since setting the state many times is terrible for performance reasons (each setState callback triggers a re-render) as well as for redability. So please don't consider this.
What I would suggest though would be to get all the values in the useEffect callback, put all the new favorite movies data in a variable and at the end of the function change the state in one call with the full updated list.
There are multiple ways to achieve this (async await is the best imo), but trying to alter the code as little as possible something like this should also work:
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
const favoriteMoviesPromises = [];
for (let i = 0; i < savedMovies.length; i++) {
favoriteMoviesPromises.push(
axios
.get(`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`)
.then((res) => res.data)
);
}
Promise.all(favoriteMoviesPromises).then((newFavoriteMovies) =>
setFavoriteMovies(newFavoriteMovies)
);
});
Please note I wasn't able to test this code since I don't have an exact reproduction of the error (so it might need some small adjustments). This code sample is rather a direction for your problem :)
Edit regarding the comment:
Despite the state issue, I would really recommend working on code cleanliness, efficiency and readability.
Examples (I put a few examples in code snippets to avoid a really long comment):
1. `function getDayOfWeek`:
First thing is that you don't need the `day` variable and all the break statement.
You could just return the value on the spot (this would also stop the execution of the function).
So instead of
case 0:
day = "Sunday";
break;
you could have
case 0:
return "Sunday";
Going even further you don't need a switch case at all. You could just create an array
`const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', "Saturday"]`
and just return daysOfWeek[date].
This would result in shorter and easier to read code.
2. Also this code is not really consistent. For example you have
onChange={(e) => {
setSearch(e.target.value);
setNumOfMovies(10);
}}
but also `onClick={bestToWorst}` which is just `function bestToWorst() { setGoodToBad(true) }`.
If this is not reusable you could just use `onClick={() => setGoodToBad(true)}`.
But even if you really want to keep the bestToWorst callback for whatever reason you could at least write and inline function
(something like `const bestToWorst = () => setGoodToBad(true)` and use it the same).
Anyway... From thoose 2 cases (bestToWorst and `Search here` onChange function),
the second one make more sense to be defined outside.
3. The next part is really hard to read and maintain:
{!moviesData.length && <p>No results found</p>}
{moviesData
.slice(0, numOfMovies)
.sort((a,b) => {
if(goodToBad) {
return b.vote_average - a.vote_average
} else if (goodToBad === false){
return a.vote_average - b.vote_average
}
})
.map((movie) => (
<Card key={movie.id} movie={movie} />
))}
Also this code doesn't belong in html.
You should at least put the slice and sort parts in a function.
Going further `if(goodToBad)` and `else if (goodToBad === false)` are also not ideal.
It would be best to use a separate function an example would be something like:
const getFormattedMoviesData = () => {
let formattedMoviesData = moviesData.slice(0, numOfMovies)
if(!goodToBad && goodToBad !== false) return formattedMoviesData;
const getMoviesDifference = (m1, m2) => m1.vote_average - m2.vote_average
return formattedMoviesData.sort((a,b) => goodToBad ? getMoviesDIfference(b,a) : getMoviesDIfference(a,b)
4. DataContext name doesn't suggest anything.
I would propose something more meaningfull (especially for contexts) like `FavoriteMoviesContext`.
In this way people can get an ideea of what it represents when they come across it in the code.
Additionally the context only contains `favorited, addToFavorites, removeFromFavorited`.
So rather than using
`const ctx = useContext(DataContext);`
you could just use
`const {favorited, addToFavorites, removeFromFavorited} = useContext(DataContext);`
and get rid of the ctx variable in your code
Regarding the api:
If the search api returns all the movie data you need you can take it from there and use it in the favorites.
Alternatively it would be great to have an endpoint to return a list of multiple movies
(so send an array of id's in the request and receive all of them).
But this is only possible if the backend supports it.
But otherwise, since the api might contain hundreds of thousands or even millions, having them all stored on the frontside state would be an overkill
(you can in some cases have this type lists stored in a redux state or a react context and filter them on frontend side.
But it won't be efficient for such a big volume of data).
Small conclusion: ignoring the state part there aren't big issues in the code (and for a personal project or for learning might be decent). But if someone else has to work on in or you have to come back on this code after a month might become a nightmare. (especially since it seems like the codebase is not very small)
And people trying to understand your code might find it hard as well (including when you are posting it on stack overflow). I highlighted just a few, but it should point in the right direction, I hope.
First of all, you should review the way you manage the favorite movies and that of what you want to do with them in your app. If you need to make a page to display the list of favorites, I would rather save in localstorage the necessary information for the list (cover, title, year, id, etc) without having to save the whole movie object. This will prevent you from having to call the API for each movie which will be very bad in terms of performance on your application. Also, it will prevent you from having to create another state on the Favorites page so it will solve your problem automatically (I think your problem came from the duplicate state you have).
I'm trying to create a component that tests english vocabulary.
Basically, there are 4 options with 1 correct.
When user chooses option, the right option is highlited in green, and the wrong one in red.
Then user can push the "next" button to go to the next batch of words.
I store refs in object (domRefs, line 68).
Populate it at line 80.
And remove all refs at line 115.
But it doesnt get removed, and leads to error (line 109)
https://stackblitz.com/edit/react-yocysc
So the question is - How to store these refs and what would be the better way to write this component?
Please help, Thanks.
You shouldn't keep refs for component in global variable, since it's making your component singleton. To apply some styles just use conditional rendering instead. Also, it's better to split your test app into several separate components with smaller responsibilities:
const getClassName(index, selected, rightAnswer) {
if (selected === null) {
return;
}
if (index === rightAnswer) {
return classes.rightAnswer;
}
if (index === selected) {
return classes.wrongAnswer;
}
}
const Step = ({ question, answers, rightAnswer, selected, onSelect, onNext }) => (
<div ...>
<div>{ question }</div>
{ answers.map(
(answer, index) => (
<Paper
key={ index }
onClick={ () => onSelect(index) }
className={ getClassName(index, selected, rightAnswer) }
) }
{ selected && <button onClick={ onNext() }>Next</button> }
</div>
);
const Test = () => {
const [ index, setIndex ] = useState();
const word = ..., answers = ..., onSelect = ..., onNext = ...,
return (
<Question
question={ word }
answers={ answers }
... />
);
}
I am making a React app that allows you to make a list and save it, but React has been giving me a warning that my elements don't have a unique key prop (elements List/ListForm). How should I create a unique key prop for user created elements? Below is my React code
var TitleForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var listName = {'name':this.refs.listName.value};
this.props.handleCreate(listName);
this.refs.listName.value = "";
},
render: function() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input className='form-control list-input' type='text' ref='listName' placeholder="List Name"/>
<br/>
<button className="btn btn-primary" type="submit">Create</button>
</form>
</div>
);
}
});
var ListForm = React.createClass({
getInitialState: function() {
return {items:[{'name':'item1'}],itemCount:1};
},
handleSubmit: function(e) {
e.preventDefault();
var list = {'name': this.props.name, 'data':[]};
var items = this.state.items;
for (var i = 1; i < items.length; i++) {
list.data.push(this.refs[items[i].name]);
}
this.props.update(list);
$('#'+this.props.name).remove();
},
handleClick: function() {
this.setState({
items: this.state.items.concat({'name':'item'+this.state.itemCount+1}),
itemCount: this.state.itemCount+1
});
},
handleDelete: function() {
this.setState({
itemCount: this.state.itemCount-1
});
},
render: function() {
var listItems = this.state.items.map(function(item) {
return (
<div>
<input type="text" className="list-form" placeholder="List Item" ref={item.name}/>
<br/>
</div>
);
});
return (
<div>
<form onSubmit={this.handleSubmit} className="well list-form-container">
{listItems}
<br/>
<div onClick={this.handleClick} className="btn btn-primary list-button">Add</div>
<div onClick={this.handleDelete} className="btn btn-primary list-button">Delete</div>
<button type="submit" className="btn btn-primary list-button">Save</button>
</form>
</div>
)
}
});
var List = React.createClass({
getInitialState: function() {
return {lists:[], savedLists: []};
},
handleCreate: function(listName) {
this.setState({
lists: this.state.lists.concat(listName)
});
},
updateSaved: function(list) {
this.setState({
savedLists: this.state.savedLists.concat(list)
});
},
render: function() {
var lst = this;
var lists = this.state.lists.map(function(list) {
return(
<div>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
var savedLists = this.state.savedLists.map(function(list) {
var list_data = list.data;
list_data.map(function(data) {
return (
<li>{data}</li>
)
});
return(
<div>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
var save_msg;
if(savedLists.length == 0){
save_msg = 'No Saved Lists';
}else{
save_msg = 'Saved Lists';
}
return (
<div>
<TitleForm handleCreate={this.handleCreate} />
{lists}
<h2>{save_msg}</h2>
{savedLists}
</div>
)
}
});
ReactDOM.render(<List/>,document.getElementById('app'));
My HTML:
<div class="container">
<h1>Title</h1>
<div id="app" class="center"></div>
</div>
There are many ways in which you can create unique keys, the simplest method is to use the index when iterating arrays.
Example
var lists = this.state.lists.map(function(list, index) {
return(
<div key={index}>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
Wherever you're lopping over data, here this.state.lists.map, you can pass second parameter function(list, index) to the callback as well and that will be its index value and it will be unique for all the items in the array.
And then you can use it like
<div key={index}>
You can do the same here as well
var savedLists = this.state.savedLists.map(function(list, index) {
var list_data = list.data;
list_data.map(function(data, index) {
return (
<li key={index}>{data}</li>
)
});
return(
<div key={index}>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
Edit
However, As pointed by the user Martin Dawson in the comment below, This is not always ideal.
So whats the solution then?
Many
You can create a function to generate unique keys/ids/numbers/strings and use that
You can make use of existing npm packages like uuid, uniqid, etc
You can also generate random number like new Date().getTime(); and prefix it with something from the item you're iterating to guarantee its uniqueness
Lastly, I recommend using the unique ID you get from the database, If you get it.
Example:
const generateKey = (pre) => {
return `${ pre }_${ new Date().getTime() }`;
}
const savedLists = this.state.savedLists.map( list => {
const list_data = list.data.map( data => <li key={ generateKey(data) }>{ data }</li> );
return(
<div key={ generateKey(list.name) }>
<h2>{ list.name }</h2>
<ul>
{ list_data }
</ul>
</div>
)
});
It is important to remember that React expects STABLE keys, meaning you should assign the keys once and every item on your list should receive the same key every time, that way React can optimize around your data changes when it is reconciling the virtual DOM and decides which components need to re-render.
So, if you are using UUID you need to do it at the data level, not at the UI level.
Also keep in mind you can use any string you want for the key, so you can often combine several fields into one unique ID, something like ${username}_${timestamp} can be a fine unique key for a line in a chat, for example.
Keys helps React identify which items have changed/added/removed and should be given to the elements inside the array to give the elements a stable identity.
With that in mind, there are basically three different strategies as described bellow:
Static Elements (when you don't need to keep html state (focus, cursor position, etc)
Editable and sortable elements
Editable but not sortable elements
As React Documentation explains, we need to give stable identity to the elements and because of that, carefully choose the strategy that best suits your needs:
STATIC ELEMENTS
As we can see also in React Documentation, is not recommended the use of index for keys "if the order of items may change. This can negatively impact performance and may cause issues with component state".
In case of static elements like tables, lists, etc, I recommend using a tool called shortid.
1) Install the package using NPM/YARN:
npm install shortid --save
2) Import in the class file you want to use it:
import shortid from 'shortid';
2) The command to generate a new id is shortid.generate().
3) Example:
renderDropdownItems = (): React.ReactNode => {
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={shortid.generate()}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
IMPORTANT: As React Virtual DOM relies on the key, with shortid every time the element is re-rendered a new key will be created and the element will loose it's html state like focus or cursor position. Consider this when deciding how the key will be generated as the strategy above can be useful only when you are building elements that won't have their values changed like lists or read only fields.
EDITABLE (sortable) FIELDS
If the element is sortable and you have a unique ID of the item, combine it with some extra string (in case you need to have the same information twice in a page). This is the most recommended scenario.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${item.id}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
EDITABLE (non sortable) FIELDS (e.g. INPUT ELEMENTS)
As a last resort, for editable (but non sortable) fields like input, you can use some the index with some starting text as element key cannot be duplicated.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach((item:any index:number) => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${index}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
Hope this helps.
Do not use this return `${ pre }_${ new Date().getTime()}`;. It's better to have the array index instead of that because, even though it's not ideal, that way you will at least get some consistency among the list components, with the new Date function you will get constant inconsistency. That means every new iteration of the function will lead to a new truly unique key.
The unique key doesn't mean that it needs to be globally unique, it means that it needs to be unique in the context of the component, so it doesn't run useless re-renders all the time. You won't feel the problem associated with new Date initially, but you will feel it, for example, if you need to get back to the already rendered list and React starts getting all confused because it doesn't know which component changed and which didn't, resulting in memory leaks, because, you guessed it, according to your Date key, every component changed.
Now to my answer. Let's say you are rendering a list of YouTube videos. Use the video id (arqTu9Ay4Ig) as a unique ID. That way, if that ID doesn't change, the component will stay the same, but if it does, React will recognize that it's a new Video and change it accordingly.
It doesn't have to be that strict, the little more relaxed variant is to use the title, like Erez Hochman already pointed out, or a combination of the attributes of the component (title plus category), so you can tell React to check if they have changed or not.
edited some unimportant stuff
Let React Assign Keys To Children
You may leverage React.Children API:
const { Children } = React;
const DATA = [
'foo',
'bar',
'baz',
];
const MyComponent = () => (
<div>
{Children.toArray(DATA.map(data => <p>{data}</p>))}
</div>
);
ReactDOM.render(<MyComponent />,document.getElementById("root"));
<div id="root"></div>
<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>
To add the latest solution for 2021...
I found that the project nanoid provides unique string ids that can be used as key while also being fast and very small.
After installing using npm install nanoid, use as follows:
import { nanoid } from 'nanoid';
// Have the id associated with the data.
const todos = [{id: nanoid(), text: 'first todo'}];
// Then later, it can be rendered using a stable id as the key.
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
Another option is weak-key: https://www.npmjs.com/package/weak-key
import weakKey from "weak-key";
const obj1 = {a : 42};
const obj2 = {b : 123};
const obj3 = {a : 42};
console.log(weakKey(obj1)); // 'weak-key-1'
console.log(weakKey(obj2)); // 'weak-key-2'
console.log(weakKey(obj3)); // 'weak-key-3'
console.log(weakKey(obj1)); // 'weak-key-1'
For a simple array of text-strings; I'm trying one of the two ways:
1. encodeURI which is available on both; NodeJS and browser
const WithEncoder = () => {
const getKey = useCallback((str, idx) => encodeURI(`${str},${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
2. window.btoa which is available only in browser.
const WithB2A = () => {
const getKey = useCallback((str, idx) => window.btoa(`${str}-${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
Depends on the situation, choose a uniqueId creator is ok when you just want render silly items, but if you render items like drag&drop etc and you haven't any uniqueId for each item, I recommend remap that data in your redux, mapper, wherever and add for each item an uniqueId (and not in the render like <Item key={...}) because React couldn't perform any check between renders (and with that all the benefits).
With that remapped that you can use that new Id in your Component.
Here is what I have done, it works for reordering, adding, editing and deleting. Once set the key is not changed, so no unnecessary re-render. One PROBLEM which may be a show stopper for some: it requires adding a property to your object at first render say "_reactKey".
Example for functional component in psuedo TS (ie it won't run in snippets):
interface IRow{
myData: string,
_reactKey?:number
}
export default function List(props: {
rows: Array<IRow>
}) {
const {myRows} = props;
const [nextKey, setNextKey] = useState(100);
const [rows, setRows] = useState<Array<IRow>|undefined>();
useEffect(function () {
if (myRows) {
for (let row of myRows){
if (!row._reactKey){
row._reactKey = nextKey;
setNextKey(nextKey+1);
}
}
setRows(myRows);
} else if (!rows) {
setRows([]);
}
}, [myRows, columns]);
addRow(){
let newRow = { blah, blah, _reactKey : nextKey};
setNextKey(nextKey+1);
rows.push(newRow);
setRows({...rows});
}
function MyRow(props:{row:IRow}){
const {row} = props;
return <tr><td>{row._reactKey}</td><td>row.myData</td></tr>
}
return <table>
<tr><th>Index</th><th>React Key</th><th>My Data</th></tr>
rows.map((row, key)=>{
return <MyRow key={row._reactKey} row={row} />
}
</table>
}
I don't use react too much, but the last time I saw this issue I just created a new state array, and tracked the keys there.
const [keys, setKeys] = useState([0]);
const [items, setItems] = useState([value: "", key: 0,])
Then when I add a new item to list, I get the last key from the keys array, add 1, then use setKeys to update the keys array. Something like this:
const addItemWithKey = () => {
// create a new array from the state variable
let newKeyArr = [...keys];
// create a new array from the state variable that needs to be tracked with keys
let newItemArr = [...items];
// get the last key value and add 1
let key = newKeyArr[newKeyArr.length-1] + 1;
newKeyArr.push(key);
newItemArr.push({value: "", key: key,});
// set the state variable
setKeys(newKeyArr);
setItems(newItemArr);
};
I don't worry about removing values from the keys array because it's only being used for iterating in the component, and we're trying to solve for the case where we remove an item from the list and/or add a new item. By getting the last number from the keys array and adding one, we should always have unique keys.
import React, {useState} from 'react';
import {SafeAreaView,ScrollView,StyleSheet,Text,View,Dimensions} from 'react-native';
const {width}=Dimensions.get('window');
function sayfalar(){
let pages=[]
for (let i = 0; i < 100; i++) {
pages.push(<View key={i} style={styles.pages}><Text>{i}</Text></View>)
}
return pages
}
const App=()=>{
return(
<View style={styles.container}>
<ScrollView horizontal={true} pagingEnabled={true}>
{sayfalar()}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container:{
flexDirection:'row',
flex:1
},
pages:{
width:width
}
})
export default App;
You can use react-html-id to generate uniq id easely : https://www.npmjs.com/package/react-html-id
Use the mapped index (i)
things.map((x,i) => {
<div key=i></div>
});
Hope this helps.
The fastest solution in 2021 is to use uniqid: Go to https://www.npmjs.com/package/uniqid for more info but to sum up:
First in your terminal and your project file: npm install uniqid
Import uniqid in your project
Use it in any key that you need!
uniqid = require('uniqid');
return(
<div>
<div key={ uniqid() } id={list.name}>
<h2 key={ uniqid() }>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
I am using this:
<div key={+new Date() + Math.random()}>