Skip to content

Commit fe7e1bc

Browse files
committed
feat(app_state): implement state management with useAppState hook and enhance documentation
1 parent 68383ec commit fe7e1bc

9 files changed

Lines changed: 514 additions & 4 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
"@equinor/fusion-framework-react-app": major
3+
---
4+
5+
🎉 **Introducing State Management for React Applications!**
6+
7+
We're excited to bring you a complete state management solution that makes sharing data between components effortless. Say goodbye to prop drilling and hello to persistent, synchronized state that survives page refreshes!
8+
9+
**What's new?** The powerful `useAppState` hook works just like React's `useState` but with superpowers:
10+
11+
🔄 **Persistent by default** - Your state survives page refreshes using browser storage
12+
🔗 **Automatically synchronized** - Share state between any components in real-time
13+
**Optimistically updated** - Lightning-fast UI with automatic error recovery
14+
🛡️ **Fully type-safe** - Complete TypeScript support with type inference
15+
🎯 **Dead simple API** - If you know `useState`, you already know `useAppState`
16+
17+
**Getting started is easy:**
18+
```typescript
19+
import { enableAppState } from '@equinor/fusion-framework-react-app/state';
20+
21+
export const configure = (configurator) => {
22+
enableAppState(configurator);
23+
};
24+
```
25+
26+
**Then use it anywhere:**
27+
```typescript
28+
import { useAppState } from '@equinor/fusion-framework-react-app/state';
29+
30+
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
31+
```
32+
33+
**Perfect for:**
34+
- User preferences and UI settings
35+
- Form data that enhances user experience
36+
- Filters, sorting, and view states
37+
- Any data that needs to be shared across components
38+
- Caching expensive operations
39+
40+
**Important:** Uses browser storage (localStorage/IndexedDB via PouchDB) - perfect for enhancing user experience but not suitable for critical data that must be guaranteed to persist. Always have fallback strategies for essential information.
41+
42+
**Examples that show the magic:**
43+
44+
```typescript
45+
// Simple counter that persists across refreshes
46+
const Counter = () => {
47+
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
48+
return <button onClick={() => setCount(c => (c || 0) + 1)}>Count: {count}</button>;
49+
};
50+
51+
// Components automatically sync - change in one updates all others!
52+
const Settings = () => {
53+
const [theme, setTheme] = useAppState('theme', { defaultValue: 'light' });
54+
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
55+
Switch to {theme === 'light' ? 'dark' : 'light'} mode
56+
</button>;
57+
};
58+
59+
const Header = () => {
60+
const [theme] = useAppState('theme', { defaultValue: 'light' });
61+
return <header className={`header-${theme}`}>My App</header>;
62+
};
63+
```
64+
65+
This feature includes comprehensive documentation with practical examples, best practices, and TypeScript patterns. Applications need to install `@equinor/fusion-framework-module-state` to unlock these capabilities.
66+
67+
Ready to eliminate prop drilling and embrace persistent state? Check out the updated README for complete setup instructions and advanced usage patterns!

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@equinor/fusion-framework-module-event": "workspace:^",
6363
"@equinor/fusion-framework-module-http": "workspace:^",
6464
"@equinor/fusion-framework-module-msal": "workspace:^",
65+
"fast-deep-equal": "^3.1.3",
6566
"lodash.clonedeep": "^4.5.0"
6667
},
6768
"devDependencies": {

packages/modules/state/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export { StateModuleConfigurator } from './StateModuleConfigurator.js';
66

77
export { enableStateModule } from './enable-state-module.js';
88

9+
export { StateModule, module, module as default } from './StateModule.js';
10+
911
export type {
1012
AllowedValue,
1113
StateItem,

packages/react/app/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,140 @@ const App = () => {
9191
}
9292
```
9393

94+
## State Management
95+
96+
[<img src="https://img.shields.io/github/package-json/v/equinor/fusion-framework?filename=packages%2Fmodules%2Fstate%2Fpackage.json&label=@equinor/fusion-framework-module-state&style=for-the-badge" />](https://github.com/equinor/fusion-framework/tree/main/packages/modules/state)
97+
98+
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.
99+
100+
**Key Benefits:**
101+
- 🔄 **Persistent State**: Survives page refreshes and app restarts
102+
- 🔗 **Cross-Component Sync**: Share state between any components instantly
103+
-**Optimistic Updates**: Responsive UI with automatic rollback on errors
104+
- 🛡️ **Type Safe**: Full TypeScript support with type inference
105+
- 🎯 **Simple API**: Works like `useState` but with persistence
106+
107+
**Use Cases:**
108+
- User preferences and settings
109+
- Form data that should persist
110+
- UI state like filters, sorting, or view modes
111+
- Data that needs to be shared across multiple components
112+
- Cache management for expensive operations
113+
114+
### Installation
115+
116+
First, install the state module package:
117+
118+
```sh
119+
pnpm install @equinor/fusion-framework-module-state
120+
```
121+
122+
### Setup
123+
124+
Enable the state module in your app configuration. This initializes the persistent storage and makes `useAppState` available throughout your application:
125+
126+
```typescript
127+
import { enableAppState } from '@equinor/fusion-framework-react-app/state';
128+
export const configure: ModuleInitiator = (appConfigurator) => {
129+
enableAppState(appConfigurator);
130+
};
131+
```
132+
133+
### Basic Usage
134+
135+
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:
136+
137+
```typescript
138+
import { useAppState } from '@equinor/fusion-framework-react-app/state';
139+
140+
const Counter = () => {
141+
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
142+
return (
143+
<div>
144+
<span>Count: {count}</span>
145+
<button onClick={() => setCount((prev) => (prev ?? 0) + 1)}>Increment</button>
146+
</div>
147+
);
148+
};
149+
```
150+
151+
### Cross-Component Synchronization
152+
153+
Multiple components can share the same state by using the same key. Changes in one component automatically update all others:
154+
155+
```typescript
156+
import { useAppState } from '@equinor/fusion-framework-react-app/state';
157+
158+
const Incrementer = () => {
159+
const [count, setCount] = useAppState('counter', { defaultValue: 0 });
160+
return (
161+
<button onClick={() => setCount((prev) => (prev || 0) + 1)}>
162+
Increment
163+
</button>
164+
);
165+
};
166+
167+
const Display = () => {
168+
const [count] = useAppState('counter', { defaultValue: 0 });
169+
return <span>Current count: {count}</span>;
170+
};
171+
172+
// Usage in your app
173+
const App = () => (
174+
<div>
175+
<Incrementer />
176+
<Display />
177+
</div>
178+
);
179+
```
180+
181+
### Best Practices
182+
183+
> [!WARNING]
184+
> **Avoid Stale Closures**: When updating state based on the current value, always use the updater function to prevent stale closure issues in concurrent updates.
185+
>
186+
> ```typescript
187+
> const [count, setCount] = useAppState('counter', { defaultValue: 0 });
188+
>
189+
> // ❌ Bad: Can use stale value in rapid updates
190+
> const increment = () => setCount(count + 1);
191+
>
192+
> // ✅ Good: Always gets the latest value
193+
> const increment = () => setCount(prev => (prev || 0) + 1);
194+
> ```
195+
196+
### Advanced Usage
197+
198+
**Complex Objects with TypeScript:**
199+
```typescript
200+
interface UserPreferences {
201+
theme: 'light' | 'dark';
202+
language: string;
203+
notifications: boolean;
204+
}
205+
206+
const SettingsPanel = () => {
207+
const [settings, setSettings] = useAppState<UserPreferences>('user-settings', {
208+
defaultValue: { theme: 'light', language: 'en', notifications: true }
209+
});
210+
211+
const toggleTheme = () => {
212+
setSettings(prev => ({
213+
...prev!,
214+
theme: prev!.theme === 'light' ? 'dark' : 'light'
215+
}));
216+
};
217+
218+
return <button onClick={toggleTheme}>Theme: {settings?.theme}</button>;
219+
};
220+
```
221+
222+
**Clearing State:**
223+
```typescript
224+
// Remove from storage completely
225+
const clearSettings = () => setSettings(undefined);
226+
```
227+
94228
## Feature Flag
95229

96230
> [!IMPORTANT]

packages/react/app/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@
123123
"@equinor/fusion-framework-module-navigation": "workspace:^",
124124
"@equinor/fusion-framework-react": "workspace:^",
125125
"@equinor/fusion-framework-react-module": "workspace:^",
126-
"@equinor/fusion-framework-react-module-http": "workspace:^",
127-
"fast-deep-equal": "^3.1.3"
126+
"@equinor/fusion-framework-react-module-http": "workspace:^"
128127
},
129128
"devDependencies": {
130129
"@equinor/fusion-framework-module-ag-grid": "workspace:^",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export {
2+
AllowedValue,
3+
IStateProvider,
4+
StateItem,
5+
} from '@equinor/fusion-framework-module-state';
6+
7+
export { enableState as enableAppState } from '@equinor/fusion-framework-app/enable-state';
8+
9+
export { useAppState } from './useAppState';

0 commit comments

Comments
 (0)