Skip to Content
GuidesReact Integration

Last Updated: 3/9/2026


React Integration

Best practices for using Nano ID in React applications.

Quick Start

npm install nanoid
import { 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

  1. Keys change on every render - React can’t track which items changed
  2. Performance issues - Unnecessary re-renders and DOM updates
  3. Lost component state - Controlled inputs lose focus, animations restart
  4. 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)