BlogZustand in Liferay Client Extensions: Performance and Simplified Global State
Zustand in Liferay Client Extensions: Performance and Simplified Global State
General

Zustand in Liferay Client Extensions: Performance and Simplified Global State

Miguel Ángel Júlvez

Miguel Ángel Júlvez

Equipo técnico

April 06, 2026
8 min lectura
Compartir:
Modern frontend development architecture with React

When developing React Client Extensions in Liferay, we face unique challenges: multiple decoupled microfrontends, complex shared state, and above all, performance issues derived from unnecessary re-renders. Traditional solutions like Context API or Redux often introduce excessive complexity or overhead that affects user experience.

At JULDITEC we have adopted Zustand as our reference solution for state management in modern architectures with Liferay. In this article we explain why this minimalist library drastically improves the performance of your applications and how to integrate it correctly in your Client Extensions.

What is Zustand and why does it stand out?

Zustand is a state management library for React based on three fundamental principles: simplicity, performance, and absence of boilerplate. Unlike Redux, it doesn't require providers, reducers, or complex actions. Unlike Context API, it doesn't cause cascading re-renders of the entire component hierarchy.

Zustand's philosophy is straightforward: you create a store with a simple function, consume the state through hooks, and the component only re-renders when the specific portions of state it's using change.

Quick comparison: Zustand vs Redux vs Context API

  • Redux: Powerful but verbose. Requires actions, reducers, middleware. Ideal for large applications with complex state logic, but excessive for many cases.
  • Context API: Native to React but with performance issues. Every context change re-renders all consumers, even if they don't use the modified portion.
  • Zustand: Lightweight (less than 1KB), minimalist API, selective subscription by default. The perfect balance for Client Extensions.
Clean and optimized code on screen

The specific problem in Liferay Client Extensions

Client Extensions in Liferay are decoupled frontend applications that integrate into the portal through iframes or as Custom Elements. This architecture presents unique challenges:

  • Decoupled nature: Each Client Extension is an independent application with its own lifecycle.
  • Multiple microfrontends: On the same page, several Client Extensions can coexist that need to share state.
  • Complex shared state: User information, configuration, business data that must be synchronized.
  • Performance issues: Unnecessary re-renders when using Context API or extensive props drilling.

Imagine a dashboard with 5 independent widgets (each one a Client Extension). If you use Context API and update the user state, all widgets re-render even though only one needs that information. With Zustand, only the widget subscribed to that specific data updates.

How to integrate Zustand in a Client Extension

Integrating Zustand into a Client Extension project is surprisingly simple. First, we install the dependency:

npm install zustand

Basic store structure

We create a store specific to our business domain. For example, a user store:

// stores/userStore.js import { create } from 'zustand'; const useUserStore = create((set) => ({ user: null, preferences: {}, setUser: (user) => set({ user }), updatePreferences: (prefs) => set((state) => ({ preferences: { ...state.preferences, ...prefs } })), clearUser: () => set({ user: null, preferences: {} }) })); export default useUserStore;

Usage in components

State consumption is extremely simple and performant:

// components/UserProfile.jsx import useUserStore from '../stores/userStore'; function UserProfile() { // Selective subscription: only re-renders if 'user' changes const user = useUserStore((state) => state.user); if (!user) return

Not authenticated

{user.name}

Integration with Liferay globals

You can initialize your store with global Liferay data available in window.Liferay:

// stores/liferayStore.js import { create } from 'zustand'; const useLiferayStore = create((set) => ({ themeDisplay: window.Liferay?.ThemeDisplay || {}, currentUser: window.Liferay?.ThemeDisplay?.getUserName() || 'Guest', languageId: window.Liferay?.ThemeDisplay?.getLanguageId() || 'es_ES' })); export default useLiferayStore; Dashboard with performance charts

Why it drastically improves performance

The key to Zustand's performance lies in its selector-based selective subscription. When you do:

const user = useUserStore((state) => state.user);

The component only re-renders when state.user changes, not when any other store property changes. This contrasts radically with Context API, where any context change causes re-renders of all consumers.

Practical comparison: before vs after

With Context API: 15 re-renders in a 5-widget dashboard when updating a single data point. With Zustand: 1 re-render, only of the widget consuming that specific data.

Additionally, Zustand has lower overhead than Redux: you don't need complex middleware, there's no action serialization, and the final bundle is significantly smaller (less than 1KB vs ~10KB for Redux).

Real use cases in Client Extensions

1. Complex forms

In multi-step forms with validations, Zustand allows managing form state without prop drilling and with surgical re-renders only on modified fields.

2. Dashboards with multiple widgets

Each widget (independent Client Extension) can subscribe only to the portion of state it needs: metrics, filters, user configuration, etc.

3. Integration with external APIs

Centralize the state of asynchronous calls, loading states, and errors in a dedicated store:

const useApiStore = create((set) => ({ data: null, loading: false, error: null, fetchData: async (endpoint) => { set({ loading: true, error: null }); try { const response = await fetch(endpoint); const data = await response.json(); set({ data, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } } }));

4. Independent Client Extensions sharing state

You can expose stores as shared modules between different Client Extensions, allowing state synchronization without complex communication between iframes.

Development team working on modern architecture

Best practices when using Zustand in Liferay

1. Separate stores by domain

Don't create a monolithic store. Divide by responsibilities: userStore, cartStore, notificationsStore, etc. This improves maintainability and performance.

2. Avoid giant stores

If a store grows too large, consider splitting it or using slices (store composition pattern).

3. Using middleware

Zustand supports middleware for advanced functionality:

  • persist: Automatically saves state to localStorage
  • devtools: Integration with Redux DevTools for debugging
  • immer: Simplified immutable updates

import { create } from 'zustand'; import { persist, devtools } from 'zustand/middleware'; const useStore = create( devtools( persist( (set) => ({ user: null, setUser: (user) => set({ user }) }), { name: 'user-storage' } ) ) );

4. Testing state

Zustand facilitates testing by being pure functions. You can test store actions without needing to mount components:

import useUserStore from './userStore'; test('setUser updates the user', () => { const { setUser, user } = useUserStore.getState(); setUser({ name: 'Juan' }); expect(useUserStore.getState().user.name).toBe('Juan'); }); Performance metrics in web applications

Conclusion: Zustand as the optimal solution in modern architectures

At JULDITEC we have verified that Zustand is the optimal solution for state management in Liferay Client Extensions. Its combination of simplicity, performance, and absence of boilerplate makes it the perfect tool for decoupled architectures and microfrontends.

Performance improvements are measurable and significant: reduction of unnecessary re-renders, lighter bundles, and more maintainable code. If you're developing modern applications on Liferay DXP, Zustand should be in your technology stack.

Our approach at JULDITEC always prioritizes performance and simplicity. Zustand fits perfectly into this philosophy, allowing us to deliver robust enterprise solutions without sacrificing development experience or end-user performance.

Want to implement high-performance Client Extensions in your Liferay project? Contact us and discover how we can help you build modern and scalable architectures.

Etiquetas:liferayreactzustandclient extensionsgestión de estadorendimientomicrofrontends
Siguiente

Devcontainers: The Foundation of Modern AI-Powered Development

¿Listo para llevar tu proyecto al siguiente nivel?

En JULDITEC transformamos ideas en soluciones digitales innovadoras. Trabajemos juntos.