State Management in UI5: Models, Patterns, and Best Practices
State management is the backbone of any complex UI application. In SAP UI5, managing application state effectively determines whether your Fiori app is maintainable and scalable, or becomes a tangled mess of scattered data and unpredictable behavior.
This comprehensive guide explores state management strategies in UI5, from built-in models to advanced patterns inspired by modern frameworks like Redux.
Understanding State in UI5 Applications
What is Application State?
State includes:
- UI State – Which tabs are open, what's selected, loading indicators
- Data State – Business data from backend services
- Session State – User preferences, authentication tokens
- Transient State – Form inputs, validation errors
The Challenge
// Without proper state management - scattered data
var oController = {
_selectedCustomer: null,
_isLoading: false,
_filterValue: "",
_sortOrder: "asc",
// Data everywhere, hard to track changes
};Approach 1: JSON Model (Simple State)
When to Use
- Client-side data management
- View-specific state
- Form data before submission
- UI control states
Basic Implementation
// In Component.js
import JSONModel from "sap/ui/model/json/JSONModel";
onInit: function() {
// Create application state model
var oAppState = new JSONModel({
ui: {
busy: false,
selectedTab: "customers",
sidebarCollapsed: false
},
filters: {
searchQuery: "",
dateRange: { from: null, to: null },
status: []
},
data: {
customers: [],
selectedCustomer: null,
orders: []
},
user: {
name: "",
role: "",
preferences: {}
}
});
this.setModel(oAppState, "appState");
}Accessing State in Controller
onCustomerSelect: function(oEvent) {
var oModel = this.getModel("appState");
var oContext = oEvent.getParameter("listItem").getBindingContext("appState");
var oCustomer = oContext.getObject();
// Update state
oModel.setProperty("/data/selectedCustomer", oCustomer);
oModel.setProperty("/ui/selectedTab", "details");
},
getBusyState: function() {
var oModel = this.getModel("appState");
return oModel.getProperty("/ui/busy");
},
setBusyState: function(bBusy) {
var oModel = this.getModel("appState");
oModel.setProperty("/ui/busy", bBusy);
}Two-Way Binding in View
<Input
value="{appState>/filters/searchQuery}"
liveChange=".onSearchChange"/>
<List
items="{appState>/data/customers}"
mode="SingleSelectMaster">
<StandardListItem
title="{appState>name}"
description="{appState>email}"/>
</List>
<Panel visible="{appState>/data/selectedCustomer}">
<Title text="{appState>/data/selectedCustomer/name}"/>
<Text text="{appState>/data/selectedCustomer/email}"/>
</Panel>Approach 2: OData Model (Backend State)
When to Use
- SAP backend data
- CRUD operations
- Real-time data synchronization
- Complex queries and filters
V2 Model Configuration
// In manifest.json
"models": {
"": {
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultBindingMode": "TwoWay",
"defaultCountMode": "Inline",
"useBatch": true,
"refreshAfterChange": false
},
"dataSource": "mainService",
"preload": true
}
}
// In Component.js
onInit: function() {
var oModel = this.getModel();
// Set size limit for client-side operations
oModel.setSizeLimit(1000);
// Handle metadata loaded
oModel.metadataLoaded().then(function() {
console.log("Metadata loaded successfully");
});
}Reading Data
// Method 1: List Binding
<Table items="{/Customers}">
<columns>
<Column><Text text="Name"/></Column>
<Column><Text text="Email"/></Column>
</columns>
<items>
<ColumnListItem>
<cells>
<Text text="{Name}"/>
<Text text="{Email}"/>
</cells>
</ColumnListItem>
</items>
</Table>
// Method 2: Programmatic Read
onLoadCustomers: function() {
var oModel = this.getModel();
var that = this;
this.setBusy(true);
oModel.read("/Customers", {
filters: [
new Filter("Status", FilterOperator.EQ, "Active")
],
urlParameters: {
"$expand": "Orders",
"$top": 50
},
success: function(oData) {
var aCustomers = oData.results;
// Process data
that.getModel("view").setProperty("/customers", aCustomers);
that.setBusy(false);
},
error: function(oError) {
MessageBox.error("Failed to load customers");
that.setBusy(false);
}
});
}Creating and Updating
onCreate: function() {
var oModel = this.getModel();
var oNewCustomer = {
Name: "John Doe",
Email: "john@example.com",
Status: "Active"
};
oModel.create("/Customers", oNewCustomer, {
success: function(oData) {
MessageToast.show("Customer created");
},
error: function(oError) {
MessageBox.error("Creation failed");
}
});
},
onUpdate: function(sPath, oUpdatedData) {
var oModel = this.getModel();
oModel.update(sPath, oUpdatedData, {
success: function() {
MessageToast.show("Updated successfully");
},
error: function() {
MessageBox.error("Update failed");
}
});
}Batch Operations
onBatchUpdate: function() {
var oModel = this.getModel();
// Defer changes
oModel.setDeferredGroups(["batchGroup"]);
// Queue multiple changes
oModel.update("/Customers('1')", { Status: "Inactive" }, {
groupId: "batchGroup"
});
oModel.update("/Customers('2')", { Status: "Inactive" }, {
groupId: "batchGroup"
});
oModel.create("/Orders", { CustomerId: "1", Amount: 500 }, {
groupId: "batchGroup"
});
// Submit all at once
oModel.submitChanges({
groupId: "batchGroup",
success: function(oData) {
MessageToast.show("Batch update successful");
},
error: function(oError) {
MessageBox.error("Batch update failed");
}
});
}Approach 3: Redux-like State Management
Why Redux Pattern in UI5?
- Predictable state – Single source of truth
- Testable – Pure functions, easy to test
- Time-travel debugging – Track all state changes
- Large applications – Scales better than scattered models
Store Implementation
// store/Store.js
sap.ui.define([
"sap/ui/base/Object"
], function(BaseObject) {
"use strict";
return BaseObject.extend("com.mycompany.store.Store", {
constructor: function() {
this._state = {};
this._listeners = [];
this._reducers = {};
},
registerReducer: function(sKey, fnReducer) {
this._reducers[sKey] = fnReducer;
},
getState: function() {
return Object.assign({}, this._state);
},
dispatch: function(oAction) {
console.log("Action:", oAction.type, oAction.payload);
var oNewState = {};
// Run all reducers
for (var sKey in this._reducers) {
oNewState[sKey] = this._reducers[sKey](
this._state[sKey],
oAction
);
}
this._state = oNewState;
// Notify listeners
this._listeners.forEach(function(fnListener) {
fnListener(this._state);
}.bind(this));
return oAction;
},
subscribe: function(fnListener) {
this._listeners.push(fnListener);
// Return unsubscribe function
return function() {
var iIndex = this._listeners.indexOf(fnListener);
if (iIndex > -1) {
this._listeners.splice(iIndex, 1);
}
}.bind(this);
}
});
});Reducers
// reducers/customerReducer.js
sap.ui.define([], function() {
"use strict";
var initialState = {
customers: [],
selectedCustomer: null,
loading: false,
error: null
};
return function customerReducer(state, action) {
state = state || initialState;
switch (action.type) {
case "CUSTOMERS_LOAD_START":
return Object.assign({}, state, {
loading: true,
error: null
});
case "CUSTOMERS_LOAD_SUCCESS":
return Object.assign({}, state, {
customers: action.payload,
loading: false
});
case "CUSTOMERS_LOAD_ERROR":
return Object.assign({}, state, {
loading: false,
error: action.payload
});
case "CUSTOMER_SELECT":
return Object.assign({}, state, {
selectedCustomer: action.payload
});
case "CUSTOMER_DESELECT":
return Object.assign({}, state, {
selectedCustomer: null
});
default:
return state;
}
};
});Actions
// actions/customerActions.js
sap.ui.define([], function() {
"use strict";
return {
loadCustomers: function(oStore, oModel) {
oStore.dispatch({ type: "CUSTOMERS_LOAD_START" });
oModel.read("/Customers", {
success: function(oData) {
oStore.dispatch({
type: "CUSTOMERS_LOAD_SUCCESS",
payload: oData.results
});
},
error: function(oError) {
oStore.dispatch({
type: "CUSTOMERS_LOAD_ERROR",
payload: oError.message
});
}
});
},
selectCustomer: function(oStore, oCustomer) {
oStore.dispatch({
type: "CUSTOMER_SELECT",
payload: oCustomer
});
},
deselectCustomer: function(oStore) {
oStore.dispatch({ type: "CUSTOMER_DESELECT" });
}
};
});Component Integration
// Component.js
import Store from "./store/Store";
import customerReducer from "./reducers/customerReducer";
import orderReducer from "./reducers/orderReducer";
import JSONModel from "sap/ui/model/json/JSONModel";
onInit: function() {
// Initialize store
this._store = new Store();
// Register reducers
this._store.registerReducer("customers", customerReducer);
this._store.registerReducer("orders", orderReducer);
// Create view model
var oViewModel = new JSONModel(this._store.getState());
this.setModel(oViewModel, "view");
// Subscribe to store changes
this._store.subscribe(function(oState) {
oViewModel.setData(oState);
});
},
getStore: function() {
return this._store;
}Controller Usage
import customerActions from "../actions/customerActions";
onInit: function() {
this._store = this.getOwnerComponent().getStore();
},
onLoadPress: function() {
var oModel = this.getModel();
customerActions.loadCustomers(this._store, oModel);
},
onCustomerSelect: function(oEvent) {
var oContext = oEvent.getParameter("listItem").getBindingContext("view");
var oCustomer = oContext.getObject();
customerActions.selectCustomer(this._store, oCustomer);
}Advanced Patterns
Middleware (Side Effects)
// middleware/loggerMiddleware.js
function loggerMiddleware(oStore) {
return function(next) {
return function(oAction) {
console.group(oAction.type);
console.log("Previous State:", oStore.getState());
console.log("Action:", oAction);
var result = next(oAction);
console.log("Next State:", oStore.getState());
console.groupEnd();
return result;
};
};
}
// Apply middleware
oStore.applyMiddleware(loggerMiddleware, thunkMiddleware);Async Actions with Thunk
// Thunk middleware
function thunkMiddleware(oStore) {
return function(next) {
return function(oAction) {
if (typeof oAction === "function") {
return oAction(oStore.dispatch, oStore.getState);
}
return next(oAction);
};
};
}
// Async action creator
loadCustomersAsync: function(oModel) {
return function(dispatch, getState) {
dispatch({ type: "CUSTOMERS_LOAD_START" });
return new Promise(function(resolve, reject) {
oModel.read("/Customers", {
success: function(oData) {
dispatch({
type: "CUSTOMERS_LOAD_SUCCESS",
payload: oData.results
});
resolve(oData);
},
error: function(oError) {
dispatch({
type: "CUSTOMERS_LOAD_ERROR",
payload: oError.message
});
reject(oError);
}
});
});
};
}
// Usage
oStore.dispatch(customerActions.loadCustomersAsync(oModel))
.then(function() {
MessageToast.show("Loaded successfully");
});Selectors (Derived State)
// selectors/customerSelectors.js
sap.ui.define([], function() {
"use strict";
return {
getCustomers: function(oState) {
return oState.customers.customers || [];
},
getSelectedCustomer: function(oState) {
return oState.customers.selectedCustomer;
},
getActiveCustomers: function(oState) {
return this.getCustomers(oState).filter(function(c) {
return c.Status === "Active";
});
},
getCustomerById: function(oState, sId) {
return this.getCustomers(oState).find(function(c) {
return c.Id === sId;
});
},
isLoading: function(oState) {
return oState.customers.loading;
},
getError: function(oState) {
return oState.customers.error;
}
};
});
// Usage in controller
import selectors from "../selectors/customerSelectors";
getActiveCustomers: function() {
var oState = this._store.getState();
return selectors.getActiveCustomers(oState);
}State Persistence
LocalStorage Integration
// Save state to localStorage
saveState: function() {
var oState = this._store.getState();
var sState = JSON.stringify(oState);
localStorage.setItem("appState", sState);
},
// Load state from localStorage
loadState: function() {
var sState = localStorage.getItem("appState");
if (sState) {
try {
return JSON.parse(sState);
} catch (e) {
console.error("Failed to load state", e);
return undefined;
}
}
return undefined;
},
// Initialize with persisted state
onInit: function() {
var oInitialState = this.loadState();
this._store = new Store(oInitialState);
// Save on every state change
this._store.subscribe(function() {
this.saveState();
}.bind(this));
}Comparison Table
| Aspect | JSON Model | OData Model | Redux Pattern |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Setup Time | Minutes | Hours | Days |
| Best For | Small apps, UI state | SAP backends, CRUD | Large apps, complex state |
| Testability | Medium | Low | High |
| Debugging | Basic | Medium | Excellent |
| Performance | Fast | Backend-dependent | Fast |
| Learning Curve | Easy | Moderate | Steep |
Best Practices
✅ DO
- Use JSON Model for UI-specific state
- Use OData Model for backend data
- Implement Redux pattern for complex apps (10+ views)
- Keep state immutable in Redux reducers
- Use selectors for derived state
- Centralize state updates through actions
- Persist critical state to localStorage
- Document state structure clearly
❌ AVOID
- Mixing state management approaches randomly
- Direct state mutation in Redux
- Storing everything in global state
- Over-engineering simple apps with Redux
- Ignoring OData model batch operations
- Not handling loading and error states
Decision Guide
Use JSON Model When:
- Building simple apps (1-5 views)
- Managing UI state only
- Rapid prototyping
- No backend integration needed
Use OData Model When:
- Working with SAP backends
- Need automatic CRUD operations
- Two-way binding to backend data
- Standard Fiori applications
Use Redux Pattern When:
- Building large applications (10+ views)
- Complex state interactions
- Need time-travel debugging
- Multiple developers on same codebase
- Strict testing requirements
Conclusion
State management in UI5 isn't one-size-fits-all. The right approach depends on your application'scomplexity, team size, and requirements.
Key takeaways:
- Start simple with JSON Model
- Use OData Model for SAP integration
- Graduate to Redux for complex applications
- Always consider testability and maintainability
Master state management, and you master UI5 application architecture.
