I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.
How can I prevent the inputs to lose focus when typing?
Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx
Here is the source code demonstrating the issue:
import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<For each={todos()}>
{(todo, index) => {
console.log('render', index(), todo)
return <div>
<input
value={todo.text}
onInput={event => {
setTodos(todos => {
return replace(todos, index(), {
...todo,
text: event.target.value
})
})
}}
/>
</div>
}}
</For>
Data: {JSON.stringify(todos())}
</div>
</div>
);
}
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
const clone = array.slice(0)
clone[index] = newItem
return clone
}
render(() => <App />, document.getElementById("app")!);
UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx
<For> components keys items of the input array by the reference.
When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.
You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.
const [todos, setTodos] = createStore([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
setTodos(o => o.id === id, "text", text)
}
Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property:
https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key
<Key each={todos()} by="id">
...
</Key>
While #thetarnav solutions work, I want to propose my own.
I would solve it by using <Index>
import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
const clone = array.slice(0);
clone[index] = newItem;
return clone;
}
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<Index each={todos()}>
{(todo, index) => {
console.log("render", index, todo());
return (
<div>
<input
value={todo().text}
onInput={(event) => {
setTodos((todos) => {
return replace(todos, index, {
...todo(),
text: event.target.value
});
});
}}
/>
</div>
);
}}
</Index>
Dat: {JSON.stringify(todos())}
</div>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline.
To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.
This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.
With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.
You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.
The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<ul>
{indexArray(todos, (todo, index) => (
<li>
<input
value={todo().text}
onInput={(event) => {
const text = event.target.value;
setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
}}
/>
</li>
))}
</ul>
);
}
Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.
Related
In a react component I have an array of things. I iterate through that array display the name of the thing in a plain div, then pass each element to another component to display details.
What's happening: if I delete an element from anywhere except the bottom (last array element) the header that is displayed in the main element containing the array is correct (the one I clicked "delete" on disappeared), but the "body" (which is another component) remains. Instead, the inner component is acting as if I deleted the last element of the array and kind of "moves" up the array.
It's hard to describe in words. See example below. Delete the top element or one of the middle ones and see how the header for the section starts not matching the contents.
I'm trying to understand why this is happening.
(EDIT/NOTE: State IS needed in the child component because in real life it's a form and updates the object being passed in. I Just removed the updating here to make the example shorter and simpler)
Example code (delete the middle element of the array and see what happens):
https://codesandbox.io/s/confident-buck-dodvgu?file=/src/App.tsx
Main component:
import { useState, useEffect } from "react";
import InnerComponent from "./InnerComponent";
import Thing from "./Thing";
import "./styles.css";
export default function App() {
const [things, setThings] = useState<Thing[]>([]);
useEffect(() => resetThings(), []);
const resetThings = () => {
setThings([
{ name: "dog", num: 5 },
{ name: "cat", num: 7 },
{ name: "apple", num: 11 },
{ name: "book", num: 1}
]);
};
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
return (
<div className="App">
{things.map((thing, index) => (
<div key={`${index}`} className="thing-container">
<h2>{thing.name}</h2>
<InnerComponent
thing={thing}
index={index}
onDelete={onDeleteThing}
/>
</div>
))}
<div>
<button onClick={resetThings}>Reset Things</button>
</div>
</div>
);
}
Inner component:
import { useEffect, useState } from "react";
import Thing from "./Thing";
interface InnerComponentParams {
thing: Thing;
index: number;
onDelete: (indexToDelete: number) => void;
}
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
const [name, setName] = useState(thing.name);
const [num, setNum] = useState(thing.num);
return (
<div>
<div>Name: {name}</div>
<div>Num: {num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
export default InnerComponent;
You are creating unnecessary states in the child component, which is causing problems when React reconciles the rearranged Things. Because you aren't setting the state in the child component, leave it off entirely - instead, just reference the prop.
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
return (
<div>
<div>Name: {thing.name}</div>
<div>Num: {thing.num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
The other reason this is happening is because your key is wrong here:
{things.map((thing, index) => (
<div key={`${index}`}
Here, you're telling React that when an element of index i is rendered, on future renders, when another element with the same i key is returned, that corresponds to the JSX element from the prior render - which is incorrect, because the indicies do not stay the same. Use a proper key instead, something unique to each object being iterated over - such as the name.
<div key={thing.name}
Using either of these approaches will fix the issue (but it'd be good to use both anyway).
This is also wrong. You're removing everything except the index.
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
Use filter:
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things].filter(
(thing, index) => index !== indexToDelete
);
setThings(newThings);
};
How can I create an array of input elements in react which are being "watched" without triggering the error for using useState outside the body of the FunctionComponent?
if I have the following (untested, simplified example):
interface Foo {
val: string;
setVal: React.Dispatch<React.SetStateAction<string>>;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
const [val, setVal] = useState('')
setAllVals(allVals.concat({val, setVal}))
}
return (
<input type="button" value="Add input" onClick={addVal}>
allVals.map(v => <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>)
)
}
I will get the error Hooks can only be called inside of the body of a function component.
How might I dynamically add "watched" elements in the above code, using FunctionComponents?
Edit
I realise a separate component for each <li> above would be able to solve this problem, but I am attempting to integrate with Microsoft Fluent UI, and so I only have the onRenderItemColumn hook to use, rather than being able to create a separate Component for each list item or row.
Edit 2
in response to Drew Reese's comment: apologies I am new to react and more familiar with Vue and so I am clearly using the wrong terminology (watch, ref, reactive etc). How would I rewrite the code example I provided so that there is:
An add button
Each time the button is pressed, another input element is added.
Each time a new value is entered into the input element, the input element shows the value
There are not excessive or unnecessary re-rendering of the DOM when input elements have their value updated or new input element is added
I have access to all the values in all the input elements. For example, if a separate submit button is pressed I could get an array of all the string values in each input element. In the code I provided, this would be with allVals.map(v => v.val)
const [val, setVal] = useState('') is not allowed. The equivalent effect would be just setting value to a specific index of allVals.
Assuming you're only adding new items to (not removing from) allVals, the following solution would work. This simple snippet just shows you the basic idea, you'll need to adapt to your use case.
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
setAllVals(allVals => {
// `i` would be the fixed index of newly added item
// it's captured in closure and would never change
const i = allVals.length
const setVal = (v) => setAllVals(allVals => {
const head = allVals.slice(0, i)
const target = allVals[i]
const tail = allVals.slice(i+1)
const nextTarget = { ...target, val: v }
return head.concat(nextTarget).concat(tail)
})
return allVals.concat({
val: '',
setVal,
})
})
}
return (
<input type="button" value="Add input" onClick={addVal} />
{allVals.map(v =>
<li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>
)}
)
}
React hooks cannot be called in callbacks as this breaks the Rules of Hooks.
From what I've gathered you want to click the button and dynamically add inputs, and then be able to update each input. You can add a new element to the allVals array in the addVal callback, simply use a functional state update to append a new element to the end of the allVals array and return a new array reference. Similarly, in the updateVal callback use a functional state update to map the previous state array to a new array reference, using the index to match the element you want to update.
interface Foo {
val: string;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([]);
const addVal = () => {
setAllVals((allVals) => allVals.concat({ val: "" }));
};
const updateVal = (index: number) => (e: any) => {
setAllVals((allVals) =>
allVals.map((el, i) =>
i === index
? {
...el,
val: e.target.value
}
: el
)
);
};
return (
<>
<input type="button" value="Add input" onClick={addVal} />
{allVals.map((v, i) => (
<li key={i}>
<input value={v.val} onChange={updateVal(i)} />
</li>
))}
</>
);
}
List of items to render
Given a list of items (coming from the server):
const itemsFromServer = {
"1": {
id: "1",
value: "test"
},
"2": {
id: "2",
value: "another row"
}
};
Function component for each item
We want to render each item, but only when necessary and something changes:
const Item = React.memo(function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
});
ItemList function component with a handleSave function that needs to be memoized.
And there is a possibility to save each individual item:
function ItemList() {
const [items, setItems] = useState(itemsFromServer);
const handleChange = useCallback(
function handleChange(id, value) {
setItems(currentItems => {
return {
...currentItems,
[id]: {
...currentItems[id],
value
}
};
});
},
[setItems]
);
async function handleSave(id) {
const item = items[id];
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
await save(item);
alert("Save done :)");
}
return (
<ul>
{Object.values(items).map(item => (
<Item
key={item.id}
id={item.id}
value={item.value}
onChange={handleChange}
onSave={handleSave}
/>
))}
</ul>
);
}
How to prevent unnecessary re-renders of each Item when only one item changes?
Currently on each render a new handleSave function is created. When using useCallback the items object is included in the dependency list.
Possible solutions
Pass value as parameter to handleSave, thus removing the items object from the dependency list of handleSave. In this example that would be a decent solution, but for multiple reasons it's not preferred in the real life scenario (eg. lots more parameters etc.).
Use a separate component ItemWrapper where the handleSave function can be memoized.
function ItemWrapper({ item, onChange, onSave }) {
const memoizedOnSave = useCallback(onSave, [item]);
return (
<Item
id={item.id}
value={item.value}
onChange={onChange}
onSave={memoizedOnSave}
/>
);
}
With the useRef() hook, on each change to items write it to the ref and read items from the ref inside the handleSave function.
Keep a variable idToSave in the state. Set this on save. Then trigger the save function with useEffect(() => { /* save */ }, [idToSave]). "Reactively".
Question
All of the solutions above seem not ideal to me. Are there any other ways to prevent creating a new handleSave function on each render for each Item, thus preventing unnecessary re-renders? If not, is there a preferred way to do this?
CodeSandbox: https://codesandbox.io/s/wonderful-tesla-9wcph?file=/src/App.js
The first question I'd like to ask : is it really a problem to re-render ?
You are right that react will re-call every render for every function you have here, but your DOM should not change that much it might not be a big deal.
If you have heavy calculation while rendering Item, then you can memoize the heavy calculations.
If you really want to optimize this code, I see different solutions here:
Simplest solution : change the ItemList to a class component, this way handleSave will be an instance method.
Use an external form library that should work fine: you have powerfull form libraries in final-form, formik or react-hook-form
Another external library : you can try recoiljs that has been build for this specific use-case
Wow this was fun! Hooks are very different then classes. I got it to work by changing your Item component.
const Item = React.memo(
function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
},
(prevProps, nextProps) => {
// console.log("PrevProps", prevProps);
// console.log("NextProps", nextProps);
return prevProps.value === nextProps.value;
}
);
By adding the second parameter to React.memo it only updates when the value prop changes. The docs here explain that this is the equivalent of shouldComponentUpdate in classes.
I am not an expert at Hooks so anyone who can confirm or deny my logic, please chime in and let me know but I think that the reason this needs to be done is because the two functions declared in the body of the ItemList component (handleChange and handleSave) are in fact changing on each render. So when the map is happening, it passes in new instances each time for handleChange and handleSave. The Item component detects them as changes and causes a render. By passing the second parameter you can control what the Item component is testing and only check for the value prop being different and ignore the onChange and onSave.
There might be a better Hooks way to do this but I am not sure how. I updated the code sample so you can see it working.
https://codesandbox.io/s/keen-roentgen-5f25f?file=/src/App.js
I've gained some new insights (thanks Dan), and I think I prefer something like this below. Sure it might look a bit complicated for such a simple hello world example, but for real world examples it might be a good fit.
Main changes:
Use a reducer + dispatch for keeping state. Not required, but to make it complete. Then we don't need useCallback for the onChange handler.
Pass down dispatch via context. Not required, but to make it complete. Otherwise just pass down dispatch.
Use an ItemWrapper (or Container) component. Adds an additional component to the tree, but provides value as the structure grows. It also reflects the situation we have: each item has a save functionality that requires the entire item. But the Item component itself does not. ItemWrapper might be seen as something like a save() provider in this scenario ItemWithSave.
To reflect a more real world scenario there is now also a "item is saving" state and the other id that's only used in the save() function.
The final code (also see: https://codesandbox.io/s/autumn-shape-k66wy?file=/src/App.js).
Intial state, items from server
const itemsFromServer = {
"1": {
id: "1",
otherIdForSavingOnly: "1-1",
value: "test",
isSaving: false
},
"2": {
id: "2",
otherIdForSavingOnly: "2-2",
value: "another row",
isSaving: false
}
};
A reducer to manage state
function reducer(currentItems, action) {
switch (action.type) {
case "SET_VALUE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
value: action.value
}
};
case "START_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: true
}
};
case "STOP_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: false
}
};
default:
throw new Error();
}
}
Our ItemList to render all items from the server
export default function ItemList() {
const [items, dispatch] = useReducer(reducer, itemsFromServer);
return (
<ItemListDispatch.Provider value={dispatch}>
<ul>
{Object.values(items).map(item => (
<ItemWrapper key={item.id} item={item} />
))}
</ul>
</ItemListDispatch.Provider>
);
}
The main solution ItemWrapper or ItemWithSave
function ItemWrapper({ item }) {
const dispatch = useContext(ItemListDispatch);
const handleSave = useCallback(
// Could be extracted entirely
async function save() {
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
dispatch({ type: "START_SAVE", id: item.id });
// Save to API
// eg. this will use otherId that's not necessary for the Item component
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: "STOP_SAVE", id: item.id });
},
[item, dispatch]
);
return (
<Item
id={item.id}
value={item.value}
isSaving={item.isSaving}
onSave={handleSave}
/>
);
}
Our Item
const Item = React.memo(function Item({ id, value, isSaving, onSave }) {
const dispatch = useContext(ItemListDispatch);
console.log("render", id);
if (isSaving) {
return <li>Saving...</li>;
}
function onChange(event) {
dispatch({ type: "SET_VALUE", id, value: event.target.value });
}
return (
<li>
<input value={value} onChange={onChange} />
<button onClick={onSave}>Save</button>
</li>
);
});
I just started React, and in this Item list tutorial I have some question about updating the states of the item. Also, I'm using functional component .So in app.js
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false
},
...
])
Then I have a function in app.js to update the bought to true or false when I check a box
// The id is passed in from Item.js down below
const markBought = (id) => {
setItems(
items.map(
item => {
if (item.id === id) {
/// If bought is false, checking it will make it true and vice versa
item.bought = !item.bought; // (1)
}
return item; // (2)
})
);
};
return (
<div className="App">
<Items items={items} markBought={markBought}></Items>
</div>
);
The teacher said we are using something called Component Drilling. So in Items.js, we map through every item to display them one by one, but I don't think it is neccessary to show.
Finally in Item.js
<input type="checkbox" onChange={() => props.markBought(props.item.id)} />
{props.item.title}
The application worked perfectly, but it's a little bit confusing for me. So:
In app.js, after we change the bought status, shouldn't we also need to return item, the same way we return the item if the condition is false? Why only return the item when if is wrong, but when it is right we only change it without a return?
I read that map will not modify the array, so markBought function should create a new items array, with the bought modified already, but what happens to this array, how do React know to "props" this to item.js, rather than the ones I hard coded?
Sorry if this is a little bit long, any help will be really appreciated. Thanks for reading
You are mutating an item in your map, if you optimized your Item component to be a pure component then that component won't re render because of the mutation. Try the following instead:
//use useCallback so marBought doesn't change and cause
// needless DOM re renders
const markBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? { ...item, bought: !item.bought } //copy item with changed value
: item //not this item, just return the item
)
);
}, []);
Here is a full example:
const { useCallback, useState, useRef, memo } = React;
function Items() {
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false,
},
{
id: 2,
title: 'Peach',
bought: false,
},
]);
const toggleBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? { ...item, bought: !item.bought } //copy item with changed value
: item //not this item, just return the item
)
);
}, []);
return (
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
toggleBought={toggleBought}
/>
))}
</div>
);
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
const renderedRef = useRef(0);
renderedRef.current++;
return (
<div>
<div>{item.title}</div>
<div>bought: {item.bought ? 'yes' : 'no'}</div>
<button onClick={() => toggleBought(item.id)}>
toggle bought
</button>
<div>Rendered: {renderedRef.current} times</div>
</div>
);
});
//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is a broken example where you mutate the item and won't see the re render even though the state did change:
const { useCallback, useState, useRef, memo } = React;
function Items() {
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false,
},
{
id: 2,
title: 'Peach',
bought: false,
},
]);
const toggleBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? ((item.bought = !item.bought),item) //mutate item
: item //not this item, just return the item
)
);
}, []);
return (
<div>
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
toggleBought={toggleBought}
/>
))}
</div>
<div>{JSON.stringify(items)}</div>
</div>
);
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
const renderedRef = useRef(0);
renderedRef.current++;
return (
<div>
<div>{item.title}</div>
<div>bought: {item.bought ? 'yes' : 'no'}</div>
<button onClick={() => toggleBought(item.id)}>
toggle bought
</button>
<div>Rendered: {renderedRef.current} times</div>
</div>
);
});
//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Hi there and welcome to Stackoverflow.
You are always returning the item. You just have an if statement that will change the bought state and item will get returned even if the condition above was false, which is the correct way of doing.
Map will indeed not modify the array but return a new one. If you want to get that returning array you could simply do :
const myNewArray = items.map(...)
The way this new array is getting to your other component is because this new array is given to your useState().
You see setItems() ? This will set your state and Item.js will automatically be updated.
That is what is so great about react. All components that are served from state will be updated once this state is updated.
Your map function always returns an item. It just modifies the item first if the item you're modifying matches the id of the item currently being mapped. Map returns a new array of items (even if it doesn't change anything), which causes useState to see a new value. By default, in React, the check for updates isn't very clever - it's just checking if oldValue === newValue.
For primitives like strings, object equality tests return true for two separate objects, as long as their values match.
"foo" === "foo" // => true
However, this isn't true for object or arrays. Two different arrays containing the same values will not compare as equal (because Javascript isn't comparing their contents, but rather their object IDs):
["foo"] === ["foo"] // => false
So, when you map your items, you get a new array object (because recall: map collects the return values of the callback function into a new array), which will never match the previous value of items, so every call to setItems will cause React to say "hm, my previous items isn't the same object as my new items, I must re-render this component".
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()}>