Home / SharePoint / SharePoint Framework (SPFx): Building Custom Solutions
SharePoint

SharePoint Framework (SPFx): Building Custom Solutions

SharePoint Framework (SPFx) is the modern development model for building SharePoint customizations. This guide covers SPFx architecture, creating web parts...

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

SharePoint Framework (SPFx): Building Custom Solutions

} ] } ] };``` } }


### React Component

```typescript
// src/webparts/helloWorld/components/HelloWorld.tsx
import * as React from 'react';
import styles from './HelloWorld.module.scss';
import { IHelloWorldProps } from './IHelloWorldProps';
import { PrimaryButton } from '@fluentui/react/lib/Button';
import { TextField } from '@fluentui/react/lib/TextField';

export interface IHelloWorldState {
  name: string;
  greeting: string;
}

export default class HelloWorld extends React.Component<IHelloWorldProps, IHelloWorldState> {
  
  constructor(props: IHelloWorldProps) {
```text
super(props);

this.state = {
  name: '',
  greeting: ''
};```
  }

  private handleNameChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
```text
this.setState({ name: newValue || '' });```
  }

  private handleGreet = (): void => {
```text
this.setState({ greeting: `Hello, ${this.state.name}!` });```
  }

  public render(): React.ReactElement<IHelloWorldProps> {
```text
return (
  <div className={styles.helloWorld}>
    <div className={styles.container}>
      <div className={styles.row}>
        <div className={styles.column}>
          <h2>{this.props.title}</h2>
          <p>{this.props.description}</p>
          
          <TextField 
            label="Enter Vladimir Luis"
            value={this.state.name}
            onChange={this.handleNameChange}
          />
          
          <PrimaryButton 
            text="Greet Me" 
            onClick={this.handleGreet}
          />
          
          {this.state.greeting && (
            <div className={styles.greeting}>
              <h3>{this.state.greeting}</h3>
            </div>
          )}
          
          <p>Current user: {this.props.userDisplayName}</p>
        </div>
      </div>
    </div>
  </div>
);```
  }
}

Styling with SCSS

// src/webparts/helloWorld/components/HelloWorld.module.scss
.helloWorld {
  .container {
```yaml
max-width: 700px;
margin: 0px auto;
padding: 20px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);```
  }

  .row {
```yaml
padding: 20px;```
  }

  .column {
```text
.greeting {
  margin-top: 20px;
  padding: 15px;
  background-color: #f3f2f1;
  border-left: 4px solid #0078d4;
  
  h3 {
    margin: 0;
    color: #0078d4;
  }
}```
  }
}

SharePoint REST API Integration

SharePoint REST API Integration

Read List Items

// src/services/SharePointService.ts
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';

export interface IListItem {
  Id: number;
  Title: string;
  Description: string;
  Modified: string;
}

export class SharePointService {
  
  public static async getListItems(context: WebPartContext, listTitle: string): Promise<IListItem[]> {
```javascript
const endpoint = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listTitle}')/items?$select=Id,Title,Description,Modified&$orderby=Modified desc&$top=100`;

const response: SPHttpClientResponse = await context.spHttpClient.get(
  endpoint,
  SPHttpClient.configurations.v1
);

if (!response.ok) {
  throw new Error(`Error fetching list items: ${response.statusText}`);
}

const data = await response.json();
return data.value;```
  }
  
  public static async createListItem(context: WebPartContext, listTitle: string, item: Partial<IListItem>): Promise<IListItem> {
```javascript
const endpoint = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listTitle}')/items`;

const response: SPHttpClientResponse = await context.spHttpClient.post(
  endpoint,
  SPHttpClient.configurations.v1,
  {
    headers: {
      'Accept': 'application/json;odata=nometadata',
      'Content-Type': 'application/json;odata=nometadata'
    },
    body: JSON.stringify(item)
  }
);

if (!response.ok) {
  throw new Error(`Error creating item: ${response.statusText}`);
}

return await response.json();```
  }
  
  public static async updateListItem(context: WebPartContext, listTitle: string, itemId: number, updates: Partial<IListItem>): Promise<void> {
```javascript
const endpoint = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listTitle}')/items(${itemId})`;

