- Published on
Getting User Profile Data with Graph in SPFx
The SharePoint user profile service has its own REST API, but Microsoft Graph gives you a richer, more consistent dataset — and it is the direction Microsoft is investing in going forward.
This article builds a complete user profile card web part using Graph: display name, job title, department, manager, contact details, profile photo, and presence status — all rendered with the Fluent UI v9 Persona component.
📦 Install Required Packages
npm install @fluentui/react-components --save
npm install @fluentui/react-icons --save
🗂️ Define the Profile Interface
src/models/IUserProfile.ts
export interface IUserProfile {
id: string;
displayName: string;
mail: string;
jobTitle: string | null;
department: string | null;
officeLocation: string | null;
mobilePhone: string | null;
businessPhones: string[];
managerDisplayName: string | null;
managerMail: string | null;
photoUrl: string | null;
presence: 'Available' | 'Busy' | 'Away' | 'DoNotDisturb' | 'BeRightBack' | 'Offline' | null;
}
🔧 Build the Profile Service
src/services/UserProfileService.ts
import { MSGraphClientV3 } from '@microsoft/sp-http';
import { IUserProfile } from '../models/IUserProfile';
export class UserProfileService {
private readonly _client: MSGraphClientV3;
constructor(client: MSGraphClientV3) {
this._client = client;
}
public async getCurrentUserProfile(): Promise<IUserProfile> {
// Fetch profile, manager, and presence in parallel
const [profileResponse, managerResponse, presenceResponse, photoResponse] =
await Promise.allSettled([
this._client
.api('/me')
.select('id,displayName,mail,jobTitle,department,officeLocation,mobilePhone,businessPhones')
.get(),
this._client
.api('/me/manager')
.select('displayName,mail')
.get(),
this._client
.api('/me/presence')
.get(),
this._client
.api('/me/photo/$value')
.responseType('blob' as any)
.get()
]);
// Base profile — required, throw if this fails
if (profileResponse.status === 'rejected') {
throw new Error(`Failed to load user profile: ${profileResponse.reason}`);
}
const profile = profileResponse.value;
// Manager — optional, degrade gracefully on 404
const manager = managerResponse.status === 'fulfilled'
? managerResponse.value
: null;
// Presence — optional, may be restricted by tenant policy
const presence = presenceResponse.status === 'fulfilled'
? presenceResponse.value
: null;
// Profile photo — convert blob to object URL
let photoUrl: string | null = null;
if (photoResponse.status === 'fulfilled' && photoResponse.value) {
try {
photoUrl = URL.createObjectURL(photoResponse.value);
} catch {
photoUrl = null;
}
}
return {
id: profile.id,
displayName: profile.displayName,
mail: profile.mail,
jobTitle: profile.jobTitle ?? null,
department: profile.department ?? null,
officeLocation: profile.officeLocation ?? null,
mobilePhone: profile.mobilePhone ?? null,
businessPhones: profile.businessPhones ?? [],
managerDisplayName: manager?.displayName ?? null,
managerMail: manager?.mail ?? null,
photoUrl,
presence: presence?.availability ?? null
};
}
}
Using Promise.allSettled ensures the profile loads even if the manager lookup or presence check fails due to permissions or policy. Each optional piece degrades independently.
⚛️ Build the Profile Card Component
src/components/UserProfileCard.tsx
import * as React from 'react';
import {
Avatar,
Badge,
Text,
makeStyles,
tokens
} from '@fluentui/react-components';
import {
MailRegular,
PhoneRegular,
BuildingRegular,
PeopleRegular,
LocationRegular
} from '@fluentui/react-icons';
import { IUserProfile } from '../models/IUserProfile';
const useStyles = makeStyles({
card: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
padding: tokens.spacingVerticalL,
background: tokens.colorNeutralBackground1,
borderRadius: tokens.borderRadiusMedium,
boxShadow: tokens.shadow4,
maxWidth: '360px'
},
header: {
display: 'flex',
alignItems: 'center',
gap: tokens.spacingHorizontalM
},
avatarWrap: {
position: 'relative'
},
presenceBadge: {
position: 'absolute',
bottom: 0,
right: 0
},
details: {
flex: 1
},
name: {
fontWeight: tokens.fontWeightSemibold,
fontSize: tokens.fontSizeBase400
},
title: {
color: tokens.colorNeutralForeground2,
fontSize: tokens.fontSizeBase200
},
infoRow: {
display: 'flex',
alignItems: 'center',
gap: tokens.spacingHorizontalS,
fontSize: tokens.fontSizeBase200,
color: tokens.colorNeutralForeground2
},
divider: {
height: '1px',
background: tokens.colorNeutralStroke1
}
});
const PRESENCE_COLORS: Record<string, 'available' | 'away' | 'busy' | 'do-not-disturb' | 'offline' | 'out-of-office'> = {
Available: 'available',
Away: 'away',
BeRightBack: 'away',
Busy: 'busy',
DoNotDisturb: 'do-not-disturb',
Offline: 'offline'
};
interface IUserProfileCardProps {
profile: IUserProfile;
}
const UserProfileCard: React.FC<IUserProfileCardProps> = ({ profile }) => {
const styles = useStyles();
const presenceStatus = profile.presence ? PRESENCE_COLORS[profile.presence] : undefined;
return (
<div className={styles.card}>
<div className={styles.header}>
<div className={styles.avatarWrap}>
<Avatar
name={profile.displayName}
image={profile.photoUrl ? { src: profile.photoUrl } : undefined}
size={56}
/>
{presenceStatus && (
<Badge
className={styles.presenceBadge}
size="small"
status={presenceStatus}
/>
)}
</div>
<div className={styles.details}>
<Text className={styles.name} block>{profile.displayName}</Text>
{profile.jobTitle && (
<Text className={styles.title} block>{profile.jobTitle}</Text>
)}
{profile.department && (
<Text className={styles.title} block>{profile.department}</Text>
)}
</div>
</div>
<div className={styles.divider} />
{profile.mail && (
<div className={styles.infoRow}>
<MailRegular />
<a href={`mailto:${profile.mail}`}>{profile.mail}</a>
</div>
)}
{profile.businessPhones[0] && (
<div className={styles.infoRow}>
<PhoneRegular />
<span>{profile.businessPhones[0]}</span>
</div>
)}
{profile.officeLocation && (
<div className={styles.infoRow}>
<LocationRegular />
<span>{profile.officeLocation}</span>
</div>
)}
{profile.managerDisplayName && (
<div className={styles.infoRow}>
<PeopleRegular />
<span>Reports to: {profile.managerDisplayName}</span>
</div>
)}
</div>
);
};
export default UserProfileCard;
🔧 Wire It Up in the Web Part
import { MSGraphClientV3 } from '@microsoft/sp-http';
import { UserProfileService } from './services/UserProfileService';
import { IUserProfile } from './models/IUserProfile';
export default class UserProfileWebPart extends BaseClientSideWebPart<{}> {
private _profileService: UserProfileService;
protected async onInit(): Promise<void> {
await super.onInit();
const client: MSGraphClientV3 =
await this.context.msGraphClientFactory.getClient('3');
this._profileService = new UserProfileService(client);
}
public render(): void {
const element = React.createElement(UserProfileContainer, {
profileService: this._profileService
});
ReactDOM.render(
React.createElement(FluentProvider, { theme: webLightTheme }, element),
this.domElement
);
}
protected onDispose(): void {
ReactDOM.unmountComponentAtNode(this.domElement);
}
}
const UserProfileContainer: React.FC<{ profileService: UserProfileService }> = ({ profileService }) => {
const [profile, setProfile] = React.useState<IUserProfile | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
profileService.getCurrentUserProfile()
.then(setProfile)
.catch(err => setError(err.message));
}, []);
if (error) return <p>Error: {error}</p>;
if (!profile) return <Spinner label="Loading profile..." />;
return <UserProfileCard profile={profile} />;
};
📋 Required Permissions
{
"solution": {
"webApiPermissionRequests": [
{ "resource": "Microsoft Graph", "scope": "User.Read" },
{ "resource": "Microsoft Graph", "scope": "User.ReadBasic.All" },
{ "resource": "Microsoft Graph", "scope": "Presence.Read.All" }
]
}
}
Presence.Read.All is optional — the card degrades gracefully without it. Only request it if your tenant allows it and your use case genuinely needs presence status.
🚀 Deploy
npm run build
npm run bundle -- --ship
npm run package-solution -- --ship
📂 GitHub Source
View full SPFx project on GitHub:SPFx user profile card web part — Graph API + Fluent UI v9 Persona component
✅ Summary
- Use
Promise.allSettledto fetch profile, manager, presence, and photo in parallel — each piece degrades independently if permissions are missing. - Convert the photo blob to an
objectURLfor use in theAvatarcomponent — revoke it inonDisposeto avoid memory leaks. - Map Graph presence availability strings to Fluent UI v9
Badgestatus values for the presence indicator. - Declare
Presence.Read.Allonly if you need presence — it is sensitive and often restricted in enterprise tenants. - The
UserProfileServicepattern keeps all Graph calls out of your React components.
Happy coding!
Author
Ravichandran@Hi_Ravichandran
