Home Services Work About Blog Contact Let's Talk
Blog / SPFx Development
SPFx Development

React Hooks in SPFx: useState, useEffect, and Custom Hooks in SharePoint Web Parts

Why React Hooks Transform SPFx Web Part Development

Class-based React components dominated SPFx development for years because the Yeoman generator scaffolded them by default and Microsoft's early SPFx samples all used them. But since SPFx 1.12 shipped with React 16.8 support, functional components backed by hooks have become the cleaner, more maintainable choice. If you are still writing extends React.Component in new web parts, you are carrying unnecessary complexity.

The practical difference is significant. Class components require you to split lifecycle logic across componentDidMount, componentDidUpdate, and componentWillUnmount, which means related code lives in three different methods. A useEffect hook collocates setup and teardown in a single block. State that used to require a this.setState call on an object now lives in discrete, named useState variables. The result is components that are easier to read, easier to test, and dramatically easier to extract into reusable custom hooks.

In this guide we walk through the four hook patterns we use in almost every SPFx project: useState for local component state, useEffect for SharePoint REST and Graph calls, useMemo for filtering and sorting without redundant renders, and custom hooks that encapsulate SharePoint data-access logic and can be shared across multiple web parts in a monorepo.

useState Patterns for SharePoint Web Part State

The first thing most developers notice when migrating to hooks is that useState encourages you to split your state into small, independent variables rather than one large state object. This is almost always the right approach in SPFx, because web parts tend to have distinct pieces of state that update independently — a loading flag, an error message, and the actual data items are three separate concerns.

A common pattern in our projects is the loading/error/data triple. Declare them as separate useState calls at the top of your component, update them inside a useEffect, and render conditionally based on the loading and error states before showing your data.

TypeScript — useState loading/error/data triple in an SPFx functional component
import * as React from 'react';
import { useState, useEffect } from 'react';
import { IAnnouncementItem } from '../models/IAnnouncementItem';

interface IAnnouncementsProps {
  spHttpClient: SPHttpClient;
  siteUrl: string;
  listName: string;
}