const response: SPHttpClientResponse = await context.spHttpClient.post(
  endpoint,
  SPHttpClient.configurations.v1,
  {
    headers: {
      'Accept': 'application/json;odata=nometadata',
      'Content-Type': 'application/json;odata=nometadata',
      'IF-MATCH': '*',
      'X-HTTP-Method': 'MERGE'
    },
    body: JSON.stringify(updates)
  }
);

if (!response.ok) {
  throw new Error(`Error updating item: ${response.statusText}`);
}```
  }
  
  public static async deleteListItem(context: WebPartContext, listTitle: string, itemId: number): Promise<void> {
```javascript
const endpoint = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listTitle}')/items(${itemId})`;

const response: SPHttpClientResponse = await context.spHttpClient.post(
  endpoint,
  SPHttpClient.configurations.v1,
  {
    headers: {
      'Accept': 'application/json;odata=nometadata',
      'IF-MATCH': '*',
      'X-HTTP-Method': 'DELETE'
    }
  }
);

if (!response.ok) {
  throw new Error(`Error deleting item: ${response.statusText}`);
}```
  }
}

Use in Component

import * as React from 'react';
import { SharePointService, IListItem } from '../services/SharePointService';
import { IListViewProps } from './IListViewProps';
import { DetailsList, IColumn } from '@fluentui/react/lib/DetailsList';
import { Spinner, SpinnerSize } from '@fluentui/react/lib/Spinner';

export interface IListViewState {
  items: IListItem[];
  loading: boolean;
  error: string;
}

export default class ListView extends React.Component<IListViewProps, IListViewState> {
  
  private columns: IColumn[] = [
```json
{ key: 'Title', name: 'Title', fieldName: 'Title', minWidth: 200, maxWidth: 300 },
{ key: 'Description', name: 'Description', fieldName: 'Description', minWidth: 300 },
{ key: 'Modified', name: 'Modified', fieldName: 'Modified', minWidth: 150 }```
  ];
  
  constructor(props: IListViewProps) {
```text
super(props);

this.state = {
  items: [],
  loading: true,


  error: ''
};```
  }
  
  public async componentDidMount(): Promise<void> {
```text
await this.loadItems();```
  }
  
  private async loadItems(): Promise<void> {
```javascript
try {
  const items = await SharePointService.getListItems(this.props.context, this.props.listName);
  this.setState({ items, loading: false });
} catch (error) {
  this.setState({ error: error.message, loading: false });
}```
  }
  
  public render(): React.ReactElement<IListViewProps> {
```javascript
const { items, loading, error } = this.state;

if (loading) {
  return <Spinner size={SpinnerSize.large} label="Loading items..." />;
}

if (error) {
  return <div>Error: {error}</div>;
}

return (
  <DetailsList
    items={items}
    columns={this.columns}
    setKey="set"
    selectionMode={0}
  />
);```
  }
}

Microsoft Graph API Integration

Configure API Permissions

// config/package-solution.json
{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
  "solution": {
```text
"name": "my-solution",
"id": "12345678-1234-1234-1234-123456789abc",
"version": "1.0.0.0",
"webApiPermissionRequests": [
  {
    "resource": "Microsoft Graph",
    "scope": "User.Read.All"
  },
  {
    "resource": "Microsoft Graph",
    "scope": "Mail.Read"
  }
]```
  }
}

Call Microsoft Graph

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

export interface IUser {
  displayName: string;
  mail: string;
  jobTitle: string;
  department: string;
}

export class GraphService {
  
  public static async getCurrentUser(context: WebPartContext): Promise<IUser> {
```javascript
const client: MSGraphClientV3 = await context.msGraphClientFactory.getClient('3');

