use useOptimistic hook in react for WishlistHeart and instead of fetching getwishlist each time, introduce a newprop isInWishlist...
and fetch it once in productlist...
WishlistHeart.jsx:
import React, { useState, useEffect } from 'react';
import { Heart } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import {
    AlertDialog,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";

const WishlistHeart = ({ product }) => {
    const [isInWishlist, setIsInWishlist] = useState(false);
    const [showDialog, setShowDialog] = useState(false);
    const [isRedirecting, setIsRedirecting] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    const router = useRouter();
    useEffect(() => {
        checkWishlistStatus();
    }, [product.id]);

    const checkWishlistStatus = async () => {
        const token = localStorage.getItem("token");
        if (!token) return;

        try {
            const response = await fetch(`${process.env.NEXT_PUBLIC_API}/wishlist`, {
                headers: {
                    'Authorization': `Bearer ${token}`,
                },
            });
            if (response.ok) {
                const { wishlist } = await response.json();
                setIsInWishlist(wishlist.some(item => item.id === product.id));
            }
        } catch (error) {
            console.error('Error checking wishlist status:', error);
        }
    };

    const handleClick = async (e) => {
        e.stopPropagation();
        if (isLoading) return;

        const token = localStorage.getItem("token");
        if (!token) {
            setShowDialog(true);
            return;
        }

        setIsLoading(true);
        const method = isInWishlist ? "DELETE" : "POST";

        try {
            const response = await fetch(`${process.env.NEXT_PUBLIC_API}/wishlist`, {
                method,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${token}`,
                },
                body: JSON.stringify({ product_id: product.id }),
            });

            if (response.ok) {
                setIsInWishlist(!isInWishlist);
            } else {
                throw new Error('Failed to update wishlist');
            }
        } catch (error) {
            console.error('Error updating wishlist:', error);
        } finally {
            setIsLoading(false);
        }
    };

    const handleSignIn = (e) => {
        e.preventDefault();
        setIsRedirecting(true);
        setTimeout(() => {
            router.push('/signin');
        }, 1000);
    };

    const handleCancel = () => {
        setShowDialog(false);
    };

    return (
        <>
            <div className="relative">
                <motion.div
                    whileTap={{ scale: 0.8 }}
                    transition={{ duration: 0.3 }}
                >
                    <Heart
                        className={`w-[1.85rem] h-[1.85rem] z-10 stroke-[1.2] ${
                            isInWishlist
                                ? "fill-[#C91C1C] stroke-[#C91C1C]"
                                : "fill-transparent stroke-[#C91C1C] hover:fill-[#C91C1C]"
                        } cursor-pointer transition-colors duration-300`}
                        onClick={handleClick}
                    />
                </motion.div>
            </div>
            <AlertDialog open={showDialog} onOpenChange={setShowDialog}>
                <AlertDialogContent className="z-50">
                    <AlertDialogHeader>
                        <AlertDialogTitle className="font-sans font-medium">Sign in required</AlertDialogTitle>
                        <AlertDialogDescription className="font-serif">
                            You need to sign in first to add items to your wishlist.
                        </AlertDialogDescription>
                    </AlertDialogHeader>
                    <AlertDialogFooter className="font-serif">
                        <Button variant="outline" onClick={handleCancel}>Cancel</Button>
                        <Button onClick={handleSignIn} disabled={isRedirecting}>
                            {isRedirecting ? 'Redirecting' : 'Sign In'}
                        </Button>
                    </AlertDialogFooter>
                </AlertDialogContent>
            </AlertDialog>
        </>
    );
};

export default WishlistHeart;
ProductList.jsx:
'use client';

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { usePathname } from 'next/navigation';
import { useInView } from 'react-intersection-observer';
import dynamic from 'next/dynamic';
import ProductItem from './ProductItem';
import ProductLoading from '@/components/ProductLoading';
import NoProductsFound from '@/components/NoProductsFound';
import ProductDetail from '@/components/ProductDetail';
import { NotificationProvider } from '@/components/NotificationContext';

const SearchComponent = dynamic(() => import('@/components/SearchComponent'), {
    ssr: false,
    loading: () => <SearchComponentSkeleton />
});

const ProductModal = dynamic(() => import('@/components/ProductDetailModal'), {
    ssr: false
});

export default function ProductList({ initialFilters }) {
    const { c, sc, b, title, sortBy } = initialFilters;
    const [products, setProducts] = useState([]);
    const [page, setPage] = useState(1);
    const [hasMore, setHasMore] = useState(true);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [filter, setFilter] = useState('');
    const [query, setQuery] = useState('');
    const [minPrice, setMinPrice] = useState(0);
    const [maxPrice, setMaxPrice] = useState(250000); // Adjust based on your data
    const [detail, setDetail] = useState(null);
    const [selectedProduct, setSelectedProduct] = useState(null);
    const first = useRef(true);
    const { ref, inView } = useInView({
        threshold: 0,
        triggerOnce: false
    });

    const pathname = usePathname(); // Import and use usePathname

    const fetchProducts = useCallback(async (currentPage, searchQuery) => { // Added searchQuery
        setLoading(true);
        setError(null);
        try {
            const params = new URLSearchParams({
                page: currentPage,
                itemsPerPage: 15, // Can be adjusted or made dynamic
            });

            if (c) params.append('c', c);
            if (sc) params.append('sc', sc);
            if (b) params.append('b', b);
            if (sortBy) params.append("sortBy", sortBy);
            if (searchQuery) params.append('search', searchQuery); // Use searchQuery
            if (minPrice) params.append('minPrice', minPrice);
            if (maxPrice && maxPrice !== Infinity) params.append('maxPrice', maxPrice);
            if (filter) params.append('filter', filter); // Ensure filter is appended correctly

            console.log(`Fetching products with filter: ${filter}`);
            console.log(`${process.env.NEXT_PUBLIC_API}/products?${params.toString()}`);

            const response = await fetch(`${process.env.NEXT_PUBLIC_API}/products?${params.toString()}&${filter}`);
            if (!response.ok) {
                throw new Error('Failed to fetch products');
            }
            const data = await response.json();
            setProducts((prev) => [...prev, ...data.products]);
            setHasMore(data.hasMore);
            setPage((prev) => prev + 1);

            // After the initial fetch, set first.current to false
            if (first.current) {
                first.current = false;
            }
        } catch (err) {
            console.error('Error fetching products:', err);
            setError(err.message || 'An error occurred');
        } finally {
            setLoading(false);
        }
    }, [c, sc, b, minPrice, maxPrice, filter]);

    // Initial fetch on mount
    useEffect(() => {
        fetchProducts(page, query); // Pass current query
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []); // Only run on mount

    // Fetch more products when in view
    useEffect(() => {
        if (inView && hasMore && !loading) {
            fetchProducts(page, query); // Pass current query
        }
    }, [inView, hasMore, loading, fetchProducts, page, query]);

    // Handle search functionality
    const handleSearch = useCallback((newQuery) => {
        setQuery(newQuery);
        setProducts([]);
        setPage(1);
        setHasMore(true);
        first.current = true;
        fetchProducts(1, newQuery); // Pass newQuery
    }, [fetchProducts]);

    // Handle filter changes
    const handleFilterChange = useCallback(() => {
        setProducts([]);
        setPage(1);
        setHasMore(true);
        first.current = true; // Reset first ref when filtering
        fetchProducts(1, query); // Pass current query
    }, [fetchProducts, query]);

    // Watch for filter changes
    useEffect(() => {
        if (!first.current) {
            handleFilterChange();
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [filter]);

    useEffect(() => {
        if (pathname === '/products') {
            setDetail(null);
        }
    }, [pathname]);

    const handleProductClick = useCallback((product) => {
        setDetail(product);
        const newURL = `/products/${product.id}`;
        window.history.pushState({ path: newURL }, '', newURL);
    }, []);

    useEffect(() => {
        const handlePopState = (event) => {
            const currentPath = window.location.pathname;
            if (currentPath === '/products') {
                setDetail(null);
            } else {
            }
        };

        window.addEventListener('popstate', handlePopState);

        return () => {
            window.removeEventListener('popstate', handlePopState);
        };
    }, []);

    // Handle cart interactions
    const handleCartClick = useCallback((product) => {
        setSelectedProduct(product);
    }, []);

    const handleModalClose = useCallback(() => {
        setSelectedProduct(null);
    }, []);

    const memoizedProducts = useMemo(() => products.map((product) => ({
        ...product,
        cheapestPrice: getCheapestPrice(product),
    })), [products]);

    return (
        <>
            {detail ? (
                <NotificationProvider>
                    <ProductDetail product={detail} />
                </NotificationProvider>
            ) : (
                <>
                    <header className="flex justify-center relative mt-20 mb-4">
                        <h1 className="text-3xl z-10 text-[#181717] font-sans text-center font-medium">
                            {title || 'All Products'}
                        </h1>
                    </header>
                    <SearchComponent
                        minPrice={minPrice}
                        maxPrice={maxPrice}
                        query={query}
                        setQuery={handleSearch}
                        filterQuery={filter}
                        setFilterQuery={setFilter}
                    />
                    {error && <div className="text-red-500 text-center">{error}</div>}
                    {memoizedProducts.length > 0 ? (
                        <div className="grid grid-cols-2 w-full justify-between gap-4 sm:gap-6 ssm3:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
                            {memoizedProducts.map((product) => (
                                <ProductItem
                                    key={product.id}
                                    id={product.id}
                                    name={product.name}
                                    product={product}
                                    handleClick={() => handleProductClick(product)}
                                    price={formatPrice(product.cheapestPrice)}
                                    imageUrl={product.images[0]?.url || ''}
                                    onCartClick={handleCartClick}
                                />
                            ))}
                        </div>
                    ) : (
                        // Only show NoProductsFound if not loading and not the first load
                        !loading && !first.current && <NoProductsFound />
                    )}
                    {loading && <ProductLoading />}
                    {!loading && hasMore && (
                        <div ref={ref} style={{ height: '20px' }}></div>
                    )}
                </>
            )}
            {selectedProduct && (
                <NotificationProvider>
                    <ProductModal
                        productData={{ product: selectedProduct }}
                        isOpen={!!selectedProduct}
                        onClose={handleModalClose}
                    />
                </NotificationProvider>
            )}
        </>
    );
}

const getCheapestPrice = (product) => {
    const prices = [
        product.price && product.price !== '0.00'
            ? parseFloat(product.price)
            : Infinity,
        ...(product.sizes?.map((size) => parseFloat(size.price)) || []),
        ...(product.colors?.flatMap((color) => [
            parseFloat(color.price),
            ...(color.sizes?.map((size) => parseFloat(size.price)) || []),
        ]) || []),
    ].filter((price) => !isNaN(price) && isFinite(price));

    return prices.length > 0 ? Math.min(...prices) : null;
};

const formatPrice = (price) => {
    if (price == null || price === Infinity) return 'N/A';
    return `${price >= 10000 ? price.toLocaleString() : price} IQD`;
};

const SearchComponentSkeleton = () => (
    <div className="flex mb-4 items-center space-x-2 animate-pulse">
        <div className="h-10 bg-gray-200 rounded-md flex-grow"></div>
        <div className="h-10 w-[5.5rem] bg-gray-200 rounded-md"></div>
    </div>
);
ProductItem.jsx:
// /components/ProductItem.jsx

'use client';

import React, { memo } from 'react';
import Image from 'next/image';
import WishlistHeart from "@/components/WishlistHeart";

const CartIcon = memo(() => (
    <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect width="32" height="32" rx="16" fill="#3B5345"/>
        <path d="M12.5715 12.7009V11.9618C12.5715 10.2476 13.9506 8.56375 15.6649 8.40375C17.7068 8.20565 19.4287 9.81327 19.4287 11.8171V12.8685" stroke="white" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round"/>
        <path d="M13.7143 23.619H18.2858C21.3486 23.619 21.8972 22.3923 22.0572 20.899L22.6286 16.3276C22.8343 14.4685 22.301 12.9523 19.0477 12.9523H12.9524C9.69909 12.9523 9.16576 14.4685 9.37147 16.3276L9.9429 20.899C10.1029 22.3923 10.6515 23.619 13.7143 23.619Z" stroke="white" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="
round"/>
        <path d="M18.6631 15.9999H18.67" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
        <path d="M13.3292 15.9999H13.336" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
));

const ImageSkeleton = () => (
    <div className="absolute inset-0 bg-gray-200 animate-pulse rounded-t-lg" />
);

const ProductItem = memo(({ id, name, price, imageUrl, product, handleClick, onCartClick }) => {
    const [imageLoaded, setImageLoaded] = React.useState(false);

    const handleProductClick = (e) => {
        if (!e.target.closest('.heart-icon')) {
            handleClick();
        }
    };

    const handleCartButtonClick = (e) => {
        e.stopPropagation();
        onCartClick(product);
    };

    const handleImageLoad = () => {
        setImageLoaded(true);
    };

    const isOutOfStock = React.useMemo(() => {
        return product.sizes.every(size => !size.qty) && !product.qty && product.colors.every(color => !color.qty);
    }, [product]);

    return (
        <div className="rounded-lg" onClick={handleProductClick}>
            <div className="bg-white aspect-[186/275] relative flex flex-col w-full drop-shadow overflow-hidden group">
                <button className="absolute left-1 top-4 z-20 heart-icon" onClick={(e) => e.stopPropagation()}>
                    <WishlistHeart product={product} />
                </button>

                {!imageLoaded && <ImageSkeleton />}
                <Image
                    src={`https://storage.naayiq.com/resources/${imageUrl}`}
                    alt={name}
                    fill={true}
                    sizes="(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 10vw"
                    onError={(e) => {
                        e.target.src = "https://storage.naayiq.com/resources/noimage.webp";
                    }}
                    onLoad={handleImageLoad}
                    className={`object-cover relative rounded-t-lg w-full h-full overflow-hidden transition-transform duration-300 group-hover:scale-110 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
                />

                <div className="absolute min-h-[84px] bottom-0 left-0 right-0 bg-gradient-custom rounded-t-lg p-2 flex flex-col justify-end">
                    <h3 className="font-serif line-clamp-2 capitalize overflow-ellipsis font-medium text-xs sm:text-sm leading-tight tracking-tight text-[#181717] mb-auto">
                        {name}
                    </h3>
                    <div className="flex justify-between items-center mt-2">
                        {isOutOfStock ? (
                            <p className="font-medium font-sans text-lg leading-tight tracking-tight text-[#C91C1C]">
                                Out of stock
                            </p>
                        ) : (
                            <p className="font-semibold font-serif text-lg leading-tight tracking-tight text-[#181717]">
                                {price}
                            </p>
                        )}
                        <button
                            className={`w-8 h-8 rounded-full flex items-center justify-center ${isOutOfStock ? 'bg-gray-400 cursor-not-allowed' : 'bg-[#3B5345]'}`}
                            onClick={handleCartButtonClick}
                            disabled={isOutOfStock}
                        >
                            <CartIcon />
                        </button>
                    </div>
                </div>
            </div>
        </div>
    );
});

ProductItem.displayName = "ProductItem";
CartIcon.displayName = "CartIcon";

export default ProductItem;
I'm using nextjs app router...
docs:
useOptimistic is a React Hook that lets you optimistically update the UI.

  const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

    Reference
        useOptimistic(state, updateFn) 
    Usage
        Optimistically updating forms 

Reference
useOptimistic(state, updateFn)

useOptimistic is a React Hook that lets you show a different state while an async action is underway. It accepts some state as an argument and returns a copy of that state that can be different during the duration of an async action such as a network request. You provide a function that takes the current state and the input to the action, and returns the optimistic state to be used while the action is pending.

This state is called the “optimistic” state because it is usually used to immediately present the user with the result of performing an action, even though the action actually takes time to complete.

import { useOptimistic } from 'react';

function AppContainer() {
  const [optimisticState, addOptimistic] = useOptimistic(
    state,
    // updateFn
    (currentState, optimisticValue) => {
      // merge and return new state
      // with optimistic value
    }
  );
}

See more examples below.
Parameters

    state: the value to be returned initially and whenever no action is pending.
    updateFn(currentState, optimisticValue): a function that takes the current state and the optimistic value passed to addOptimistic and returns the resulting optimistic state. It must be a pure function. updateFn takes in two parameters. The currentState and the optimisticValue. The return value will be the merged value of the currentState and optimisticValue.

Returns

    optimisticState: The resulting optimistic state. It is equal to state unless an action is pending, in which case it is equal to the value returned by updateFn.
    addOptimistic: addOptimistic is the dispatching function to call when you have an optimistic update. It takes one argument, optimisticValue, of any type and will call the updateFn with state and optimisticValue.

Usage
Optimistically updating forms

The useOptimistic Hook provides a way to optimistically update the user interface before a background operation, like a network request, completes. In the context of forms, this technique helps to make apps feel more responsive. When a user submits a form, instead of waiting for the server’s response to reflect the changes, the interface is immediately updated with the expected outcome.

For example, when a user types a message into the form and hits the “Send” button, the useOptimistic Hook allows the message to immediately appear in the list with a “Sending…” label, even before the message is actually sent to a server. This “optimistic” approach gives the impression of speed and responsiveness. The form then attempts to truly send the message in the background. Once the server confirms the message has been received, the “Sending…” label is removed.
Example:
import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  const [messages, setMessages] = useState([
    { text: "Hello there!", sending: false, key: 1 }
  ]);
  async function sendMessage(formData) {
    const sentMessage = await deliverMessage(formData.get("message"));
    setMessages((messages) => [...messages, { text: sentMessage }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

React 19 new features:
React 19 - Vercel
Michael NovotnyDirector of Developer Experience
17–22 minutes

Explore React 19 and how to start using it on Vercel today.

React 19 is near. The React Core Team announced a React 19 release candidate (RC) this past April. This major version brings several updates and new patterns, aimed at improving performance, ease of use, and developer experience.

Many of these features were introduced as experimental in React 18, but they will be marked as stable in React 19. Here’s a high-level look at what you need to know to be ready.
Server Components

Server Components are one of the biggest changes to React since its initial release 10 years ago. They act as a foundation for React 19’s new features, improving:

    Initial page load times. By rendering components on the server, they reduce the amount of JavaScript sent to the client, resulting in faster initial loads. They also let data queries start on the server before the page is sent to the client.

    Code portability. Server Components let developers write components that can run on both the server and client, which reduces duplication, improves maintainability, and enables easier sharing of logic across your codebase.

    SEO. Server-side rendering of components allows search engines and LLMs to crawl and index content more effectively, improving search engine optimization.

We won’t dive deep into Server Components or rendering strategies in this post. However, to understand the significance of Server Components, let’s take a brief look at how React rendering has evolved.

React started with Client-Side Rendering (CSR), which served minimal HTML to the user.

<!DOCTYPE html>

<html>

  <body>

    <div id="root"></div>

    <script src="/static/js/bundle.js"></script>

  </body>

</html>

The linked script includes everything about your application—React, third-party dependencies, and all your application code. As your application grew, so did your bundle size. The JavaScript is downloaded and parsed, and then React loads the DOM elements into the empty div. While this was happening, all the user sees is a blank page.

Even when the initial UI finally shows, the page content is still missing, which is why loading skeletons gained popularity. Data is then fetched and the UI renders a second time, replacing loading skeletons with actual content.

React improved with Server-Side Rendering (SSR), which moves the first render to the server. The HTML served to the user wasn’t empty anymore, and it improves how quickly the user saw the initial UI. However, the data still needs to be fetched to display actual content.

React frameworks stepped in to further improve the user experience with concepts like Static-Site Generation (SSG), which caches and renders dynamic data during the build, and Incremental Static Regeneration (ISR), which recaches and rerenders dynamic data on-demand.

This brings us to React Server Components (RSC). For the first time, native to React, we can fetch data before the UI renders and displays to the user.

export default async function Page() {

  const res = await fetch("https://api.example.com/products");

  const products = res.json();

  return (

    <>

      <h1>Products</h1>

      {products.map((product) => (

        <div key={product.id}>

          <h2>{product.title}</h2>

          <p>{product.description}</p>

        </div>

      ))}

    </>

  );

}

HTML served to the user is fully populated with actual content on the first render, and there is no need to fetch additional data or render a second time.

Server Components are a big step forward for speed and performance, enabling a better developer and user experience. Learn more about React Server Components.

Credit to Josh W. Comeau for inspiration on the rendering diagrams.

Try Server Components

This Next.js App Router template lets you experience Server Components with just a few clicks.

Deploy Now
New directives

Directives are not a React 19 feature, but they are related. With the introduction of React Server Components, bundlers need to distinguish where components and functions run. To accomplish this, there are two new directives to be aware of when creating React components:

    'use client' marks code that runs only on the client. Since Server Components are the default, you will add 'use client' to Client Components when using hooks for interactivity and state.

    'use server' marks server-side functions that can be called from client-side code. You do not need to add 'use server' to Server Components, only Server Actions (more on that below). If you’d like to make sure a certain piece of code can only runs on the server, you can use the server-only npm package.

Learn more about Directives.
Actions

React 19 introduces Actions. These functions replace using event handlers and integrate with React transitions and concurrent features.
﻿
Actions can be used on both the client and server. For example, you can have a Client Action which replaces previous usage of onSubmit for a form.
﻿
Rather than needing to parse the event, the action is directly passed the FormData.

import { useState } from "react";

export default function TodoApp() {

  const [items, setItems] = useState([

    { text: "My first todo" },

  ]);

  async function formAction(formData) {

    const newItem = formData.get("item");

    // Could make a POST request to the server to save the new item

    setItems((items) => [...items, { text: newItem }]);

  }

  return (

    <>

      <h1>Todo List</h1>

      <form action={formAction}>

        <input type="text" name="item" placeholder="Add todo..." />

        <button type="submit">Add</button>

      </form>

      <ul>

        {items.map((item, index) => (

          <li key={index}>{item.text}</li>

        ))}

      </ul>

    </>

  );

}

Server Actions

Going further, Server Actions allow Client Components to call async functions executed on the server. This provides additional advantages, like reading the file system or making direct database calls, removing the need for creating bespoke API endpoints for your UI.

Actions are defined with the 'use server' directive and integrate with client-side components.

To call a Server Action in a Client Component, create a new file and import it:

'use server'

export async function create() {

  // Insert into database

}

"use client";

import { create } from "./actions";

export default function TodoList() {

  return (

    <>

      <h1>Todo List</h1>

      <form action={create}>

        <input type="text" name="item" placeholder="Add todo..." />

        <button type="submit">Add</button>

      </form>

    </>

  );

}

Learn more about Server Actions.
New hooks

To complement Actions, React 19 introduces three new hooks to make state, status, and visual feedback easier. These are particularly useful when working with forms, but they can also be used on other elements, like buttons.
useActionState

This hook simplifies managing form states and form submissions. Using Actions, it captures form input data, handles validation, and error states, reducing the need for custom state management logic. The useActionState hook also exposes a pending state that can show a loading indicator while the action is being executed.

"use client";

import { useActionState } from "react";

import { createUser } from "./actions";

const initialState = {

  message: "",

};

export function Signup() {

  const [state, formAction, pending] = useActionState(createUser, initialState);

  return (

    <form action={formAction}>

      <label htmlFor="email">Email</label>

      <input type="text" id="email" name="email" required />

      {/* ... */}

      {state?.message && <p aria-live="polite">{state.message}</p>}

      <button aria-disabled={pending} type="submit">

        {pending ? "Submitting..." : "Sign up"}

      </button>

    </form>

  );

}

Learn more about useActionState.
useFormStatus

This hook manages the status of the last form submission, and it must be called from inside a component that is also inside a form.

import { useFormStatus } from "react-dom";

import action from "./actions";

function Submit() {

  const status = useFormStatus();

  return <button disabled={status.pending}>Submit</button>;

}

export default function App() {

  return (

    <form action={action}>

      <Submit />

    </form>

  );

}

While useActionState has a built-in pending status, useFormStatus is useful on its own when:

    There is no form state

    Creating shared form components

    There are multiple forms on the same page—useFormStatus will only return status information for a parent form

Learn more about useFormStatus.
useOptimistic

This hook lets you optimistically update the UI before the Server Action finishes executing, rather than waiting for the response. When the async action completes, the UI updates with the final state from the server.

The following example demonstrates optimistically adding a new message to a thread immediately, while the message is also sent to the Server Action for persistence.

"use client";

import { useOptimistic } from "react";

import { send } from "./actions";

export function Thread({ messages }) {

  const [optimisticMessages, addOptimisticMessage] = useOptimistic(

    messages,

    (state, newMessage) => [...state, { message: newMessage }],

  );

  const formAction = async (formData) => {

    const message = formData.get("message") as string;

    addOptimisticMessage(message);

    await send(message);

  };

  return (

    <div>

      {optimisticMessages.map((m, i) => (

        <div key={i}>{m.message}</div>

      ))}

      <form action={formAction}>

        <input type="text" name="message" />

        <button type="submit">Send</button>

      </form>

    </div>

  );

}

Learn more about useOptimistic.
New API: use

The use function offers first-class support for promises and context during rendering. Unlike other React Hooks, use can be called within loops, conditional statements, and early returns. Error handling and loading will be handled by the nearest Suspense boundary.

The following example shows a loading message while the cart items promise resolves.

import { use } from "react";

function Cart({ cartPromise }) {

  // `use` will suspend until the promise resolves

  const cart = use(cartPromise);

  return cart.map((item) => <p key={item.id}>{item.title}</p>);

}

function Page({ cartPromise }) {

  return (

    /*{ ... }*/

    // When `use` suspends in Cart, this Suspense boundary will be shown

    <Suspense fallback={<div>Loading...</div>}>

      <Cart cartPromise={cartPromise} />

    </Suspense>

  );

}

This allows you to group components together so they render only when all the components’ data is available.

Learn more about use.
Preloading resources

React 19 adds several new APIs to improve page load performance and user experience by loading and preloading resources such as scripts, stylesheets, and fonts.

    prefetchDNS prefetches the IP address of a DNS domain name you expect to connect to.

    preconnect connects to a server you expect to request resources from, even if the exact resources are unknown at the time.

    preload fetches a stylesheet, font, image, or external script that you expect to use.

    preloadModule fetches an ESM module that you expect to use.

    preinit fetches and evaluates an external script or fetches and inserts a stylesheet.

    preinitModule fetches and evaluates an ESM module.

For example, this React code would result in the following HTML output. Note that links and scripts are prioritized and ordered by how early they should load, not based on the order they are used in React.

// React code

import { prefetchDNS, preconnect, preload, preinit } from "react-dom";

function MyComponent() {

  preinit("https://.../path/to/some/script.js", { as: "script" });

  preload("https://.../path/to/some/font.woff", { as: "font" });

  preload("https://.../path/to/some/stylesheet.css", { as: "style" });

  prefetchDNS("https://...");

  preconnect("https://...");

}

<!-- Resulting HTML -->

<html>

  <head>

    <link rel="prefetch-dns" href="https://..." />

    <link rel="preconnect" href="https://..." />

    <link rel="preload" as="font" href="https://.../path/to/some/font.woff" />

    <link

      rel="preload"

      as="style"

      href="https://.../path/to/some/stylesheet.css"

    />

    <script async="" src="https://.../path/to/some/script.js"></script>

  </head>

  <body>

    <!-- ... -->

  </body>

</html>

React frameworks frequently handle resource loading like this for you, so you might not have to call these APIs yourself.

Learn more about Resource Preloading APIs.
Other improvements
ref as a prop

There’s no need for forwardRef anymore. React will provide a codemod to make transitioning easier.

function CustomInput({ placeholder, ref }) {

  return <input placeholder={placeholder} ref={ref} />;

}

// ...

<CustomInput ref={ref} />;

ref callbacks

In addition to ref as a prop, refs can also return a callback function for cleanup. When a component unmounts, React will call the cleanup function.

<input

  ref={(ref) => {

    // ref created

    // Return a cleanup function to reset

    // ref when element is removed from DOM.

    return () => {

      // ref cleanup

    };

  }}

/>;

Context as a provider

There’s no need for <Context.Provider> anymore. You can use <Context> directly instead. React will provide a codemod to convert existing providers.

const ThemeContext = createContext("");

function App({ children }) {

  return <ThemeContext value="dark">{children}</ThemeContext>;

}

useDeferredValue initial value

An initialValue option has been added to useDeferredValue. When provided, useDeferredValue will use the value for the initial render and schedule a re-render in the background, returning the deferredValue.

function Search({ deferredValue }) {

  // On initial render the value is ''.

  // Then a re-render is scheduled with the deferredValue.

  const value = useDeferredValue(deferredValue, "");

  return <Results value={value} />;

}

Document metadata support

React 19 will natively hoist and render title, link, and meta tags, even from nested components. There’s no need for third-party solutions to manage these tags anymore.

function BlogPost({ post }) {

  return (

    <article>

      <h1>{post.title}</h1>

      <title>{post.title}</title>

      <meta name="author" content="Jane Doe" />

      <link rel="author" href="https://x.com/janedoe" />

      <meta name="keywords" content={post.keywords} />

      <p>...</p>

    </article>

  );

}

Stylesheet support

React 19 allows controlling stylesheet loading order with precedence. This makes colocating stylesheets near components easier, and React only loads them if they are used.

There are a few points to keep in mind:

    If you render the same component in multiple places within your application, React will deduplicate the stylesheet and only include it once in the document.

    When server-side rendering, React will include the stylesheet in the head. This ensures that the browser will not paint until it has loaded.

    If the stylesheet is discovered after streaming has started, React will ensure that the stylesheet is inserted into the <head> on the client before revealing the content that depends on that stylesheet through a Suspense boundary.

    During client-side rendering, React will wait for newly rendered stylesheets to load before committing the render.

function ComponentOne() {

  return (

    <Suspense fallback="loading...">

      <link rel="stylesheet" href="one" precedence="default" />

      <link rel="stylesheet" href="two" precedence="high" />

      <article>...</article>

    </Suspense>

  );

}

function ComponentTwo() {

  return (

    <div>

      <p>...</p>

      {/* Stylesheet "three" below will be inserted between "one" and "two" */}

      <link rel="stylesheet" href="three" precedence="default" />

    </div>

  );

}

Async scripts support

Render async scripts in any component. This makes colocating scripts near components easier, and React only loads them if they are used.

There are a few points to keep in mind:

    If you render the same component in multiple places within your application, React will deduplicate the script and only include it once in the document.

    When server-side rendering, async scripts will be included in the head and prioritized behind more critical resources that block paint, such as stylesheets, fonts, and image preloads.

function Component() {

  return (

    <div>

      <script async={true} src="..." />

      // ...

    </div>

  );

}

function App() {

  return (

    <html>

      <body>

        <Component>

          // ...

        </Component> // Won't duplicate script in the DOM

      </body>

    </html>

  );

}

Custom Elements support

Custom Elements allow developers to define their own HTML elements as a part of the Web Components specification. In previous versions of React, using Custom Elements has been difficult because React treats unrecognized props as attributes rather than properties.

React 19 adds full support for Custom Elements and passes all tests on Custom Elements Everywhere.
Better error reporting

Error handling improves by removing duplicate error messages.

Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error.Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error.
Previously, React would throw the error twice. Once for the original error, then a second time after failing to automatically recover, followed by information about the error.

In React 19, the error is only displayed once.In React 19, the error is only displayed once.
In React 19, the error is only displayed once.

Hydration errors improve by logging a single mismatch error instead of multiple errors. Error messages also include information on how to possibly fix the error.

Example of a hydration error message in React 18.Example of a hydration error message in React 18.
Example of a hydration error message in React 18.

Example of an improved hydration error message in React 19.Example of an improved hydration error message in React 19.
Example of an improved hydration error message in React 19.

Hydration errors when using third-party scripts and browser extensions also improve. Previously, elements inserted by third-party scripts or browser extensions would trigger a mismatch error. In React 19, unexpected tags in the head and body will be skipped over and will not throw an error.

Lastly, React 19 adds two new root options in addition to the existing onRecoverableError, to provide better clarity on why the error is happening.

    onCaughtError triggers when React catches an error in an Error Boundary.

    onUncaughtError triggers when an error is thrown and not caught by an Error Boundary.

    onRecoverableError triggers when an error is thrown and automatically recovered.