const Announcements: React.FC<IAnnouncementsProps> = ({ spHttpClient, siteUrl, listName }) => {
  const [items, setItems] = useState<IAnnouncementItem[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  // useEffect wired below — rendering logic uses these three values
  if (loading) return <Spinner label="Loading announcements..." />;
  if (error) return <MessageBar messageBarType={MessageBarType.error}>{error}</MessageBar>;
  return (
    <ul>{items.map(i => <li key={i.Id}>{i.Title}</li>)}</ul>
  );
};

One subtlety that trips up SPFx developers: when the listName prop comes from the property pane and can change at runtime without a page reload, your useEffect must declare listName as a dependency so it re-fetches when the configuration changes. This is actually where hooks shine versus class components — the dependency array makes the re-fetch trigger explicit and visible in the code.

Tip

Avoid initialising useState with data derived from props inside the component body — use the lazy initialiser form useState(() => computeInitialValue(props)) for expensive computations, or simply run the logic inside useEffect.

useEffect for SharePoint REST and Graph Data Fetching

Every SPFx web part that talks to SharePoint or Microsoft Graph eventually needs to handle async data loading. The useEffect hook is where this logic lives. The key pattern is: declare an async inner function inside the effect, call it immediately, and return a cleanup that sets a cancellation flag to avoid updating state on an unmounted component.

TypeScript — useEffect with cancellation flag for SharePoint REST
useEffect(() => {
  let cancelled = false;

  const fetchItems = async () => {
    try {
      setLoading(true);
      setError(null);
      const endpoint = `${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Id,Title,Body,Expires&$orderby=Created desc&$top=10`;
      const response = await spHttpClient.get(endpoint, SPHttpClient.configurations.v1);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const data = await response.json();
      if (!cancelled) {
        setItems(data.value);
        setLoading(false);
      }
    } catch (err) {
      if (!cancelled) {
        setError(err.message);
        setLoading(false);
      }
    }
  };

  fetchItems();
  return () => { cancelled = true; };
}, [spHttpClient, siteUrl, listName]); // re-fetch when props change

The cancellation flag pattern is essential in SPFx because property pane changes can trigger rapid re-renders while a previous fetch is still in flight. Without the flag, you risk a race condition where a slow earlier response overwrites the result of a faster later response. This is especially visible when the user changes the listName property quickly — the first list's data should never overwrite the second list's data if the second request resolves first.

For Microsoft Graph calls, swap spHttpClient for the Graph client obtained via this.context.msGraphClientFactory.getClient('3'). Pass the client as a prop into your functional component and use it in the same useEffect pattern. The Graph client handles token refresh transparently, so your effect code stays clean.

Watch Out

Never put an async function directly as the useEffect callback — useEffect expects either nothing or a cleanup function as the return value, not a Promise. Always define the async function inside and call it immediately.

Building Reusable Custom Hooks for SharePoint

Once you have the loading/error/data pattern working in one component, the natural next step is extracting it into a custom hook so every web part in your monorepo can reuse the same data-access logic. A custom hook is simply a function whose name starts with use and which calls other hooks internally. It can return whatever values the consuming component needs.

In practice, we create a hooks/ folder alongside the web part's components/ folder and put each SharePoint data concern in its own hook file. A hook like useSharePointList accepts the list name and query parameters, handles fetching and error states internally, and exposes a clean { items, loading, error, refetch } interface. The refetch function is important — it lets the parent component trigger a manual reload after a form submission without remounting the component.

TypeScript — useSharePointList custom hook
// hooks/useSharePointList.ts
import { useState, useEffect, useCallback } from 'react';
import { SPHttpClient } from '@microsoft/sp-http';

interface UseSharePointListOptions {
  spHttpClient: SPHttpClient;
  siteUrl: string;
  listName: string;
  select?: string;
  filter?: string;
  orderBy?: string;
  top?: number;
}

export function useSharePointList<T>(options: UseSharePointListOptions) {
  const { spHttpClient, siteUrl, listName, select, filter, orderBy, top = 50 } = options;
  const [items, setItems] = useState<T[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [tick, setTick] = useState(0);

  const refetch = useCallback(() => setTick(t => t + 1), []);

  useEffect(() => {
    let cancelled = false;
    const load = async () => {
      setLoading(true); setError(null);
      const params = [
        select && `$select=${select}`,
        filter && `$filter=${filter}`,
        orderBy && `$orderby=${orderBy}`,
        `$top=${top}`,
      ].filter(Boolean).join('&');
      const url = `${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?${params}`;
      try {
        const res = await spHttpClient.get(url, SPHttpClient.configurations.v1);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        if (!cancelled) { setItems(json.value); setLoading(false); }
      } catch (e) {
        if (!cancelled) { setError(e.message); setLoading(false); }
      }
    };
    load();
    return () => { cancelled = true; };
  }, [spHttpClient, siteUrl, listName, select, filter, orderBy, top, tick]);

  return { items, loading, error, refetch };
}

This single hook can serve an announcements web part, a news web part, a document library web part, and any other list-driven component. The generic type parameter T means TypeScript still enforces the shape of your item objects at the call site, giving you full type safety without duplicating the fetch logic.

useMemo and useCallback for Filtering and Event Handlers

SharePoint list web parts frequently need client-side filtering and sorting — the user types in a search box and the displayed items update without a round trip to the server. Without memoisation, every keystroke causes React to re-compute the filtered array even if the items array itself has not changed. useMemo solves this by caching the computed result and only recomputing when its dependencies change.

TypeScript — useMemo for client-side list filtering
import { useMemo, useState } from 'react';

const [searchQuery, setSearchQuery] = useState('');
const [sortField, setSortField] = useState<string>('Title');

const filteredItems = useMemo(() => {
  const q = searchQuery.toLowerCase();
  return items
    .filter(item =>
      item.Title.toLowerCase().includes(q) ||
      item.Department.toLowerCase().includes(q)
    )
    .sort((a, b) => (a[sortField] > b[sortField] ? 1 : -1));
}, [items, searchQuery, sortField]); // recompute only when these three change

// useCallback stabilises the handler reference so child components
// wrapped in React.memo do not re-render on every parent render
const handleSearchChange = useCallback(
  (ev: React.ChangeEvent<HTMLInputElement>) => setSearchQuery(ev.target.value),
  [] // setSearchQuery is stable — no dependencies needed
);

useCallback is the counterpart for functions. When you pass an event handler to a child component that is wrapped in React.memo, a new function reference on every parent render defeats the memoisation. Wrapping the handler in useCallback with an appropriate dependency array keeps the reference stable and prevents unnecessary child re-renders — which matters in SPFx because Fluent UI components already use React.memo internally.

Note

Do not over-memoize. useMemo and useCallback have their own overhead. Reserve them for computationally expensive operations (filtering large lists, sorting, mapping complex objects) and for stabilising references passed to memoised child components.

Context API in SPFx: Sharing Web Part Context Without Prop Drilling

SPFx web parts receive the framework context object — containing the SPFx HTTP clients, page context, and user information — in the top-level web part class. Passing this context as a prop through five layers of components is verbose and makes refactoring painful. The React Context API solves this elegantly and pairs naturally with hooks via useContext.

Create a SpfxContext.ts file that exports a typed context object and a custom useSpfxContext hook. The top-level web part component wraps its render tree in the context provider, and any deeply nested component can call useSpfxContext() to access the HTTP clients, site URL, and current user without a single prop being drilled.

TypeScript — SpfxContext provider and consumer hook
// context/SpfxContext.ts
import { createContext, useContext } from 'react';
import { SPHttpClient } from '@microsoft/sp-http';

interface ISpfxContext {
  spHttpClient: SPHttpClient;
  siteUrl: string;
  currentUserEmail: string;
  isTeamsContext: boolean;
}

export const SpfxContext = createContext<ISpfxContext | null>(null);

export function useSpfxContext(): ISpfxContext {
  const ctx = useContext(SpfxContext);
  if (!ctx) throw new Error('useSpfxContext must be used inside SpfxContext.Provider');
  return ctx;
}

// In your web part's render() method:
// <SpfxContext.Provider value={{ spHttpClient, siteUrl, currentUserEmail, isTeamsContext }}>
//   <YourRootComponent />
// </SpfxContext.Provider>

Testing Custom Hooks with React Testing Library

One of the most compelling reasons to extract logic into custom hooks is testability. You can test a hook in complete isolation from any component UI using the renderHook utility from @testing-library/react. SPFx projects that use Jest (configured via the @microsoft/sp-build-web build chain) can add @testing-library/react as a dev dependency without conflicting with the existing build setup.

TypeScript — Testing useSharePointList with renderHook
import { renderHook, waitFor } from '@testing-library/react';
import { useSharePointList } from '../hooks/useSharePointList';

const mockSpHttpClient = {
  get: jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ value: [{ Id: 1, Title: 'Test Item' }] }),
  }),
};

