Framework-agnostic React controls for Vite, Next.js, and SPFx - based on Fluent UI 9
npm install @spteck/react-controls-v2> Framework-agnostic React UI controls library based on Fluent UI 9 - Works seamlessly with Vite, Next.js, and SPFx!
V2 is a complete rewrite that eliminates all SPFx-specific dependencies, making these controls truly universal:
- ✅ Zero SPFx dependencies in the core library
- ✅ Works in any React app: Vite, Next.js, Create React App, SPFx
- ✅ Abstraction layer for flexibility and testability
- ✅ Tree-shakeable - only import what you need
- ✅ Full TypeScript support with comprehensive types
- ✅ Modern build system using Vite
``bash`
npm install @spteck/react-controls-v2
`bash`
npm install @spteck/react-controls-v2 @spteck/react-controls-v2-spfx-adapter
Some components (like UserCard, LivePersona, AIAssistant) require a Microsoft Graph client to fetch user data and presence information.
📖 See the complete setup guide: GRAPH_CLIENT_SETUP.md
Quick overview:
- SPFx: Automatically handled by the SPFx adapter ✅
- Vite/Next.js: You must provide a graph client with authentication (MSAL recommended)
- Development: Use a static token from Graph Explorer
`typescript
import React from 'react';
import {
UniversalProvider,
UserCard,
DefaultGraphClient,
ConsoleLoggingProvider,
LocalStorageProvider
} from '@spteck/react-controls-v2';
function App() {
const context = {
graphClient: new DefaultGraphClient({
accessToken: 'YOUR_ACCESS_TOKEN'
}),
loggingProvider: new ConsoleLoggingProvider('MyApp'),
storageProvider: new LocalStorageProvider(),
pageContext: {
user: {
displayName: 'John Doe',
email: 'john@contoso.com',
loginName: 'john@contoso.com'
}
}
};
return (
My Application
);
}
export default App;
`
`typescript
// app/layout.tsx or pages/_app.tsx
import {
UniversalProvider,
DefaultGraphClient,
IApplicationContext
} from '@spteck/react-controls-v2';
export default function RootLayout({ children }: { children: React.ReactNode }) {
// You can fetch the access token from your auth provider (NextAuth, etc.)
const context: IApplicationContext = {
graphClient: new DefaultGraphClient({
getAccessToken: async (scopes) => {
// Your logic to get token
return await getAccessToken(scopes);
}
}),
pageContext: {
user: {
displayName: 'Current User',
email: 'user@contoso.com',
loginName: 'user@contoso.com'
}
}
};
return (
$3
`typescript
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { UniversalProvider } from '@spteck/react-controls-v2';
import { SPFxContextAdapter } from '@spteck/react-controls-v2-spfx-adapter';
import { UserCard } from '@spteck/react-controls-v2';export default class MyWebPart extends BaseClientSideWebPart {
public render(): void {
// Convert SPFx context to Universal context
const universalContext = SPFxContextAdapter.adapt(
this.context,
'MyWebPart'
);
const element = (
Hello from SPFx!
);
ReactDom.render(element, this.domElement);
}
}
`🏗️ Architecture
$3
The library uses an abstraction layer that allows it to work across different platforms:
`typescript
interface IApplicationContext {
pageContext?: IPageContext;
graphClient?: IGraphProvider;
authProvider?: IAuthenticationProvider;
loggingProvider?: ILoggingProvider;
storageProvider?: IStorageProvider;
}
`$3
#### Default Providers (For Vite/Next.js)
`typescript
import {
DefaultGraphClient,
ConsoleLoggingProvider,
LocalStorageProvider
} from '@spteck/react-controls-v2/providers';const graphClient = new DefaultGraphClient({
accessToken: 'token',
// OR
getAccessToken: async (scopes) => await fetchToken(scopes)
});
const logger = new ConsoleLoggingProvider('MyApp');
const storage = new LocalStorageProvider('myapp_');
`#### SPFx Providers (via Adapter)
`typescript
import { SPFxContextAdapter } from '@spteck/react-controls-v2-spfx-adapter';const context = SPFxContextAdapter.adapt(this.context, 'ComponentName');
// This automatically provides SPFx-native implementations of all providers
`📚 Available Components
$3
- Card - Flexible card container
- Grid - Responsive grid system
- Layout - Page layout wrapper
- Space - Spacing component
- Stack - Vertical/horizontal stack
- AspectRatio - Maintain aspect ratios
- Center - Center content
- Container - Content container
- Carousel - Image/content carousel
- Popup - Popup dialogs$3
- TypographyControl - Text styling
- ButtonMenu - Button with dropdown menu
- RenderLabel - Label rendering
- RenderSpinner - Loading spinners$3
(More components coming in future releases)🎣 Available Hooks
$3
Access the application context anywhere in your component tree:`typescript
import { useApplicationContext } from '@spteck/react-controls-v2';function MyComponent() {
const context = useApplicationContext();
const user = context?.pageContext?.user;
return
Welcome, {user?.displayName}!;
}
`$3
Make Microsoft Graph API calls:`typescript
import { useGraphAPI } from '@spteck/react-controls-v2';function UserProfile() {
const { getUser, getCurrentUser } = useGraphAPI();
useEffect(() => {
async function fetchUser() {
const user = await getCurrentUser();
console.log(user);
}
fetchUser();
}, []);
return
...;
}
`$3
Logging with different levels:`typescript
import { useLogging, LogLevel, ErrorType } from '@spteck/react-controls-v2';function MyComponent() {
const { log, error, warn, info } = useLogging();
useEffect(() => {
info('Component mounted');
log('Custom log message', LogLevel.Verbose);
}, []);
const handleError = (err: Error) => {
error('An error occurred', err, ErrorType.Error);
};
return
...;
}
`$3
Caching with expiration:`typescript
import { useIndexedDBCache } from '@spteck/react-controls-v2';function CachedDataComponent() {
const { getCachedData, setCachedData } = useIndexedDBCache();
useEffect(() => {
async function loadData() {
const cached = await getCachedData('myKey');
if (!cached) {
const freshData = await fetchData();
await setCachedData('myKey', freshData, 60); // Cache for 60 minutes
}
}
loadData();
}, []);
return
...;
}
`$3
Poll data at regular intervals:`typescript
import { usePolling } from '@spteck/react-controls-v2';function LiveDataComponent() {
const { startPolling, stopPolling, isPolling } = usePolling(
async () => {
const data = await fetchLiveData();
setData(data);
},
5000, // Poll every 5 seconds
true // Start immediately
);
return (
{isPolling && Polling...}
);
}
`$3
Handle timezone conversions:`typescript
import { useTimeZoneHelper } from '@spteck/react-controls-v2';function DateDisplay() {
const { formatInTimeZone, timeZone } = useTimeZoneHelper();
const formattedDate = formatInTimeZone(
new Date(),
'yyyy-MM-dd HH:mm:ss',
'America/New_York'
);
return
Current timezone: {timeZone};
}
`🔧 Utility Functions
$3
`typescript
import { debounce, throttle, cloneDeep } from '@spteck/react-controls-v2';const debouncedSearch = debounce((query) => {
performSearch(query);
}, 300);
`🎨 Theming
All components use Fluent UI 9 theming. You can wrap your app with Fluent UI providers:
`typescript
import { FluentProvider, webLightTheme } from '@fluentui/react-components';
import { UniversalProvider } from '@spteck/react-controls-v2';function App() {
return (
);
}
`📖 Migration from V1
If you're migrating from V1 (the SPFx-dependent version):
1. For SPFx apps: Install the adapter package and use
SPFxContextAdapter.adapt()
2. For non-SPFx apps: Replace context with IApplicationContext
3. Update imports: Change from @spteck/m365-hooks to internal hooks
4. Provider setup: Wrap your app with UniversalProviderExample migration:
`typescript
// V1 (SPFx only)
import { UserCard } from '@spteck/react-controls';
// V2 (SPFx)
import { UniversalProvider, UserCard } from '@spteck/react-controls-v2';
import { SPFxContextAdapter } from '@spteck/react-controls-v2-spfx-adapter';
const universalContext = SPFxContextAdapter.adapt(this.context);
// V2 (Non-SPFx)
import { UniversalProvider, UserCard } from '@spteck/react-controls-v2';
``Contributions are welcome! Please read our contributing guidelines before submitting PRs.
MIT © João Mendes
- NPM Package
- GitHub Repository
- Documentation
Give a ⭐️ if this project helped you!