12 Prompts for Building React/Next.js Apps Faster

Introduction

Building React and Next.js applications efficiently requires more than just knowing the framework—it's about leveraging the right prompts to generate quality code quickly. Whether you're scaffolding a new component, handling state, or optimizing server-side rendering, a well-crafted prompt can save hours. This article collects 12 battle-tested prompts I use daily in my frontend workflow. Each prompt is paired with a concrete example and the code it produces, so you can drop them straight into your AI assistant or IDE.

1. Generate a Basic React Functional Component

Prompt: "Create a React functional component for a card that accepts title, description, and imageUrl props. Include TypeScript types. Use Tailwind CSS for styling."

Example output:

interface CardProps {
  title: string;
  description: string;
  imageUrl: string;
}

export const Card: React.FC<CardProps> = ({ title, description, imageUrl }) => {
  return (
    <div className="rounded-lg shadow-md overflow-hidden">
      <img src={imageUrl} alt={title} className="w-full h-48 object-cover" />
      <div className="p-4">
        <h3 className="text-lg font-semibold">{title}</h3>
        <p className="text-gray-600">{description}</p>
      </div>
    </div>
  );
};

2. Add useState with Logical Initializer

Prompt: "Write a React component with useState that initializes a counter from localStorage and provides increment/decrement buttons. Use TypeScript."

Example output:

import { useState } from 'react';

export const Counter: React.FC = () => {
  const [count, setCount] = useState<number>(() => {
    const stored = localStorage.getItem('count');
    return stored ? parseInt(stored, 10) : 0;
  });

  const increment = () => setCount(prev => {
    const newVal = prev + 1;
    localStorage.setItem('count', newVal.toString());
    return newVal;
  });

  const decrement = () => setCount(prev => {
    const newVal = prev - 1;
    localStorage.setItem('count', newVal.toString());
    return newVal;
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

3. useEffect for Data Fetching with Cleanup

Prompt: "Create a custom hook useFetch that takes a URL, returns { data, loading, error }, and cancels the request on unmount using AbortController."

Example output:

import { useState, useEffect } from 'react';

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

export function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error('Network response was not ok');
        return res.json();
      })
      .then(json => setData(json))
      .catch(err => {
        if (err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

4. Next.js Page with getServerSideProps

Prompt: "Write a Next.js page that fetches user data from an external API on every request using getServerSideProps. Display name and email. Handle 404 and loading states."

Example output:

import { GetServerSideProps } from 'next';

interface User {
  name: string;
  email: string;
}

export default function UserPage({ user, error }: { user?: User; error?: string }) {
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Loading...</div>;
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.params!;
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (res.status === 404) return { props: { error: 'User not found' } };
  const user: User = await res.json();
  return { props: { user } };
};

5. Next.js API Route with Validation

Prompt: "Create a Next.js API route that accepts POST requests with email and password, validates input (email format, password length >= 8), and returns appropriate errors or a success response."

Example output:

import type { NextApiRequest, NextApiResponse } from 'next';

type Data = { message: string } | { error: string };

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' });

  const { email, password } = req.body;

  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    return res.status(400).json({ error: 'Invalid email' });
  }
  if (!password || password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters' });
  }

  return res.status(200).json({ message: 'Valid input' });
}

6. Context Provider with useReducer

Prompt: "Build an auth context with useReducer that stores user state (null or user object) and provides login and logout actions. Include TypeScript types."

Example output:

import React, { createContext, useReducer, useContext } from 'react';

interface User { id: string; name: string; }
type AuthState = { user: User | null };
type Action = { type: 'LOGIN'; user: User } | { type: 'LOGOUT' };

const authReducer = (state: AuthState, action: Action): AuthState => {
  switch (action.type) {
    case 'LOGIN': return { user: action.user };
    case 'LOGOUT': return { user: null };
    default: return state;
  }
};

const AuthContext = createContext<{ state: AuthState; dispatch: React.Dispatch<Action> } | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(authReducer, { user: null });
  return <AuthContext.Provider value={{ state, dispatch }}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}

7. Form Handling with react-hook-form and Zod

Prompt: "Create a login form using react-hook-form and zod validation. Show errors inline. Submit calls an API and displays success/error."

Example output:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

type FormData = z.infer<typeof schema>;

export default function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
  });

  const onSubmit = async (data: FormData) => {
    const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify(data) });
    const json = await res.json();
    alert(json.message || json.error);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}
      <input type="password" {...register('password')} placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">Login</button>
    </form>
  );
}

8. Next.js Dynamic Route with getStaticProps and getStaticPaths