it('fetches list items and returns them', async () => {
  const { result } = renderHook(() =>
    useSharePointList({
      spHttpClient: mockSpHttpClient as any,
      siteUrl: 'https://contoso.sharepoint.com',
      listName: 'Announcements',
    })
  );

  expect(result.current.loading).toBe(true);
  await waitFor(() => expect(result.current.loading).toBe(false));
  expect(result.current.items).toHaveLength(1);
  expect(result.current.items[0].Title).toBe('Test Item');
  expect(result.current.error).toBeNull();
});

Because the hook is a pure TypeScript function that accepts its dependencies as arguments, you mock the SPHttpClient with a simple object and control the response entirely in the test. Compare this to testing a class component with lifecycle methods — you would need to mount the component, simulate prop changes, and inspect rendered output. Hook tests are simpler, faster, and do not depend on DOM rendering at all.

Production Checklist for Hooks-Based SPFx Web Parts

Hooks in SPFx work reliably when you follow a small set of rules consistently. The most common production issues we see come from incorrect dependency arrays in useEffect, missing cleanup functions, and creating new object references inside dependency arrays on every render.

  • ESLint plugin for hooks: Install eslint-plugin-react-hooks and enable the exhaustive-deps rule. It catches incorrect dependency arrays before they reach production and explains the fix.
  • Stable references for objects in deps: If you build a query options object inside the component body and pass it to a custom hook, wrap it in useMemo — otherwise the hook's effect fires on every render because the object reference changes each time.
  • Always clean up intervals and subscriptions: If your web part polls for updates with setInterval, return a cleanup from useEffect that calls clearInterval. SPFx web parts can be removed from the page without navigating away, and un-cleaned intervals silently continue running.
  • Avoid hooks inside conditionals or loops: The rules of hooks require the same hooks to be called in the same order on every render. SPFx web part props can change at runtime via the property pane — conditional hook calls will cause unpredictable bugs.
  • Property pane reactive updates: Call this.render() inside onPropertyPaneFieldChanged when your web part uses functional components to ensure the new prop values reach the component tree immediately.
Note

SPFx 1.18 and later support React 17, but SPFx 1.20 introduced React 18 support. React 18's concurrent features (like useTransition and useDeferredValue) work in SPFx web parts but require testing in the Workbench — the SharePoint page renderer may not support StrictMode's double-invocation behaviour in all environments.

Key Takeaways

Use the loading/error/data triple of useState calls rather than a single state object — it maps directly to the three rendering states every SharePoint component needs.

Always add a cancellation flag inside useEffect to prevent race conditions when the user changes property pane settings while a fetch is in flight.

Extract SharePoint data-access logic into a generic useSharePointList custom hook — it eliminates duplication across web parts and makes the logic independently testable.

Use the React Context API with a typed useSpfxContext hook to share the SPFx framework context without prop drilling through multiple component layers.

Enable eslint-plugin-react-hooks with the exhaustive-deps rule from day one — it prevents the most common production bugs caused by stale closures in useEffect.

AT

Akshara Technologies

Microsoft 365 Development Specialists

With 10+ years building enterprise SharePoint, SPFx, Power Automate, and Flutter solutions for clients across India, USA, UAE, and Australia — we write from production experience, not documentation.

Related Articles

Ready to Build with Modern SPFx?

From React Hooks to full enterprise SharePoint solutions — Akshara Technologies delivers clean, maintainable SPFx web parts that scale.

Start Your Project View Case Studies