const user = await client
  .api('/me')
  .select('displayName,mail,jobTitle,department')
  .get();

return user;```
  }
  
  public static async getUsers(context: WebPartContext): Promise<IUser[]> {
```javascript
const client: MSGraphClientV3 = await context.msGraphClientFactory.getClient('3');

const response = await client
  .api('/users')
  .top(50)
  .select('displayName,mail,jobTitle,department')
  .orderby('displayName')
  .get();

return response.value;```
  }
  
  public static async getUserPhoto(context: WebPartContext, userId: string): Promise<string> {
```javascript
const client: MSGraphClientV3 = await context.msGraphClientFactory.getClient('3');

try {
  const photoBlob = await client
    .api(`/users/${userId}/photo/$value`)
    .get();
  
  const url = window.URL.createObjectURL(photoBlob);
  return url;
} catch (error) {
  console.warn('No photo available for user');
  return '';
}```
  }
}

SPFx Extensions

SPFx Extensions

Application Customizer

Header/footer customization:

// src/extensions/header/HeaderApplicationCustomizer.ts
import { override } from '@microsoft/decorators';
import { BaseApplicationCustomizer, PlaceholderContent, PlaceholderName } from '@microsoft/sp-application-base';

export default class HeaderApplicationCustomizer extends BaseApplicationCustomizer<{}> {
  
  private topPlaceholder: PlaceholderContent | undefined;
  
  @override
  public onInit(): Promise<void> {
```text
this.context.placeholderProvider.changedEvent.add(this, this.renderPlaceHolders);
this.renderPlaceHolders();

return Promise.resolve();```
  }
  
  private renderPlaceHolders(): void {
```text
if (!this.topPlaceholder) {
  this.topPlaceholder = this.context.placeholderProvider.tryCreateContent(
    PlaceholderName.Top,
    { onDispose: this.onDispose }
  );
  
  if (this.topPlaceholder) {
    this.topPlaceholder.domElement.innerHTML = `
      <div style="background-color: #0078d4; color: white; padding: 10px; text-align: center;">
        <strong>Company Announcement:</strong> System maintenance on Saturday 10pm-2am
      </div>
    `;
  }
}```
  }
}

Field Customizer

Custom field rendering:

// src/extensions/statusField/StatusFieldCustomizer.ts
import { override } from '@microsoft/decorators';
import { BaseFieldCustomizer, IFieldCustomizerCellEventParameters } from '@microsoft/sp-listview-extensibility';

export default class StatusFieldCustomizer extends BaseFieldCustomizer<{}> {
  
  @override
  public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
```javascript
const status = event.fieldValue as string;
let color = '#605e5c';

switch (status?.toLowerCase()) {
  case 'approved':
    color = '#107c10';
    break;
  case 'pending':
    color = '#ffa500';
    break;
  case 'rejected':
    color = '#d13438';
    break;
}

