Skip to content

SharePoint

SPFx + React: Complete Developer Guide

Build production SPFx solutions with React architecture: components, hooks, services, PnPjs, Graph, Fluent UI, error handling, performance, and testing.

Suresh Girinathuni
Published
Reading time
22 min read
SPFx React guide illustration showing directory UI components composed over a data hook and service

What you’ll learn

  • React's role inside SPFx
  • Project architecture
  • Web part to React
  • Functional components
  • Props

Direct answer: React is commonly used to build the user-interface layer of SPFx solutions — but production SPFx development requires more than placing all logic inside one React component. A maintainable solution separates presentation, state, business logic, SharePoint access, Microsoft Graph access, external APIs, configuration, and error handling across components, hooks, and a service layer.

React should manage the UI — not become the entire application architecture. Components render; hooks hold stateful logic; services own data access. Everything in this guide serves that separation.

  1. SPFx web part — context, properties, lifecycle, platform integration.
  2. React application — components composed from props and state.
  3. Components — focused, typed, reusable presentation.
  4. Hooks and state — loading, data, error, and interaction state.
  5. Service layer — PnPjs, SharePoint REST, Microsoft Graph, external APIs.

This guide assumes working JavaScript and TypeScript and teaches SPFx-specific React architecture — not React basics, not Hello World scaffolding. For the full lifecycle, use the SPFx complete guide; for navigation, the SPFx hub; for legacy migrations into this architecture, Script Editor to SPFx modernization.

React's role inside SPFx

Three layers with three jobs. The SPFx web part class provides context, lifecycle, property pane, platform integration, and configuration. The React tree owns UI, composition, state, interaction, and rendering. Services own data access, Graph, SharePoint, external APIs, and reusable business operations:

  1. SPFx web part — provides context and properties.
  2. Root React component — receives only what it needs as props.
  3. Child components — compose the experience from typed props.
  4. Services — fetch, map, and guard Microsoft 365 data.
  5. Microsoft 365 — SharePoint, Graph, and business systems behind services.

Responsibility leaks — context globals, API calls in render paths, business rules in JSX — are the root cause of most unmaintainable SPFx solutions this guide prevents.

Project architecture

A recommended example layout for a feature such as an employee directory. This is an example, not a mandatory Microsoft structure — adopt the layering even if your folder names differ:

src/
  webparts/
    employeeDirectory/
      EmployeeDirectoryWebPart.ts
      components/
        EmployeeDirectory.tsx
        EmployeeCard.tsx
        EmployeeList.tsx
        LoadingState.tsx
        ErrorState.tsx
      services/
        IEmployeeService.ts
        EmployeeService.ts
      models/
        IEmployee.ts
      hooks/
        useEmployees.ts
      utils/
      loc/

Components render; services fetch; models type the boundary; hooks hold stateful logic; utils stay pure; localization files hold every user-facing string. Each layer exists so the others stay small and testable.

Web part to React

Pass only what components need — context pieces, title, configuration, display settings — as typed props from the web part's render method. Never make the application depend on global state or hand the entire context object to every component:

public render(): void {
  const element: React.ReactElement<IEmployeeDirectoryProps> =
    React.createElement(EmployeeDirectory, {
      title: this.properties.title,
      service: this._employeeService,
      displayMode: this.displayMode
    });
  ReactDom.render(element, this.domElement);
}

The web part constructs the service once (dependency injection without a framework), injects it as a prop, and disposes rendering on unmount. Components stay portable because their inputs are explicit.

Functional components

Use functional components for primary examples and new code — the modern pattern supported across current SPFx React versions. Components receive props, hold state, handle events, and delegate data work to hooks and services:

export interface IEmployeeCardProps {
  displayName: string;
  jobTitle?: string;
  onSelect?: () => void;
}

export function EmployeeCard(props: IEmployeeCardProps): JSX.Element {
  return (
    <article>
      <h3>{props.displayName}</h3>
      {props.jobTitle && <p>{props.jobTitle}</p>}
      {props.onSelect && (
        <button type="button" onClick={props.onSelect}>
          View profile
        </button>
      )}
    </article>
  );
}

Props

Props are typed inputs flowing parent to child. Prefer readonly semantics, explicit optional markers, and callback props for child-to-parent events:

export interface IDocumentListProps {
  readonly siteUrl: string;
  readonly pageSize: number;
  readonly onDocumentSelected?: (id: number) => void;
}