Prompt: "Build a blog post page with getStaticProps and getStaticPaths that fetches posts from a local JSON file. Include fallback: 'blocking'."

Example output:

import { GetStaticProps, GetStaticPaths } from 'next';
import postsData from '../../data/posts.json';

interface Post { id: string; title: string; content: string; }

export default function PostPage({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = postsData.map((post: Post) => ({ params: { id: post.id } }));
  return { paths, fallback: 'blocking' };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const post = postsData.find((p: Post) => p.id === params!.id);
  if (!post) return { notFound: true };
  return { props: { post } };
};

9. Custom Hook for Intersection Observer

Prompt: "Write a custom hook useInView that uses IntersectionObserver to return whether an element is visible. Accept options like threshold."

Example output:

import { useRef, useState, useEffect } from 'react';

export function useInView(options?: IntersectionObserverInit): [React.RefObject<HTMLDivElement>, boolean] {
  const ref = useRef<HTMLDivElement>(null!);
  const [inView, setInView] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), options);
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return [ref, inView];
}

// Usage:
// const [ref, inView] = useInView({ threshold: 0.5 });
// <div ref={ref}>{inView ? 'Visible' : 'Hidden'}</div>

10. Generate a Table Component with Sorting and Filtering

Prompt: "Create a reusable React table component that accepts columns config and data array. Support sortable columns and a text filter input. Use useState and useMemo for efficiency."

Example output (simplified):

import { useState, useMemo } from 'react';

interface Column { key: string; label: string; sortable?: boolean; }
interface TableProps { columns: Column[]; data: Record<string, any>[]; }

export const SortableTable: React.FC<TableProps> = ({ columns, data }) => {
  const [sortKey, setSortKey] = useState<string | null>(null);
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
  const [filter, setFilter] = useState('');

  const filteredData = useMemo(() => {
    return data.filter(row =>
      Object.values(row).some(val => String(val).toLowerCase().includes(filter.toLowerCase()))
    );
  }, [data, filter]);

  const sortedData = useMemo(() => {
    if (!sortKey) return filteredData;
    return [...filteredData].sort((a, b) => {
      if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
      if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
      return 0;
    });
  }, [filteredData, sortKey, sortOrder]);

  const handleSort = (key: string) => {
    if (sortKey === key) setSortOrder(o => o === 'asc' ? 'desc' : 'asc');
    else { setSortKey(key); setSortOrder('asc'); }
  };

  return (
    <>
      <input placeholder="Filter..." onChange={e => setFilter(e.target.value)} />
      <table>
        <thead>
          <tr>
            {columns.map(col => (
              <th key={col.key} onClick={() => col.sortable && handleSort(col.key)}>
                {col.label} {sortKey === col.key ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {sortedData.map((row, i) => (
            <tr key={i}>
              {columns.map(col => <td key={col.key}>{row[col.key]}</td>)}
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
};

11. Next.js Middleware for Protected Routes

Prompt: "Write a Next.js middleware that reads a cookie token, and if missing, redirects to /login for any route under /dashboard."

Example output:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const token = req.cookies.get('token');
  if (req.nextUrl.pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: '/dashboard/:path*',
};

12. Optimistic Updates with React Query

Prompt: "Create a custom mutation hook using @tanstack/react-query that updates a list of items optimistically. On error, rollback to previous data."

Example output:

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface Todo { id: number; text: string; completed: boolean; }

export function useToggleTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (todo: Todo) => 
      fetch(`/api/todos/${todo.id}`, { 
        method: 'PATCH', 
        body: JSON.stringify({ completed: !todo.completed }) 
      }).then(res => res.json()),
    onMutate: async (updatedTodo) => {
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      const previous = queryClient.getQueryData<Todo[]>(['todos']);
      if (previous) {
        queryClient.setQueryData(['todos'], previous.map(t =>
          t.id === updatedTodo.id ? { ...t, completed: !t.completed } : t
        ));
      }
      return { previous };
    },
    onError: (err, updatedTodo, context) => {
      if (context?.previous) queryClient.setQueryData(['todos'], context.previous);
    },
    onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  });
}

Conclusion

These 12 prompts cover the most common patterns I encounter when building React and Next.js applications. They are designed to be direct and actionable—copy them, tweak the variable names or API endpoints, and you'll have working code in seconds. Remember to always verify generated code against your project's existing patterns and dependencies. The real power comes from combining these prompts with your own domain logic. Try them out in your next project and see how much time you save.

Further reading: React documentation (https://react.dev), Next.js documentation (https://nextjs.org/docs), React Query docs (https://tanstack.com/query/latest).

← All posts

Comments