event.domElement.innerHTML = `
  <div style="
    display: inline-block;
    padding: 4px 12px;
    border-radius: 12px;
    background-color: ${color};
    color: white;
    font-weight: 600;
    font-size: 12px;
  ">
    ${status}
  </div>
`;```
  }
}

Command Set

Custom list item actions:

// src/extensions/customCommands/CustomCommandsCommandSet.ts
import { override } from '@microsoft/decorators';
import { BaseListViewCommandSet, IListViewCommandSetExecuteEventParameters } from '@microsoft/sp-listview-extensibility';

export default class CustomCommandsCommandSet extends BaseListViewCommandSet<{}> {
  
  @override
  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
```text
switch (event.itemId) {
  case 'ARCHIVE_ITEM':
    this.archiveItem(event.selectedRows[0].getValueByName('ID'));
    break;
  
  case 'EXPORT_ITEM':
    this.exportItem(event.selectedRows[0]);
    break;
}```
  }
  
  private async archiveItem(itemId: number): Promise<void> {
```javascript
// Archive logic
console.log(`Archiving item ${itemId}`);```
  }
  
  private exportItem(item: any): void {
```javascript
// Export logic
console.log('Exporting item:', item);```
  }
}

Packaging and Deployment

Build Package

## Update version in package-solution.json

![Update version in package-solution.json](/images/articles/sharepoint/2025-11-03-sharepoint-framework-spfx-building-custom-solutions-ctx-3.svg)




## Increment version: 1.0.0.0 → 1.0.1.0





## Bundle solution
gulp bundle --ship





## Package solution
gulp package-solution --ship





## Output: sharepoint/solution/*.sppkg

Deploy to App Catalog





## Connect to tenant
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive





## Upload to app catalog
Add-PnPApp -Path ".\sharepoint\solution\my-solution.sppkg" -Scope Tenant -Overwrite -Publish





## Deploy tenant-wide (optional)
Update-PnPApp -Identity "my-solution-client-side-solution" -Scope Tenant





Expected output:

Connected to https://contoso.sharepoint.com

Terminal output for Connect-PnPOnline

Add to Site





## Connect to site
Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/teamsite" -Interactive





## Install app
Install-PnPApp -Identity "my-solution-client-side-solution"





Expected output:

Connected to https://contoso.sharepoint.com

Terminal output for Connect-PnPOnline

Best Practices

Performance

  1. Lazy loading - Load heavy components on demand
  2. Caching - Cache API responses
  3. Pagination - Load data in chunks
  4. Bundle optimization - Tree shaking, code splitting
// Lazy load component
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function MyComponent() {
  return (
```text
<React.Suspense fallback={<Spinner />}>
  <HeavyComponent />
</React.Suspense>```
  );
}

Error Handling

try {
  const items = await SharePointService.getListItems(context, 'Tasks');
  this.setState({ items });
} catch (error) {
  console.error('Error loading items:', error);
  this.setState({ 
```yaml
error: 'Failed to load items. Please try again.' ```
  });
}

Accessibility

<PrimaryButton 
  text="Submit"
  ariaLabel="Submit form"
  onClick={this.handleSubmit}
/>

<TextField
  label="Name"
  ariaLabel="Enter Vladimir Luis"
  required
/>

Architecture Decision and Tradeoffs

When designing content management and collaboration solutions with SharePoint, consider these key architectural trade-offs:

Approach Best For Tradeoff
Managed / platform service Rapid delivery, reduced ops burden Less customisation, potential vendor lock-in
Custom / self-hosted Full control, advanced tuning Higher operational overhead and cost

Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

Validation and Versioning

  • Last validated: April 2026
  • Validate examples against your tenant, region, and SKU constraints before production rollout.
  • Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.

Security and Governance Considerations

  • Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
  • Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
  • Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.

Cost and Performance Notes

  • Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
  • Baseline performance with synthetic and real-user checks before and after major changes.
  • Scale resources with measured thresholds and revisit sizing after usage pattern changes.

Official Microsoft References

  • https://learn.microsoft.com/sharepoint/
  • https://learn.microsoft.com/microsoft-365/enterprise/
  • https://learn.microsoft.com/purview/

Public Examples from Official Sources

  • These examples are sourced from official public Microsoft documentation and sample repositories.
  • Documentation examples: https://learn.microsoft.com/sharepoint/dev/
  • Sample repositories: https://github.com/SharePoint/sp-dev-docs
  • Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.

Key Takeaways

  • SPFx enables modern client-side SharePoint development
  • React integration provides component-based architecture
  • SharePoint REST API and Microsoft Graph access data
  • Extensions customize SharePoint UI without code deployment
  • Tenant-wide deployment simplifies distribution
  • Follow best practices for performance and accessibility

Next Steps

  • Build custom web parts for common scenarios
  • Explore PnP SPFx controls and reusable components
  • Implement CI/CD pipeline for automated deployment
  • Create extension libraries for reusable code

Additional Resources


Build modern. Deploy seamlessly.

Discussion