---
name: portlet-react
description: Reference architecture for legacy Liferay 7.4 React Portlets — the source code being migrated. Use when analyzing portlet entry points, understanding Liferay globals, mapping common bundle dependencies, or identifying API and routing patterns in the source module.
---

# Legacy Liferay React Portlet Architecture

## Entry Point (Universal Pattern)

Every portlet has **exactly this pattern** as its entry point (`index.js` or `index.es.js`):

```js
import React from "react";
import ReactDOM from "react-dom";
import AppComponent from "./AppComponent";

export default function main(params) {
  ReactDOM.render(
    <AppComponent
      portletNamespace={params.portletNamespace}
      contextPath={params.contextPath}
      portletElementId={params.portletElementId}
      configuration={params.configuration}
    />,
    document.getElementById(params.portletElementId),
  );
}
```

### The 4 Standard Parameters

| Param | Type | Purpose |
|-------|------|---------|
| `portletNamespace` | string | Unique portlet prefix to avoid DOM ID conflicts |
| `contextPath` | string | Module base path on the server |
| `portletElementId` | string | ID of the `<div>` where React mounts |
| `configuration` | object | Portlet config from Liferay (instanceSettings, typeSettings) |

**Migration note:** These parameters DO NOT exist in Client Extensions. Remove all references to:
- `portletNamespace`
- `contextPath`
- `portletElementId`
- `configuration` (use CE config or environment variables instead)

### Entry Point Variations

**With routing (HashRouter):**
```js
import { HashRouter } from "react-router-dom";
export default function main(params) {
  ReactDOM.render(
    <HashRouter>
      <AppComponent {...params} />
    </HashRouter>,
    document.getElementById(params.portletElementId)
  );
}
```

**With providers (urql, antd, context):**
```js
import { ConfigProvider } from "antd";
import ptBR from "antd/lib/locale/pt_BR"; // NOTE: antd v4/v5 path — becomes antd/locale/pt_BR in v6
import { Provider } from "urql";
import { client } from "sesc-portal-queries";

export default function main(params) {
  ReactDOM.render(
    <ConfigProvider locale={ptBR}>
      <Provider value={client}>
        <ChartProvider>
          <AppComponent {...params} />
        </ChartProvider>
      </Provider>
    </ConfigProvider>,
    document.getElementById(params.portletElementId)
  );
}
```

**Real example from workspace (sesc-portal-dashboard):**
```js
import { ConfigProvider } from "antd";
import ptBR from "antd/lib/locale/pt_BR";
import { Provider } from "urql";
import { client } from "sesc-portal-queries";
import { ChartProvider } from "./context/ChartContext";
import AppComponent from "./AppComponent";

export default function main(params) {
  ReactDOM.render(
    <ConfigProvider locale={ptBR}>
      <Provider value={client}>
        <ChartProvider>
          <AppComponent {...params} />
        </ChartProvider>
      </Provider>
    </ConfigProvider>,
    document.getElementById(params.portletElementId)
  );
}
```

---

## File Structure

```
src/
├── index.js                    # Entry point (main(params))
├── AppComponent.js             # Root component
├── components/                 # UI components
│   ├── forms/
│   └── shared/
├── pages/                      # Pages (portlets with routes)
├── services/                   # API calls
│   ├── api.js                  # Generic apiRequest wrapper
│   └── entityService.js        # Per-entity CRUD
├── hooks/                      # Custom hooks
├── context/                    # React Context providers
├── constants/
│   └── apiRoutes.js            # Endpoint URLs
├── utils/                      # Pure helpers
└── css/
    └── styles.scss
```

### Arquivos NÃO relevantes para migração

Alguns arquivos presentes em portlets são específicos do tooling Liferay 7.4 e **não devem ser migrados**:

- `.npmbundlerrc` — substituído por `package.json` com `workspace:*` deps
- `jest.config.*` — CE usa Vitest (configurado em `@repo/config`)
- `babel.config.*` — CE usa Vite (não precisa Babel)
- `webpack.config.*` — CE usa Vite (não precisa Webpack)
- `tsconfig.json` — CE tem configuração própria via `@repo/config`
- Arquivos de teste (`*.test.*`, `*.spec.*`, `__tests__/`, `__mocks__/`) — precisam ser reescritos para Vitest
- `.eslintrc.*` — CE usa ESLint flat config de `@repo/config`

Esses arquivos são **automaticamente ignorados** pelo migrador.