Clear prop interfaces document the component contract: what it needs, what is optional, and what it emits. Components that accept the whole world as props cannot be reused, tested, or reasoned about.

State

State holds UI and interaction truth: loading flags, selected items, search queries, retrieved data, dialog state, filters, and pagination. Never store values in state that can be derived — derived values computed during render cannot drift out of sync, while duplicated state always eventually does.

useState

Model the three facts every data-driven component needs — data, loading, error — explicitly from the start:

const [items, setItems] = useState<IProject[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

Explicit triples force every render path to answer: are we waiting, did we fail, or do we have data? Components missing any branch render blank screens and swallowed failures in production.

useEffect done correctly

Effects handle side effects: initial data loading and responding to changed dependencies. The architecture runs component mount, effect, service, SharePoint, state update, render — and the dependency array is a correctness contract, not decoration. List every value the effect reads; omitting dependencies creates stale closures, while including unstable ones creates infinite request loops:

React.useEffect(() => {
  let cancelled = false;

  async function load(): Promise<void> {
    setLoading(true);
    setError(null);
    try {
      const result = await service.getEmployees();
      if (!cancelled) {
        setItems(result);
      }
    } catch (failure) {
      if (!cancelled) {
        setError("Employees could not be loaded.");
      }
    } finally {
      if (!cancelled) {
        setLoading(false);
      }
    }
  }

  load();
  return () => {
    cancelled = true;
  };
}, [service]);

The cancellation flag is the stale-request guard: unmounted components and superseded requests must never write state. Stable service instances keep the dependency array honest — another reason services are constructed once in the web part, not per render.

Async data loading pattern

Production loading follows one shape: render, loading state, async service request, then success renders data or failure renders an error — with try, catch, and finally guaranteeing the loading flag always resolves. Never ignore failed requests: an unhandled rejection is a blank screen with no evidence, the hardest failure class to support.

Loading states

Never render a blank screen while waiting. Prefer a loading indicator, a skeleton where the layout is known, or a clear loading message — chosen per experience, always present. Loading states are UX architecture: they tell users the system heard them, which is most of perceived performance.

Empty states

Loading, error, no-results, and empty-data are four different states with four different messages. "No documents found" means the query succeeded with nothing to show; "Unable to load documents" means the query failed. Conflating them sends users — and support desks — chasing the wrong problem, so model them as separate branches in code.

Error states

Build one reusable error experience accepting a title, a safe message, and a retry callback. Raw API exceptions — with status codes, correlation IDs, and internals — never reach end users; they reach logs, while users get actionable next steps. Retry belongs on transient failures, not on permission denials.

export interface IErrorStateProps {
  readonly title: string;
  readonly message: string;
  readonly onRetry?: () => void;
}

export function ErrorState(props: IErrorStateProps): JSX.Element {
  return (
    <div role="alert">
      <h3>{props.title}</h3>
      <p>{props.message}</p>
      {props.onRetry && (
        <button type="button" onClick={props.onRetry}>
          Try again
        </button>
      )}
    </div>
  );
}

Component composition

Split giant components into a container feature component orchestrating reusable presentation pieces:

EmployeeDirectory
├── SearchBox
├── FilterPanel
├── EmployeeList
│   └── EmployeeCard
├── LoadingState
├── EmptyState
└── ErrorState

Containers own data flow and state; presentation components receive props and emit callbacks. Avoid outdated mandatory terminology for these roles — the split matters, not the labels. A component doing fetching, filtering, rendering, and error handling at once is four components wearing one file.

Custom hooks

Encapsulate loading, data, error, and refresh behind hooks like useDocuments, useEmployees, or useCurrentUser. Components consume state; hooks own the lifecycle; services own the calls:

import * as React from "react";
import type { IEmployee } from "../models/IEmployee";
import type { IEmployeeService } from "../services/IEmployeeService";

export interface IEmployeesResult {
  readonly employees: IEmployee[];
  readonly loading: boolean;
  readonly error: string | null;
  readonly refresh: () => void;
}

export function useEmployees(
  service: IEmployeeService
): IEmployeesResult {
  const [employees, setEmployees] = React.useState<IEmployee[]>([]);
  const [loading, setLoading] = React.useState<boolean>(true);
  const [error, setError] = React.useState<string | null>(null);
  const [revision, setRevision] = React.useState<number>(0);

  React.useEffect(() => {
    let cancelled = false;
    async function load(): Promise<void> {
      setLoading(true);
      setError(null);
      try {
        const result = await service.getEmployees();
        if (!cancelled) {
          setEmployees(result);
        }
      } catch (failure) {
        if (!cancelled) {
          setError("Employees could not be loaded.");
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }
    load();
    return () => {
      cancelled = true;
    };
  }, [service, revision]);

  return {
    employees,
    loading,
    error,
    refresh: () => setRevision((value) => value + 1)
  };
}

The revision counter gives refresh semantics without refetch hacks, and the hook stays compatible with current React used across SPFx releases. Components never see the service call — only data, loading, error, and refresh.

Service layer

The strongest structural decision in this guide. Never let a component reach SharePoint, Graph, business logic, rendering, and configuration at once — route everything through service interfaces with implementations per data source. Separation buys testability, reuse, centralized error handling, and maintainability that survives team changes:

EmployeeDirectory.tsx
↓
IEmployeeService
↓
EmployeeService
↓
PnPjs / Graph / REST

Service interfaces

Components depend on operations, never on data sources:

import type { IEmployee } from "../models/IEmployee";

export interface IEmployeeService {
  getEmployees(): Promise<IEmployee[]>;
  getEmployeeById(id: number): Promise<IEmployee | null>;
}

The React layer cares about getEmployees() — whether Graph or SharePoint answers is the service's business, which is exactly what makes data sources replaceable without touching UI code.

Dependency injection without a framework

The web part creates the service and passes it to React as a prop — production service in production, mock service in tests, test service in stories. No DI framework belongs in simple web parts; constructor-and-prop injection covers the need with zero dependencies and full explicitness.

  1. SPFx web part — constructs the service once.
  2. Passes service to React — as a typed prop.
  3. Production, mock, or test service — swapped at the boundary.

Models and types

Never hand raw API payloads to components. Map Graph and SharePoint responses into UI models at the service boundary:

Graph Response
↓
Service Mapping
↓
IEmployee
↓
React Component

Decoupling, type safety, simpler components, and painless API changes follow: when the endpoint reshapes, one mapping function changes instead of every component that renders a name.

SharePoint data access

React never dictates the access technology — the service layer chooses SPHttpClient with SharePoint REST, PnPjs, or Microsoft Graph per operation, and components stay ignorant of the choice. Deeper data-access guidance lives in SPFx and PnPjs, with the dedicated Graph article tracked as future content rather than a placeholder link here.

  1. React — renders props and state.
  2. Service — owns the operation contract.
  3. Data access technology — REST, PnPjs, or Graph per call.
  4. SharePoint — lists, libraries, and content.

PnPjs with React

Initialize PnPjs once with the web part context, call it only from services, select required fields, handle errors, and align versions with the SPFx release. A list-reading service shows the complete shape:

import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import type { WebPartContext } from "@microsoft/sp-webpart-base";

export interface IDirectoryEntry {
  id: number;
  displayName: string;
}

export async function getDirectoryEntries(
  context: WebPartContext,
  listTitle: string
): Promise<IDirectoryEntry[]> {
  const sp = spfi().using(SPFx(context));
  try {
    const items = await sp.web.lists
      .getByTitle(listTitle)
      .items.select("Id", "Title")();
    return items.map((item) => ({
      id: item.Id,
      displayName: item.Title
    }));
  } catch (error) {
    throw new Error(
      "Directory entries could not be loaded. " +
        "Confirm the list exists and the user can access it."
    );
  }
}

Verify current package names against official PnP documentation before implementing. The component-to-data chain reads: component, hook, service, PnPjs, SharePoint.

Microsoft Graph with React

Reach users, groups, Teams data, and profile information through the Graph client with delegated permissions, least privilege, and distinct 401 and 403 handling — all inside services, never components:

import type { WebPartContext } from "@microsoft/sp-webpart-base";
import type { MSGraphClientV3 } from "@microsoft/sp-http";

export async function getMyProfile(
  context: WebPartContext
): Promise<{ displayName: string }> {
  const client: MSGraphClientV3 =
    await context.msGraphClientFactory.getClient("3");
  try {
    const me = await client.api("/me").select("displayName").get();
    return { displayName: me.displayName as string };
  } catch (error) {
    throw new Error(
      "Profile could not be loaded. Confirm Graph permission approval."
    );
  }
}

Verify the client pattern against current documentation for your SPFx release. Concepts: Microsoft Graph for beginners.

External APIs from React

Components call hooks, hooks call services, services call an authenticated API client, and the client calls the external API — with backend mediation wherever confidential credentials are involved. Never place client secrets or privileged API keys in React or SPFx browser code; anything shipped to the browser is visible by definition.

Fluent UI

Fluent UI earns its place for Microsoft 365-aligned buttons, text fields, dropdowns, dialogs, panels, spinners, message bars, and details-style data display with an accessibility baseline. Verify current Fluent UI and SPFx compatibility — including package names and imports — before installing anything; never blindly install the newest Fluent UI package into an SPFx project, whose supported version moves with the SPFx release.

Responsive UI

Web parts render in wide and narrow sections, different page layouts, Teams-hosted contexts where supported, and mobile browsers. Never assume viewport width equals web part width — design components against available container space, let grids reflow, and test the narrowest section the web part allows.

Property pane and React

Configuration flows editor, property pane, web part property, React props, render — with values like list name, title, page size, and display options traveling as typed props. Keep configuration responsibilities in the web part; React components consume the resulting props without owning where configuration came from.

Events and callbacks

Data flows parent to props to child; events flow child to callback to parent — document selected, filter changed, refresh requested. Reserve alternatives to prop drilling for genuinely large applications; most web parts never reach the complexity that justifies them.

React Context, judiciously

Context suits genuinely shared, rarely changing values — theme, application configuration, a shared service instance. Never pour all application state into Context; over-shared state re-renders broadly, hides data flow, and resists testing. Local state first, hooks second, Context only where sharing earns it.

State management decisions

SituationAnswer
Simple component stateuseState
Reusable stateful logicCustom hook
Shared feature stateContext where appropriate
Complex application stateEvaluate a dedicated architecture or library only if justified

Redux-style libraries are never the default for SPFx web parts — most solutions never outgrow hooks plus services.

useMemo and useCallback

Optimization tools, not mandatory boilerplate: use them where referential stability or expensive calculations materially justify the cost. Memoizing everything adds complexity, hides bugs behind stale closures, and rarely moves measurable performance — profile first, memoize second.

Rendering performance

Avoid unnecessary renders, key lists stably, paginate, virtualize large UI lists where appropriate, memoize only where justified, keep huge objects out of state, and isolate frequently changing components so typing in a search box does not re-render the world. No benchmark claims are published — measure the solution, not the framework.

Data-access performance

Data discipline beats React micro-optimization: select required fields, filter server-side, paginate, batch where appropriate, cache where appropriate, avoid duplicate requests, and never load entire large lists. Bad loads everything and filters in the browser; better filters, selects, and pages server-side, returns needed data, and renders.

Stale requests and cancellation

Unmounted components, changed search queries, and out-of-order responses corrupt state without guards. The cancellation-flag pattern in this guide's effects ignores stale results for the API approaches used here — verify cancellation support against the actual client when adopting abort-style APIs rather than copying unsupported pseudo-patterns into production.

Search and debouncing

Search-driven web parts debounce input before requesting: user types, a short quiet window passes, one service request fires, results render. Firing an API request per keystroke wastes quota and races responses; a concise debounce keeps the UI live without the storm.

import * as React from "react";

export function SearchBox(props: {
  readonly onSearch: (query: string) => void;
}): JSX.Element {
  const [value, setValue] = React.useState<string>("");
  const timer = React.useRef<number | undefined>(undefined);

  function handleChange(next: string): void {
    setValue(next);
    window.clearTimeout(timer.current);
    timer.current = window.setTimeout(() => {
      props.onSearch(next);
    }, 300);
  }

  return (
    <input
      type="search"
      aria-label="Search employees"
      value={value}
      onChange={(event) => handleChange(event.target.value)}
    />
  );
}

Pagination

Page large SharePoint datasets with next/previous, load-more, or justified infinite scrolling — where the data layer honors the underlying API's paging mechanism. Never invent universal page-size recommendations; page by what the API supports and the UI needs.

Caching

Cache reference data, configuration, and slow-changing data — while respecting staleness, user-specific scoping, permission sensitivity, and invalidation. Never cache sensitive or per-user privileged data indiscriminately; a cache hit that leaks another user's data is a security incident, not an optimization.

Theming

Respect SharePoint and Microsoft 365 themes instead of hard-coding palettes that clash with tenant branding. Verify current SPFx theming APIs before publishing theme-reading code — theme surfaces move with releases, and hardcoded colors break dark and high-contrast experiences first.

CSS and styling

Follow the styling approach your SPFx release generates and supports: component-scoped styles, CSS modules where generated, Fluent UI, and theme-aware values. Avoid global CSS and never manipulate SharePoint platform styles — unsupported overrides break on service updates and leak across pages.

Accessibility

Semantic HTML, keyboard navigation, focus management, form labels, ARIA where necessary, accessible dialogs, screen-reader behavior, contrast, error messaging, and loading announcements where relevant — tested with keyboard-only interaction through every custom component. Accessibility is implementation quality from the first sprint, not a final checklist item.

Forms in React

Controlled inputs where appropriate, validation, required fields, submission state, error state, success state, and duplicate-submission prevention. Keep this guidance component-level; no forms framework is prescribed, and framework choice never excuses missing states.

Form validation layers

Client-side UX validation (formats, required fields, instant feedback) and server, API, or business validation (authority, uniqueness, permissions) are separate layers. Never rely on browser validation alone for security-sensitive rules — the service layer re-validates everything that matters.

Dialogs and panels

Details, editing, confirmation, configuration, and secondary workflows belong in accessible Fluent UI dialogs or panels where compatible — focus-trapped, keyboard-dismissible, and labeled. Custom modal implementations must re-prove every behavior the platform components provide for free.

List and table experiences

Loading, sorting, filtering, paging, selection, empty state, responsive behavior, and accessibility — without loading massive datasets simply because React can render rows. The data layer pages; the table renders pages; sorting and filtering push server-side wherever the API allows.

Reusable components

LoadingState, ErrorState, EmptyState, ConfirmDialog, SearchBox, Pagination, StatusBadge, and UserPicker wrappers earn reuse across web parts. Reuse where shapes genuinely repeat; over-abstraction — one mega-component with twelve modes — makes code harder to understand than duplication it replaced.

Error handling architecture

Errors travel React, hook, service, API — then back as classified states: API error, service classification, hook state, React error experience. Differentiate 401, 403, 404, throttling, server errors, network failures, and configuration problems, because each routes to a different owner and fix. Raw stack traces never reach users.

401 and 403 in SPFx React

Conceptually, 401 implicates authentication or token issues while 403 implicates authorization or permission issues — but never present these as universal diagnoses. In SPFx specifically, investigate SharePoint permissions, Graph scopes, API permission approval, current user access, token audience, and external API authorization, and link Graph-specific troubleshooting onward rather than duplicating it here.

React Error Boundaries

Error Boundaries catch rendering crashes in the tree below them — accurately per the React version your SPFx release supports — and render a fallback instead of unmounting the page. They do not catch API failures, event-handler throws, or async rejections; data errors stay in hook state, boundaries stay for render crashes. Both layers, each doing its own job.

Logging

Components never scatter uncontrolled console logging through production code. Keep development diagnostics local, log production-safe categories with correlation IDs where available, and never log tokens, secrets, or sensitive user data beyond need. Services log; components render; support reads logs, not screenshots of blank screens.

Security checklist

Standing rule: anything delivered to the browser is visible to the user. Design as if every bundle will be read — because it can be.

User-generated HTML and XSS

Rendering user or API-provided HTML carries cross-site scripting risk. Never casually reach for raw HTML injection in React; prefer text rendering and safe components. Where HTML rendering is genuinely required, sanitize at a trust boundary, restrict allowed markup, and document why the exception exists. No insecure examples are published here.

Testing components

Test rendering, interactions, loading, empty, and error states, service success and failure, and permission-sensitive behavior — using the test tooling actually compatible with the current SPFx ecosystem, verified before any installation instructions are followed. Untested states are unshipped states wearing a green build badge.

Mock services

Interfaces make testing trivial: production renders through the real service into Graph, while tests render the same components through a mock service into static data. This single example justifies the entire service-layer discipline — UI tested without tenants, tenants untouched by tests:

Production:  React → EmployeeService → Graph
Testing:     React → MockEmployeeService → Static Test Data
import type {
  IEmployeeService
} from "./IEmployeeService";
import type { IEmployee } from "../models/IEmployee";

export class MockEmployeeService implements IEmployeeService {
  public async getEmployees(): Promise<IEmployee[]> {
    return [
      { id: 1, displayName: "Ava", jobTitle: "Engineer" }
    ];
  }

  public async getEmployeeById(id: number): Promise<IEmployee | null> {
    const all = await this.getEmployees();
    return all.find((entry) => entry.id === id) ?? null;
  }
}

Development versus production

Tenants, URLs, list IDs, API endpoints, permissions, data volume, user permissions, and configuration all differ between development and production. A web part working for its developer proves nothing about production users — validate configuration resolution, permission behavior, and data scale in production-like conditions before sign-off.

Test with multiple personas

Where permissions matter, exercise owner, member, visitor or read-only, and restricted personas. Developer accounts are typically the most privileged identities in the tenant; testing only as an administrator certifies the experience for exactly one user who will never file a support ticket.

Example: employee directory architecture

Illustrative architecture tying this guide together — one scenario, every layer. An employee directory web part on a SharePoint page renders through an EmployeeDirectory component, reads through a useEmployees hook and an employee service interface into a Graph-backed implementation, and presents search, filters, cards, loading, empty, and error experiences:

  1. SharePoint page — hosts the web part.
  2. SPFx web part — context, properties, service construction.
  3. EmployeeDirectory React component — SearchBox, FilterPanel, EmployeeList.
  4. useEmployees hook — data, loading, error, refresh.
  5. IEmployeeService — the contract components depend on.
  6. GraphEmployeeService — delegated Graph implementation.
  7. Microsoft Graph — users behind approved scopes.

Example: component consuming the hook

export function EmployeeDirectory(props: {
  readonly service: IEmployeeService;
  readonly title: string;
}): JSX.Element {
  const { employees, loading, error, refresh } =
    useEmployees(props.service);

  if (loading) {
    return <LoadingState message="Loading employees…" />;
  }
  if (error) {
    return (
      <ErrorState
        title="Directory unavailable"
        message={error}
        onRetry={refresh}
      />
    );
  }
  if (employees.length === 0) {
    return <p>No employees found.</p>;
  }
  return (
    <section>
      <h2>{props.title}</h2>
      <EmployeeList employees={employees} />
    </section>
  );
}

Every branch from the state model renders; the hook owns lifecycle; the service owns data. Small, typed, and complete — the template for production components.

Complete architecture

SHAREPOINT ONLINE
↓
SPFx WEB PART
↓
REACT
├── Search
├── Filters
├── List
├── Loading
├── Empty
└── Error
↓
CUSTOM HOOK
↓
SERVICE INTERFACE
↓
SERVICE IMPLEMENTATION
├── PnPjs → SharePoint
├── Graph → Microsoft 365
└── API Client → Business System
↓
MICROSOFT ENTRA ID

Surrounded by: Configuration · Security ·
Accessibility · Logging · Performance

Bad versus better architecture

Bad: one component does everythingBetter: layered responsibilities
UI, API calls, business logic, configuration, error handling, and permissions in a single fileSPFx hosts; React renders; hooks hold state; services call APIs

Better is architectural guidance — separation of concerns — never a mandatory file count or folder structure. Small web parts legitimately collapse layers; the discipline is knowing which layer each line belongs to.

Production readiness checklist

Architecture: components focused; service layer used where justified; models and interfaces defined; configuration separated.

State: loading, empty, and error handled; async race conditions considered.

Data: required fields only; filtering and paging handled; duplicate calls minimized; large data considered.

Security: no secrets; permissions reviewed; external APIs secured; inputs and outputs reviewed.

UX: responsive; accessible; clear loading state; useful errors.

Operations: logging considered; multiple personas tested; production configuration validated; ownership documented.

Common SPFx and React mistakes

MistakeCorrective direction
Everything in one componentSplit containers, presentation, hooks, and services.
API calls scattered through UIRoute every call through service interfaces.
Incorrect useEffect dependenciesList every read value; stabilize services at construction.
No loading stateModel loading explicitly with indicators or skeletons.
No empty stateSeparate empty-data messaging from errors.
No error stateClassify failures with safe messages and retry.
Loading entire listsSelect, filter, and page server-side.
Hard-coded URLsExternalize to properties and environment configuration.
Secrets in ReactMove to a secured backend; redesign the integration.
Global mutable variablesPass values through props, hooks, and services.
Overusing React ContextLocal state first; Context only where sharing earns it.
Adding Redux unnecessarilyHooks plus services cover nearly all web parts.
Memoizing everythingProfile first; memoize where measurements justify.
Ignoring accessibilityKeyboard, focus, labels, and contrast from the first sprint.
Ignoring tenant themeRespect themes; never hard-code clashing palettes.
Testing only as adminExercise every persona that will use the solution.
Using unsupported React or package versionsMatch versions to the targeted SPFx release.
Blindly upgrading dependenciesCompatibility-test upgrades against the SPFx release.

Migration and modernization connection

Legacy JavaScript selected for SPFx rebuilds lands in exactly this architecture: requirement analyzed, components composed, hooks holding state, services calling supported APIs. Depth: Script Editor to SPFx modernization, Classic SharePoint to Modern SharePoint, and the migration hub.

  1. Legacy JavaScript — classified by function.
  2. Business requirement — defended by an owner.
  3. SPFx selected — simpler architectures ruled out.
  4. React architecture — this guide's layers.
  5. Services — supported APIs behind interfaces.
  6. Supported APIs — the modernization lands safely.

PnPjs and Graph next steps

React UI complete? Continue into SPFx and PnPjs for service-layer data access — lists, CRUD, filtering, paging, batching, files, and search — and SPFx and Microsoft Graph for delegated permissions, users, groups, paging, and throttling. Microsoft Graph for beginners still carries Graph fundamentals.

Building a complex SPFx and React solution

If you are working through component architecture, SharePoint data, Microsoft Graph, authentication, performance, or enterprise SPFx requirements, share the technical challenge with nextM365: Discuss Your SPFx Project.

Continue with Explore SPFx, the SPFx complete guide, Script Editor to SPFx modernization, and Classic to Modern SharePoint.

Related resources

Share this:

Topics covered

React · Project Structure · Permissions · ALM · Security

Frequently asked questions

Does SPFx use React?

React is the standard, best-supported UI layer for SPFx web parts, though the framework itself does not mandate it. Current guidance, samples, and the patterns in this guide assume React unless a project has a specific reason to differ.

Is React required for SPFx?

No. Web parts can use any framework or none. React is recommended because it matches current samples, Fluent UI integration, and the hiring and maintenance mainstream for SharePoint development.

Which React version does SPFx support?

The supported React major version moves with the SPFx release — React 18 support arrives in the SPFx v1.24 preview line. Never upgrade React independently inside an SPFx project; match the version your SPFx release supports.

Can React Hooks be used in SPFx?

Yes. Functional components with hooks such as useState and useEffect are the recommended modern pattern for SPFx React development, replacing legacy class-component approaches for new code.

How do you use useEffect in SPFx?

For side effects such as initial data loading and responding to changed dependencies: run the service call inside the effect, list every referenced value in the dependency array, and guard state updates so unmounted components and stale responses cannot corrupt state.

Should API calls be made directly from React components?

No. Route data access through a service layer behind interfaces. Components render props and state; services own PnPjs, Graph, REST, and external calls with centralized error handling.

Can SPFx React use PnPjs?

Yes, from the service layer — never scattered through components. Initialize PnPjs with the web part context, select only required fields, handle errors, and align the PnPjs major version with the targeted SPFx release.

Can SPFx React use Microsoft Graph?

Yes, through the Graph client with delegated permissions approved by a tenant administrator. Request least-privilege scopes and handle 401 and 403 distinctly in the service layer.

Can SPFx React call external APIs?

Yes, through Microsoft Entra authentication against secured endpoints, with backend mediation wherever the API cannot authenticate the user directly. Never embed secrets in React or SPFx browser code.

Should Redux be used with SPFx?

Almost never by default. Component state, custom hooks, and React Context cover the vast majority of web parts. Evaluate a dedicated state library only when justified complexity — shared mutable state across many features — actually exists.

How do you handle errors in SPFx React?

Model loading, empty, and error states explicitly; classify service failures by cause; keep raw API details out of the UI; and reserve Error Boundaries for rendering crashes, not data-call failures.

Can secrets be stored in React or SPFx?

No. Anything delivered to the browser is visible to the user, so client secrets, passwords, and privileged keys belong in a secured backend or managed-identity flow.

Sources

Have a Microsoft 365 topic idea?

Share article suggestions, community session ideas, corrections, or real-world scenarios for future nextM365 learning notes.

Connect with me

Keep learning Microsoft 365

Explore more practical tutorials for SharePoint, Power Platform, Copilot Studio, migration, automation, governance, and security.

Continue learning