Last Updated: 3/9/2026
React Integration
Best practices for using Nano ID in React applications.
Quick Start
npm install nanoidimport { nanoid } from 'nanoid'
import { useState } from 'react'
function App() {
const [items, setItems] = useState([])
const addItem = (text) => {
setItems([...items, { id: nanoid(), text }])
}
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
)
}❌ Don’t: Generate IDs During Render
This is wrong and will cause issues:
function Todos({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={nanoid()}> {/* ❌ DON'T DO THIS */}
{todo.text}
</li>
))}
</ul>
)
}Why This Is Bad
- Keys change on every render - React can’t track which items changed
- Performance issues - Unnecessary re-renders and DOM updates
- Lost component state - Controlled inputs lose focus, animations restart
- Breaks React features - Transitions, Suspense, and concurrent features fail
✅ Do: Use Stable IDs
Best: IDs from Your Data
If your data already has IDs, use them:
function Todos({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}> {/* ✅ GOOD */}
{todo.text}
</li>
))}
</ul>
)
}Good: Generate IDs When Creating Data
import { nanoid } from 'nanoid'
import { useState } from 'react'
function TodoList() {
const [todos, setTodos] = useState([])
const addTodo = (text) => {
const newTodo = {
id: nanoid(), // ✅ Generated once when creating
text,
completed: false
}
setTodos([...todos, newTodo])
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}Acceptable: Use Array Index (Last Resort)
If you truly have no stable IDs and never reorder/filter:
function StaticList({ items }) {
return (
<ul>
{items.map((text, index) => (
<li key={index}> {/* ⚠️ Only if items never change order */}
{text}
</li>
))}
</ul>
)
}⚠️ Index as key is problematic if you:
- Reorder items (drag-and-drop, sorting)
- Filter items
- Add/remove items from the middle
- Paginate
Common Patterns
Form with Dynamic Fields
import { nanoid } from 'nanoid'
import { useState } from 'react'
function DynamicForm() {
const [fields, setFields] = useState([
{ id: nanoid(), value: '' }
])
const addField = () => {
setFields([...fields, { id: nanoid(), value: '' }])
}
const updateField = (id, value) => {
setFields(fields.map(field =>
field.id === id ? { ...field, value } : field
))
}
const removeField = (id) => {
setFields(fields.filter(field => field.id !== id))
}
return (
<div>
{fields.map(field => (
<div key={field.id}>
<input
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
/>
<button onClick={() => removeField(field.id)}>Remove</button>
</div>
))}
<button onClick={addField}>Add Field</button>
</div>
)
}API Data with Generated IDs
import { nanoid } from 'nanoid'
import { useState, useEffect } from 'react'
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
// Add client-side IDs if API doesn't provide them
const usersWithIds = data.map(user => ({
id: user.id || nanoid(), // Use API ID or generate
...user
}))
setUsers(usersWithIds)
})
}, [])
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}Optimistic UI Updates
import { nanoid } from 'nanoid'
import { useState } from 'react'
function CommentList() {
const [comments, setComments] = useState([])
const addComment = async (text) => {
const tempId = nanoid()
const optimisticComment = {
id: tempId,
text,
pending: true
}
// Add immediately to UI
setComments([...comments, optimisticComment])
try {
// Send to server
const response = await fetch('/api/comments', {
method: 'POST',
body: JSON.stringify({ text })
})
const savedComment = await response.json()
// Replace temp ID with server ID
setComments(comments.map(c =>
c.id === tempId ? { ...savedComment, pending: false } : c
))
} catch (error) {
// Remove on error
setComments(comments.filter(c => c.id !== tempId))
}
}
return (
<ul>
{comments.map(comment => (
<li key={comment.id} className={comment.pending ? 'pending' : ''}>
{comment.text}
</li>
))}
</ul>
)
}React 18: useId Hook
For linking labels and inputs (not for list keys!):
import { useId } from 'react'
function FormField({ label }) {
const id = useId() // React's built-in hook
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
)
}⚠️ useId() is NOT for list keys. It’s for associating labels/inputs within a single component.
Use Nano ID for:
- List keys
- Database IDs
- Data that persists
- IDs shared across components
Use useId() for:
- Label/input associations
- Accessibility IDs (aria-describedby, etc.)
- Component-internal IDs
Server Components (Next.js, React Server Components)
import { nanoid } from 'nanoid'
// Server Component
export default async function Page() {
const data = await fetchData()
// Generate IDs on the server
const itemsWithIds = data.map(item => ({
id: nanoid(),
...item
}))
return (
<ul>
{itemsWithIds.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}State Management (Redux, Zustand, etc.)
Redux Toolkit
import { createSlice } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
state.push(action.payload)
},
prepare(text) {
return {
payload: {
id: nanoid(),
text,
completed: false
}
}
}
}
}
})Zustand
import create from 'zustand'
import { nanoid } from 'nanoid'
const useStore = create((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: nanoid(),
text,
completed: false
}]
}))
}))TypeScript
Type-safe IDs:
import { nanoid } from 'nanoid'
interface Todo {
id: string
text: string
completed: boolean
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([])
const addTodo = (text: string) => {
const newTodo: Todo = {
id: nanoid(),
text,
completed: false
}
setTodos([...todos, newTodo])
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}See TypeScript Usage for advanced type patterns.
Performance Tips
Memoize ID Generation
If you’re generating many IDs in a loop:
import { nanoid } from 'nanoid'
import { useMemo } from 'react'
function LargeList({ data }) {
const itemsWithIds = useMemo(() =>
data.map(item => ({
id: nanoid(),
...item
})),
[data] // Only regenerate if data changes
)
return (
<ul>
{itemsWithIds.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}Use Non-Secure for Client-Only Temporary IDs
For temporary UI-only IDs that never leave the browser:
import { nanoid } from 'nanoid/non-secure'
import { useState } from 'react'
function TempList() {
const [items, setItems] = useState([])
const addItem = (text) => {
setItems([...items, {
id: nanoid(), // Faster, but don't send to server
text
}])
}
return <ul>{/* ... */}</ul>
}⚠️ Only for IDs that:
- Never get saved to a database
- Never leave the client
- Are truly temporary (session-only)
Related
- React Native Setup - React Native-specific configuration
- TypeScript Usage - Type-safe patterns
- Core API - nanoid() function details
- Quick Start Guide - Basic usage