---

## Dependency System (.npmbundlerrc)

```json
{
  "exclude": { "*": true },
  "config": {
    "imports": {
      "": {
        "sesc-portal-common-js-bundle": "*",
        "sesc-portal-queries": "*"
      },
      "sesc-portal-common-js-bundle": {
        "react-router-dom": "*",
        "yup": "*",
        "antd": "5.24.9",
        "dayjs": "*"
      },
      "sesc-portal-queries": {
        "graphql": "*",
        "urql": "*"
      }
    }
  }
}
```

**Important:** Dependencies listed here are **loaded at runtime via `Liferay.Loader`**, not bundled. Any `import X from 'antd'` in the portlet resolves to the version provided by `sesc-portal-common-js-bundle`.

---

## API Call Patterns

### Pattern 1: `Liferay.Util.fetch` (global — 308 occurrences, most common)
```js
const response = await Liferay.Util.fetch(url, {
  method: "GET",
  headers: { "Content-Type": "application/json" },
});
if (response.ok) {
  const data = await response.json();
}
```

### Pattern 2: `import { fetch } from 'frontend-js-web'` (service layer)
```js
import { fetch } from "frontend-js-web";

export async function apiRequest(url, options = {}) {
  const res = await fetch(url, options);
  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw { status: res.status, ...errorData };
  }
  if (res.status === 204 || res.status === 205) return null;
  const text = await res.text();
  return text ? JSON.parse(text) : null;
}
```

### Pattern 3: urql GraphQL (via `sesc-portal-queries`)
```js
import { client } from "sesc-portal-queries";
import { useQuery, useMutation } from "urql";
import { Provider } from "urql";

// In entry point: wrap with <Provider value={client}>
// In components: use useQuery / useMutation from urql
```

### Endpoint URL Pattern
```js
const BASE_URL = window.location.origin + '/o/sesc-portal-module-headless/v1.0';
export const items = `${BASE_URL}/items`;
export const itemById = (id) => `${items}/${id}`;
```

---

## Liferay Globals

Used directly as globals — no imports required. **No portlet calls `Liferay.Language.get()`** — all strings are hardcoded in Brazilian Portuguese.

### `Liferay.ThemeDisplay` (93+ occurrences)
```js
Liferay.ThemeDisplay.getUserId()          // → string
Liferay.ThemeDisplay.getScopeGroupId()    // → number
Liferay.ThemeDisplay.getPortalURL()       // → string (e.g. "https://portal.sesc.df.gov.br")
Liferay.ThemeDisplay.getPathThemeImages() // → string (base path for theme images)
Liferay.ThemeDisplay.isSignedIn()         // → boolean
Liferay.ThemeDisplay.getCompanyId()       // → string
Liferay.ThemeDisplay.getLanguageId()      // → string (e.g. "pt_BR")
```

### `Liferay.authToken` (122 occurrences)
```js
// CSRF token — used in mutation headers
headers: { "x-csrf-token": Liferay.authToken }
```

### `Liferay.Util.openToast` (96 occurrences)
```js
Liferay.Util.openToast({
  message: "Operation completed successfully.",
  title: "Success",
  type: "success", // "success" | "danger" | "info"
});
```

### `Liferay.on` / `Liferay.detach`
```js
Liferay.on("allPortletsReady", () => {
  // runs when all page portlets are ready
});
```

### `Liferay.Loader.require`
```js
Liferay.Loader.require("sesc-portal-common-js-bundle@1.0.0/index", (module) => {
  // loads bundle at runtime
});
```

---

## sesc-portal-common-js-bundle

Shared bundle imported by virtually all portlets (534+ imports).

```js
import { X } from "sesc-portal-common-js-bundle";
```

### Form Components
| Export | Description |
|--------|-------------|
| `FormikInput` | Input integrated with Formik, uses `@clayui/form` + antd Tooltip |
| `FormikSelect` | Select integrated with Formik |
| `FormikMultiSelect` | Multi-select integrated with Formik |
| `Input` | Standalone Clay input |
| `Select` | Standalone Clay select |
| `DatePicker` | Date picker |
| `PasswordInput` | Password input with toggle |
| `Switch` | Boolean toggle |
| `TimePicker` | Time picker |
| `ImageUpload` | Image upload |
| `MultiSelect` | Standalone multi-select |
| `Checkbox` | Checkbox |

