Introduction

Fusion Framework is a collection of libraries for helping developers create applications for the Fusion eco-system.
develop once, run anywhere
Our vision is to provide an framework which is the foundation for all applications and portals within the eco system.
Our philosophy is to create small stand-alone modules which the developers can use in portals and application. read more about modules
@equinor/fusion-framework-react-app
React bindings for building modular Fusion Framework applications. Provides rendering helpers, configuration callbacks, and React hooks for accessing framework modules (HTTP, auth, context, navigation, bookmarks, settings, and more).
Features
- One-line app bootstrap via
renderAppβ creates a render function that mounts your component with React 18'screateRoot - Module hooks β
useAppModule/useAppModulesfor type-safe access to any registered module - Sub-path entry-points for optional capabilities: MSAL auth, HTTP, context, navigation, bookmarks, feature flags, settings, analytics, AG Grid theming, help center, and apploader
- Environment variables hook β
useAppEnvironmentVariablesfor accessing app config at runtime - Lazy loading β components are wrapped in
React.lazy+Suspenseautomatically
Installation
pnpm add @equinor/fusion-framework-react-appUsage
Bootstrap an application
// config.ts
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
export const configure: AppModuleInitiator = (configurator) => {
configurator.http.configureClient('my-api', {
baseUri: 'https://api.example.com',
defaultScopes: ['api://my-api/.default'],
});
};
// App.tsx
export const App = () => <h1>Hello Fusion</h1>;
// index.ts
import { renderApp } from '@equinor/fusion-framework-react-app';
import { App } from './App';
import { configure } from './config';
export const render = renderApp(App, configure);
export default render;Access a module
import { useAppModule } from '@equinor/fusion-framework-react-app';
const MyComponent = () => {
const auth = useAppModule('auth');
// ...
};HTTP requests
import { useHttpClient } from '@equinor/fusion-framework-react-app/http';
const MyComponent = () => {
const client = useHttpClient('my-api');
// client.fetch$('endpoint').subscribe(...)
// await client.fetchAsync('endpoint')
};MSAL authentication
Note: Requires
@equinor/fusion-framework-module-msal. The MSAL module must be configured by the host/portal β apps should only consume the hooks.
import { useCurrentAccount, useAccessToken } from '@equinor/fusion-framework-react-app/msal';
const UserInfo = () => {
const user = useCurrentAccount();
const { token } = useAccessToken({ scopes: ['User.Read'] });
if (!user) return <p>Not signed in</p>;
return <p>Welcome, {user.name}</p>;
};Context
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context';
const ContextInfo = () => {
const context = useCurrentContext();
return <p>Selected: {context?.title ?? 'none'}</p>;
};Navigation / routing
import { useRouter } from '@equinor/fusion-framework-react-app/navigation';
import { RouterProvider } from '@equinor/fusion-framework-react-router';
const routes = [{ path: '/', element: <Home /> }];
const App = () => {
const router = useRouter(routes);
return <RouterProvider router={router} />;
};State Management
The Fusion Framework provides a powerful state management solution that enables persistent, cross-component state sharing with automatic synchronization. Unlike traditional React state that's lost on page refresh, this state persists across app sessions and stays synchronized between different components in real-time.
Key Benefits:
- π Persistent State: Survives page refreshes and app restarts
- π Cross-Component Sync: Share state between any components instantly
- β‘ Optimistic Updates: Responsive UI with automatic rollback on errors
- π‘οΈ Type Safe: Full TypeScript support with type inference
- π― Simple API: Works like
useStatebut with persistence
Use Cases:
- User preferences and settings
- Form data that should persist
- UI state like filters, sorting, or view modes
- Data that needs to be shared across multiple components
- Cache management for expensive operations
Installation
First, install the state module package:
pnpm install @equinor/fusion-framework-module-stateSetup
Enable the state module in your app configuration. This initializes the persistent storage and makes useAppState available throughout your application:
import { enableAppState } from '@equinor/fusion-framework-react-app/state';
export const configure: ModuleInitiator = (appConfigurator) => {
enableAppState(appConfigurator);
};Caution
The state management module is a powerful tool, but it's important to know the potential pitfalls and limitations when using it in your application. The state management is global and can lead to unexpected behavior if not used carefully.
example 1: If you have multiple components that rely on the same state, updating the state in one component can cause re-renders in all components that use that state, potentially leading to performance issues.
example 2: The user has open multiple tabs of the application, and each tab is modifying the same state. This can lead to unexpected behavior, as changes made in one tab will be reflected to all tabs. (like storing user preferences for selected columns)
Basic Usage
Use useAppState just like React's useState, but with automatic persistence. The first parameter is a unique key, and the second is an options object with the default value:
import { useAppState } from '@equinor/fusion-framework-react-app/state';
const Counter = () => {
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
return (
<div>
<span>Count: {count}</span>
<button onClick={() => setCount((prev) => (prev ?? 0) + 1)}>Increment</button>
</div>
);
};Cross-Component Synchronization
Multiple components can share the same state by using the same key. Changes in one component automatically update all others:
import { useAppState } from '@equinor/fusion-framework-react-app/state';
const Incrementer = () => {
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
return (
<button onClick={() => setCount((prev) => (prev || 0) + 1)}>
Increment
</button>
);
};
const Display = () => {
const [count] = useAppState('counter', { defaultValue: 0 });
return <span>Current count: {count}</span>;
};
// Usage in your app
const App = () => (
<div>
<Incrementer />
<Display />
</div>
);Advanced Usage
Complex Objects with TypeScript:
interface UserPreferences {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
const SettingsPanel = () => {
const [settings, setSettings] = useAppState<UserPreferences>('user-settings', {
defaultValue: { theme: 'light', language: 'en', notifications: true }
});
const toggleTheme = () => {
setSettings(prev => ({
...prev!,
theme: prev!.theme === 'light' ? 'dark' : 'light'
}));
};
return <button onClick={toggleTheme}>Theme: {settings?.theme}</button>;
};Clearing State:
// Remove from storage completely
const clearSettings = () => setSettings(undefined);Best Practices
Avoid Stale Closures
Warning
When updating state based on the current value, always use the updater function to prevent stale closure issues in concurrent updates.
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
// β Bad: Can use stale value in rapid updates
const increment = () => setCount(count + 1);
// β
Good: Always gets the latest value
const increment = () => setCount(prev => (prev || 0) + 1);State Key Organization
Use hierarchical naming for better organization:
// β
Good - hierarchical, descriptive
'user.profile.personal'
'user.preferences.theme'
'app.settings.notifications'
'feature.dashboard.filters'
// β Avoid - flat, unclear
'userdata'
'settings'
'stuff'Use strong typing
// β
Good - strong typing
interface UserProfile {
id: string;
name: string;
email: string;
}
const [user, setUser] = useAppState<UserProfile>('user.profile');
// β Avoid - weak typing
const [user, setUser] = useAppState('user.profile');[!TIP] Validate Complex Schemas
Use a library likezodoryupto validate complex state schemas before using them.const userSchema = z.object({ id: z.string().uuid(), name: z.string().min(2).max(100), email: z.string().email(), }); type UserProfile = z.infer<typeof userSchema>; // β Good - strong typing with validation const useMyUser = () => { const [value, setValue] = useAppState<UserProfile>('user.profile'); const setUser = useCallback((user: UserProfile) => { if (userSchema.safeParse(user).success) { setValue(user); return true; } else { console.warn('Provided user is invalid'); return false; } }, [setValue]); if(!userSchema.safeParse(value).success) { console.warn('Current user state is invalid'); return null; } return value; };
Feature Flag
Feature flags
Note: Requires
@equinor/fusion-framework-module-feature-flag.
import { enableFeatureFlag } from '@equinor/fusion-framework-react-app/feature-flag';
export const configure: AppModuleInitiator = (configurator) => {
enableFeatureFlag(configurator, [
{ key: 'dark-mode', title: 'Dark mode' },
{ key: 'beta-ui', title: 'Beta UI', allowUrl: true },
]);
};import { useFeature } from '@equinor/fusion-framework-react-app/feature-flag';
const Toggle = () => {
const { feature, toggleFeature } = useFeature('dark-mode');
return <Switch checked={feature?.enabled} onChange={() => toggleFeature()} />;
};Bookmarks
import { useCurrentBookmark } from '@equinor/fusion-framework-react-app/bookmark';
const BookmarkView = () => {
const { currentBookmark } = useCurrentBookmark();
return <pre>{JSON.stringify(currentBookmark, null, 2)}</pre>;
};Settings
import { useAppSetting } from '@equinor/fusion-framework-react-app/settings';
const ThemePicker = () => {
const [theme, setTheme] = useAppSetting('theme', 'light');
return <button onClick={() => setTheme('dark')}>Go dark</button>;
};Environment variables
import { useAppEnvironmentVariables } from '@equinor/fusion-framework-react-app';
const EnvInfo = () => {
const env = useAppEnvironmentVariables();
if (!env.complete) return <p>Loadingβ¦</p>;
return <pre>{JSON.stringify(env.value, null, 2)}</pre>;
};API Reference
Core exports (main entry-point)
| Export | Description |
|---|---|
renderApp | Creates a mount function from a component and optional config callback |
makeComponent | Lazily initialises modules and wraps a component in framework providers |
createComponent | (deprecated) Factory returning a ComponentRenderer |
createLegacyApp | (deprecated) Wrapper for legacy Fusion CLI apps |
renderComponent | Lower-level helper: mounts a ComponentRenderer via createRoot |
useAppModule(key) | Returns a single module instance by key |
useAppModules() | Returns all initialised application modules |
useAppEnvironmentVariables() | Observable state of the app's environment config |
Sub-path entry-points
| Path | Key exports |
|---|---|
/msal | useCurrentAccount, useAccessToken, useToken |
/http | useHttpClient (re-export) |
/http/selectors | Response selector utilities |
/context | useCurrentContext, useContextProvider, useFrameworkCurrentContext |
/navigation | useRouter, useNavigationModule |
/feature-flag | enableFeatureFlag, useFeature |
/bookmark | enableBookmark, useCurrentBookmark, useBookmark |
/settings | useAppSetting, useAppSettings |
/analytics | useTrackFeature |
/help-center | useHelpCenter |
/apploader | Apploader, useApploader |
/framework | useFramework, useCurrentUser, useFrameworkHttpClient |
/widget | Widget entry-point |
Configuration
Application configuration is done via a callback passed to renderApp (or makeComponent). The callback receives an IAppConfigurator with builders for each module:
const configure: AppModuleInitiator = (configurator) => {
// HTTP clients
configurator.http.configureClient('my-api', { baseUri: '...' });
// Feature flags
enableFeatureFlag(configurator, [{ key: 'beta', title: 'Beta' }]);
// Navigation, context, etc. β see module docs
};The MSAL / auth module is configured by the host portal and hoisted to apps automatically. Do not configure it in application code.