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
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
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

## 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
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
Best Practices
Performance
- Lazy loading - Load heavy components on demand
- Caching - Cache API responses
- Pagination - Load data in chunks
- 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