### UI Components
| Export | Description |
|--------|-------------|
| `Modal` | Generic modal |
| `Alert` | Inline alert |
| `Breadcrumbs` | Breadcrumb navigation |
| `Filter` | Filter component |
| `Tabs` | Tab navigation |
| `LoadingAnimation` | Spinner/loading indicator |
| `FeedbackMessage` | Form feedback message |
| `InformationGroup` | Informational field group |

### Context and Hooks
| Export | Description |
|--------|-------------|
| `PortletConfigProvider` | Provider that exposes the 4 portlet params |
| `useConfiguration` | Hook that reads `PortletConfigContext` (portletNamespace, contextPath, portletElementId, configuration) |
| `useAlert` | Hook for displaying alerts |
| `useMedia` | Hook for media queries |
| `useAddressFetch` | Hook for zip code (CEP) address lookup |

### Namespace Utilities
```js
import { liferay, masks, dateUtil } from "sesc-portal-common-js-bundle";

// liferay.*
liferay.getUserId()
liferay.getScopeGroupId()
liferay.getPortalURL()
liferay.getPathThemeImages()
liferay.isSignedIn()
liferay.authToken
liferay.spritemap // = getPathThemeImages() + "/clay/icons.svg"

// masks.*
masks.cpf(value)
masks.cnpj(value)
masks.phone(value)
masks.cep(value)
masks.date(value)          // DD/MM/YYYY
masks.income(value)        // R$ format
masks.currencyMask(value)  // BRL locale
masks.removeMask(value)    // digits only
masks.ctps(value)
masks.number(value)        // digits only
masks.numberRegister(value)
```

**Migration target:**
```ts
import { Liferay, masks } from '@repo/common';

// Liferay.* (capital L, typed interface)
Liferay.ThemeDisplay.getUserId()
Liferay.ThemeDisplay.getScopeGroupId()
Liferay.ThemeDisplay.getPortalURL()
Liferay.ThemeDisplay.getPathThemeImages()
Liferay.ThemeDisplay.isSignedIn()
Liferay.authToken

// masks.* (same API, TypeScript types)
masks.cpf(value)
masks.cnpj(value)
// ... same as portlet
```

### Other Useful Exports
```js
import {
  openAlert,
  calculateAge,
  getErrorMessage,
  postDocumentWithUniqueName,
  deleteDocument,
  USER_CREDENTIAL_CATEGORY,
  USER_BLOCK_STATUS,
} from "sesc-portal-common-js-bundle";
```

---

## State Management Patterns

**Simple:** Local `useState`, no context.

**Medium:** React Context with custom hook:
```js
const ChartContext = createContext({});
export const ChartProvider = ({ children }) => {
  const [state, setState] = useState(initialValue);
  return <ChartContext.Provider value={{ state, setState }}>{children}</ChartContext.Provider>;
};
export const useChartContext = () => useContext(ChartContext);
```

**Complex:** `PortletConfigProvider` + `ConfigurationProvider` + cascading React Contexts.

**Forms:** Formik + Yup:
```js
const formik = useFormik({
  enableReinitialize: true,
  initialValues,
  validationSchema: yupSchema,
  onSubmit: handleSubmit,
});
// In JSX:
<Field name="field" component={FormikInput} label="Label" required />
```

---

## Styling

- **Bootstrap grid:** `row`, `col-md-X`, `d-flex`, `justify-content-between`, `align-items-center`, `mt-X`, `mb-X`, `gap-X`
- **Clay UI:** `@clayui/form`, `ClaySelect`, `ClayCheckbox`, `ClayInput` — for basic forms
- **antd:** `ConfigProvider`, `Modal`, `Tabs`, `Tooltip` — in more recent portlets
- **Custom SCSS:** in `css/styles.scss`, classes prefixed with the portlet name
- **Inline styles:** common for runtime-calculated dynamic sizes

---

## Complexity Levels

### Simple
- No API calls
- Data hardcoded or from `configuration`
- Clay UI for forms
- Local `useState` only

### Medium
- APIs via `Liferay.Util.fetch` or urql
- antd for UI
- React Context for shared state
- `PortletConfigProvider` + `useConfiguration`

### Complex
- Multiple routes (HashRouter + `<Routes>`)
- Service layer (`services/api.js` + `services/entityService.js`)
- Formik + Yup + `FormikInput` from common bundle
- `Liferay.ThemeDisplay.*` used directly in components
- `Liferay.on('allPortletsReady')`
- Multiple cascading contexts
