Every fintech product I have been close to has had the same shape: a ledger and a set of services behind it that take months to get right, and a web interface in front of it that users judge in seconds. The front-end is thin, but it is the only layer a customer, a merchant or a regulator's examiner ever sees. React has become the default way to build it, and this article is about what that default means when the thing on screen is someone's money.
Why React matters in fintech
The front-end is where trust is decided
A statement that takes four seconds to render, a transfer form that accepts "1,000.000" and silently rounds it, a balance that flickers between two values after a payment: none of these are back-end bugs, and all of them cost trust. The front-end also carries obligations the back-end cannot fulfil on its behalf: accessible forms, consent screens with the approved wording, card fields that keep the page out of PCI scope, Arabic layouts that survive a long IBAN. React matters here because it is where most of this work now happens. The library is small; the ecosystem of form libraries, server-state caches, design systems and hosted payment components is what matters.
Why a view library became a platform
React started as a way to render UI from state. Over a decade it accumulated what a product team needs: a component model that maps onto design systems, TypeScript as the norm, and frameworks on top (Next.js, React Router, TanStack Start) that handle routing, server rendering and data loading. The documentation at react.dev now recommends starting with a framework, which tells you how the maintainers see the library's role. For a fintech team the choice is rarely "React or something else". It is "which parts of the ecosystem, with what discipline, and where is the line between client and server".
This is not a tutorial but a map of the subject for people deciding whether to build on React, how to staff it, and which parts of it produce audit findings when handled carelessly.
A front-end does not need to be clever. It needs to be correct about money, honest about what the server has confirmed, and small enough in scope that an auditor can reason about it.
Where it is used
Payment providers and embedded components
Stripe's Dashboard is a React and TypeScript application, as Stripe's engineering blog has described. More relevant for most teams, Stripe Elements, the hosted inputs for cards, wallets and bank details, ship with an official React package, and the Elements documentation assumes a React tree. Adyen's Web Components and Checkout.com's Frames follow the same model: a provider-hosted iframe positioned by your component, with the sensitive fields on the provider's origin. Plaid Link, which connects a bank account to an app, embeds through a JavaScript SDK with an official React hook, documented in the Plaid Link docs.
Trading and brokerage front-ends
Robinhood's web application is a React app, and its engineering team has written publicly about building it. Coinbase has documented React on the web and React Native on mobile. Trading screens, with streaming prices and tables of thousands of rows, are the demanding case, and React is used there; the performance concerns are about how you use it, not whether it can cope.
Money movement and neobanks
Wise maintains an open-source React design system, Neptune, which underpins its web product. Many neobanks and remittance companies describe React and TypeScript front-ends in their job postings and engineering blogs, usually alongside a native or React Native mobile app.
The MENA picture
In Egypt, Saudi Arabia and the UAE the public documentation is thinner because most fintechs here are younger and publish less. What is observable: React and TypeScript appear in the large majority of front-end job descriptions from the region's payment companies, BNPL providers, wallets and digital banks, and the merchant portals you meet as a partner are overwhelmingly React single-page applications. I will not name companies whose stacks I only know from the outside.
Internal tools
The least glamorous and most common use is the operations console where compliance reviews KYC cases and support reverses charges. These are React apps in most companies, they often carry more privileged access to customer data than the customer-facing product, and they rarely get the same security attention.
Strengths
The component model fits regulated UI
Financial interfaces are repetitive by design: the same amount input, beneficiary selector, fee confirmation and status badge for pending, settled, failed and reversed. React lets you build each once, test it once, have compliance review it once, and reuse it everywhere.
TypeScript is the norm
React without TypeScript is unusual in professional teams, and in fintech it should be unthinkable. The type system lets you say "an amount is a branded integer of minor units in a specific currency" and have the compiler refuse to add EGP to USD. Libraries such as zod extend that discipline to runtime: one schema validates a form, parses an API response and documents the contract.
Form and server-state libraries are mature
Two categories of library now have a clear, boring, correct choice. For forms, react-hook-form with a zod resolver handles validation, accessibility attributes and submission state with little code. For server data, TanStack Query and its peers solved caching, deduplication, background refresh and optimistic updates in the way a banking UI needs: show what we know, refresh it quietly, never lie about it.
Design systems and right-to-left layouts
A fintech product in MENA ships in Arabic and English at minimum, often with different numeral conventions per market. React design systems handle direction through CSS logical properties and context, and the component model makes it practical to build an input that knows it is showing an IBAN (always left-to-right) inside an Arabic paragraph.
Risks and pitfalls
Floating-point money
JavaScript has one numeric type, and 0.1 + 0.2 is not 0.3. Any amount that passes through a number as a decimal is a rounding error waiting to happen, and in a ledger a rounding error is a reconciliation break. Amounts are integers of minor units (piastres, halalas, fils) from the input until they are formatted for display.
State that lies
A balance that updates before the server confirms the transfer and never rolls back when the server rejects it is a screen that lies about money. Keep one source of truth per piece of server state and derive everything else from it.
Over-fetching, waterfalls and spinners
A dashboard that fires fifteen requests on mount, each component fetching its own data and showing its own spinner, is the default outcome of React without a data layer. The fixes exist (colocated queries with a shared cache, route-level loaders, server-rendered initial state) but must be chosen deliberately, before the dashboard has sixty screens.
Dependency sprawl
A typical React project has hundreds of transitive dependencies. In fintech that is an attack surface with a documented history: the 2018 event-stream incident injected code targeting a cryptocurrency wallet through a popular npm package, and the 2024 polyfill.io compromise turned a widely embedded script into a malware channel. Supply-chain discipline is a security control, not housekeeping.
Accessibility as an afterthought
Financial services are increasingly subject to accessibility obligations, and even where the law is quiet, a form a screen-reader user cannot complete excludes customers. React does nothing to prevent accessible UI and nothing to guarantee it.
Framework churn
Class components gave way to hooks, Create React App was retired in favour of frameworks and Vite, and the server-rendering story has changed shape several times. Choosing boring, well-maintained libraries and resisting rewrites is a leadership responsibility.
The question is never "can React do this?" It almost always can. The question is "what does the team have to do so that React does this correctly, and will they still be doing it in three years?"
Architecture and integration patterns
Separate the money model from the view
The most valuable architectural decision is to define money, accounts and transfers as types independent of React, validated with a schema, and shared between the form layer, the API client and, ideally, the backend. A Money type of the shape { amountMinor: number; currency: "EGP" | "SAR" | "AED" | "USD" }, with zod schemas around it, catches more bugs than any amount of UI testing. Everything else in the front-end is a view of that model.
Server state is not client state
React's built-in state is for UI concerns: which tab is open, whether a modal is visible, what the user typed but has not submitted. Everything from the server (balances, transactions, KYC status, rates) is a cache of someone else's truth and belongs in a library built for caches. Conflating the two is how balances go stale.
| Library | Best for | Caching and invalidation | Optimistic updates | Notes for fintech |
|---|---|---|---|---|
| TanStack Query | REST or RPC server state | Built in, key-based, background refetch | First-class, with snapshot and rollback | The default choice |
| SWR | Read-heavy screens | Built in, stale-while-revalidate | Supported, less structured | Fine for read-only dashboards |
| RTK Query | Teams already on Redux | Built in, tag-based | Supported | Sensible if you need Redux anyway |
| Apollo Client | GraphQL APIs | Normalised cache | Supported | Normalisation needs care around money fields |
| Zustand or Jotai | Client-only UI state | None | Not applicable | Never for server data |
| XState | Multi-step flows such as KYC | None | Not applicable | Explicit states; easy to review with compliance |
The table is opinionated on purpose. A KYC flow with twelve steps, conditional documents per market and resumable sessions is a state machine, and modelling it as one makes it reviewable by people who do not read React.
Forms: validate at the edge of the component
A money form should produce a validated, typed object or nothing at all. Three details matter in the sample below. The amount is captured as a string and converted to minor units during validation, so no floating-point value ever exists. The IBAN check is a format check; the server validates the checksum and the beneficiary. Every error is tied to its input through aria-describedby and announced through role="alert".
Code: validated transfer form with react-hook-form and zod
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const CURRENCIES = ["EGP", "SAR", "AED", "USD"] as const;
// Money never travels as a float: "1,250.50" becomes 125050 minor units.
function toMinorUnits(raw: string): number | null {
const clean = raw.replace(/[,\s]/g, "");
if (!/^\d+(\.\d{1,2})?$/.test(clean)) return null;
const [whole, frac = ""] = clean.split(".");
return Number(whole) * 100 + Number(frac.padEnd(2, "0"));
}
const schema = z.object({
iban: z.string()
.transform((s) => s.replace(/\s+/g, "").toUpperCase())
.pipe(z.string().regex(/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/, "Enter a valid IBAN")),
currency: z.enum(CURRENCIES),
amount: z.string().min(1, "Amount is required").transform((s, ctx) => {
const minor = toMinorUnits(s);
if (minor === null || minor <= 0) {
ctx.addIssue({ code: "custom", message: "Enter a positive amount, max 2 decimals" });
return z.NEVER;
}
return minor;
}),
});
type FormInput = z.input<typeof schema>;
export type Transfer = z.output<typeof schema>; // amount is already in minor units
export function TransferForm({ onSubmit }: { onSubmit: (t: Transfer) => Promise<void> }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } =
useForm<FormInput, unknown, Transfer>({ resolver: zodResolver(schema) });
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<label htmlFor="iban">Beneficiary IBAN</label>
<input id="iban" autoComplete="off" aria-invalid={!!errors.iban}
aria-describedby={errors.iban ? "iban-error" : undefined} {...register("iban")} />
{errors.iban && <p id="iban-error" role="alert">{errors.iban.message}</p>}
<label htmlFor="currency">Currency</label>
<select id="currency" {...register("currency")}>
{CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
<label htmlFor="amount">Amount</label>
<input id="amount" inputMode="decimal" aria-invalid={!!errors.amount}
aria-describedby={errors.amount ? "amount-error" : undefined} {...register("amount")} />
{errors.amount && <p id="amount-error" role="alert">{errors.amount.message}</p>}
<button type="submit" disabled={isSubmitting}>Send transfer</button>
</form>
);
}
Tested with React 18, TypeScript 5.
Optimistic updates that can be rolled back
The pattern TanStack Query documents (snapshot in onMutate, restore in onError, invalidate in onSettled) is exactly what a transfer needs. Fintech adds an idempotency key: a unique identifier generated once per submit attempt and sent with the request, so a network retry cannot create a second transfer. The key lives in the mutation variables, so a retry reuses it while a new submit gets a new one.
Code: optimistic transfer with TanStack Query v5 and an idempotency key
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Transfer } from "./TransferForm";
type Account = { id: string; balanceMinor: number; currency: string };
type Variables = { transfer: Transfer; idempotencyKey: string };
async function postTransfer(sourceAccountId: string, { transfer, idempotencyKey }: Variables) {
const res = await fetch("/api/transfers", {
method: "POST",
credentials: "include", // session cookie; no bearer token in localStorage
headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
body: JSON.stringify({ ...transfer, sourceAccountId }),
});
if (!res.ok) throw new Error(`Transfer failed with ${res.status}`);
return (await res.json()) as { id: string; status: "pending" | "settled" };
}
export function useSendTransfer(sourceAccountId: string) {
const qc = useQueryClient();
const accountKey = ["account", sourceAccountId] as const;
const mutation = useMutation({
mutationFn: (vars: Variables) => postTransfer(sourceAccountId, vars),
onMutate: async ({ transfer }) => {
await qc.cancelQueries({ queryKey: accountKey });
const previous = qc.getQueryData<Account>(accountKey);
if (previous) {
qc.setQueryData<Account>(accountKey, {
...previous,
balanceMinor: previous.balanceMinor - transfer.amount,
});
}
return { previous };
},
onError: (_error, _vars, context) => {
// Roll back: never show a balance the server has not confirmed.
if (context?.previous) qc.setQueryData(accountKey, context.previous);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: accountKey });
qc.invalidateQueries({ queryKey: ["transfers", sourceAccountId] });
},
});
// One key per submit attempt. Retries of this mutation reuse the same
// variables, so the server can safely de-duplicate; a new submit gets a new key.
const send = (transfer: Transfer) =>
mutation.mutateAsync({ transfer, idempotencyKey: crypto.randomUUID() });
return { send, isPending: mutation.isPending, error: mutation.error };
}
Tested with React 18, TypeScript 5.
A backend-for-frontend, not a public API
Most fintech front-ends should talk to a BFF: a thin server owned by the front-end team that holds the session, aggregates internal services, strips fields the UI does not need and sets the cookies. It is the natural home for the httpOnly session cookie, the CSRF token, the rate limit and the audit log.
Real-time data
Prices, order status and fraud alerts arrive over WebSockets or server-sent events. Treat the stream as a source of cache updates (a message arrives, you call setQueryData on the affected key) rather than a separate state tree.
Security and compliance
PCI DSS scope in plain words
If your page ever handles a card number it is in scope for PCI DSS, and how it handles that number determines which self-assessment questionnaire applies:
| Approach | Where card data is entered | Typical questionnaire | What it means for your React app |
|---|---|---|---|
| Redirect to the provider's hosted page | The provider's page | SAQ A | Smallest scope, least control |
| Provider-hosted iframe fields in your page | The provider's iframe on its own origin | SAQ A, with the newer payment-page script controls | Your React tree never sees card data |
| Your own inputs posting directly to the provider's API | Your page | SAQ A-EP | Your JavaScript handles the card number; much larger scope |
| Your own inputs posting to your own server | Your page and your server | SAQ D | Avoid unless you are a payment company |
The difference between the second and third rows is the whole reason hosted fields exist. With an iframe field, a cross-site scripting bug in your React app cannot read the card number, because it lives in a different document on a different origin. The current version of PCI DSS also added requirements on managing and monitoring payment-page scripts; they apply even with hosted fields, so keep the payment page small and its script list short.
Code: PCI-scoped card field mounted as a hosted iframe
import { useEffect, useRef, useState, type FormEvent } from "react";
// Minimal shape of a hosted-fields SDK. Stripe Elements, Adyen Components and
// Checkout.com Frames all follow this pattern: the provider's script renders an
// <iframe> on the provider's origin and hands you a mount/tokenize API.
type HostedCardField = {
mount: (el: HTMLElement) => void;
unmount: () => void;
on: (event: "change", cb: (e: { complete: boolean; error?: { message: string } }) => void) => void;
tokenize: () => Promise<{ token: string }>;
};
type HostedFieldsSdk = { createCardField: (opts: { locale: string }) => HostedCardField };
declare global { interface Window { PaymentsSdk?: HostedFieldsSdk } }
export function CardField({ onToken }: { onToken: (token: string) => Promise<void> }) {
const mountRef = useRef<HTMLDivElement>(null);
const fieldRef = useRef<HostedCardField | null>(null);
const [complete, setComplete] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const sdk = window.PaymentsSdk;
if (!sdk || !mountRef.current) return;
// Why an iframe: the PAN, expiry and CVC are typed into a document on the
// provider's origin. Our React state only ever holds "complete" and an error
// string, never the digits, so this page can stay in PCI DSS SAQ A territory
// instead of A-EP, and an XSS bug in our bundle cannot read card numbers.
const field = sdk.createCardField({ locale: document.documentElement.lang || "en" });
field.mount(mountRef.current);
field.on("change", (e) => {
setComplete(e.complete);
setError(e.error?.message ?? null);
});
fieldRef.current = field;
return () => { field.unmount(); fieldRef.current = null; };
}, []);
const submit = async (ev: FormEvent) => {
ev.preventDefault();
if (!fieldRef.current) return;
const { token } = await fieldRef.current.tokenize(); // a reference, not card data
await onToken(token); // your backend charges with the token; it never sees the PAN
};
return (
<form onSubmit={submit}>
<span id="card-label">Card details</span>
<div ref={mountRef} role="group" aria-labelledby="card-label"
aria-describedby={error ? "card-error" : undefined} />
{error && <p id="card-error" role="alert">{error}</p>}
<button type="submit" disabled={!complete}>Pay</button>
</form>
);
}
Tested with React 18, TypeScript 5.
XSS and Content Security Policy
React escapes rendered text by default, which removes the most common injection vector and leaves the others: dangerouslySetInnerHTML, href values built from user data, third-party scripts, and hand-assembled server HTML. A strict Content Security Policy (nonce-based scripts, no unsafe-inline, frame-ancestors, connect-src limited to your origins and providers) turns most of these from breaches into console errors; the OWASP CSP cheat sheet is the reference.
Dependency supply chain
Commit the lockfile and install with npm ci so builds are reproducible. Run npm audit and a vulnerability scanner in CI, and treat the output as a queue with owners rather than a gate. Prefer packages published with provenance, which links a package to its source commit and build. Pin and review anything that touches the DOM, the network or cryptography, never load scripts from third-party CDNs at runtime (polyfill.io showed why), and on a payment page enforce a short script allow-list with CSP and Subresource Integrity.
Tokens, cookies and CSRF
Store the session in an httpOnly, Secure, SameSite cookie set by your BFF. Do not put access tokens in localStorage: anything an XSS payload can read it can exfiltrate. Cookies bring CSRF back into scope, so the BFF needs SameSite=Lax or Strict plus a custom request header browsers do not send cross-site, or a synchroniser token. Rotate refresh tokens and require step-up authentication for high-value actions, which is what PSD2's strong customer authentication requirement formalises in Europe and what SAMA and CBUAE guidance expect in practice.
Clickjacking and framing
Your pages should send frame-ancestors 'none' (or a strict allow-list) so nobody can overlay a transparent copy of your transfer button on a malicious site. Hosted card fields are iframes on the provider's origin, and the provider controls their framing; what you control is whether your checkout page itself can be framed. Make sure it cannot.
PII in analytics, logs and session replay
Session-replay tools, analytics SDKs and error reporters will capture a national ID typed into a KYC form or an IBAN unless told not to. Mask all inputs by default, block replay on onboarding and payment routes, and keep console logging out of production. The personal data protection laws in Egypt, Saudi Arabia and the UAE treat this data as sensitive, and a recording stored in a third-party tool abroad is exactly the kind of transfer a regulator asks about.
Feature flags for regulated changes
A fee change, new consent text, a limit change or a new KYC step is a regulated change, and the front-end should be able to switch it on at a precise time, off instantly, and prove when it was on. Use a flag system with an audit log, and keep regulatory flags separate from product experiments.
Regulators by name
The frameworks that shape front-end decisions in the region are the Saudi Central Bank (SAMA) rules and cybersecurity framework, the Central Bank of the UAE (CBUAE) regulations for payment and stored-value services, the Central Bank of Egypt (CBE) rules for payment service providers and digital banking, and, for anyone serving European customers, PSD2 and its strong customer authentication requirement. None of them mention React. All of them care about authentication strength, data residency, audit trails and consent, and the front-end is where most of those are implemented.
Accessibility obligations
WCAG 2.2 at level AA is the practical target. Government digital standards in Saudi Arabia and the UAE reference WCAG, and the European Accessibility Act applies to financial services for EU customers. In React terms: real buttons and inputs, labels tied to fields, errors announced, focus management in modals and multi-step flows, contrast in both themes, and keyboard access to every action.
Performance and scaling
Bundle size and code splitting
A dashboard does not need the charting library on the login page or the PDF renderer on the transfer screen. Set a bundle budget per route, split on routes and heavy components with React.lazy, and watch the budget in CI. Most of the weight is not React but what is imported alongside it: date libraries, icon sets, charting, rich-text editors.
SSR, Server Components and Next.js, honestly
Server rendering helps in two situations: content that should be visible before JavaScript loads (marketing, help, pricing), and authenticated pages where first paint matters and the data is reachable from the server. React Server Components go further by keeping data-heavy, non-interactive parts of the tree off the client entirely. What they do not help with: an interactive trading screen, a multi-step form, anything driven by WebSockets. A Next.js app where every route is "use client" pays for a full-stack framework and gets a static SPA; a Vite SPA behind a BFF is still a perfectly good architecture for an authenticated dashboard.
Hydration
Hydration mismatches are a classic source of flicker on money displays: the server formatted a number in one locale and the client in another, or the server rendered a timestamp in UTC and the client in Cairo time. Format numbers and dates with an explicit locale and time zone passed from the server, or render them on the client only, and never let Date.now() into server-rendered output.
Real-time data and virtualised tables
A transaction list with ten thousand rows must be virtualised (TanStack Virtual or similar) so only the visible rows are in the DOM. A price feed updating fifty times a second must be throttled to the display's refresh rate and batched into one state update, which React 18's automatic batching helps with and useTransition keeps from blocking typing.
Numbers, currencies, RTL and Arabic numerals
Use Intl.NumberFormat (reference on MDN) with an explicit locale and currency; never concatenate a symbol onto a toFixed result. ar-EG and ar-SA default to Eastern Arabic numerals, while many banking users in the region expect Western digits on financial screens. Make that a product decision, set it once with the nu extension (ar-EG-u-nu-latn), and apply it everywhere. For RTL, use CSS logical properties, wrap IBANs, card numbers and reference codes in dir="ltr" with bidi isolation, and test every amount with a negative sign and a currency code.
Core Web Vitals in practice
Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift map well onto what a financial UI needs: show the balance quickly, respond to taps immediately, do not shift the layout as data arrives. The web.dev guide explains the thresholds. Measure with real-user monitoring from the markets you serve; a lab score from a European data centre says little about 4G in Upper Egypt.
Team and hiring in MENA
Talent depth
Egypt has one of the largest pools of front-end engineers in the region, and React is the dominant skill in it. Saudi Arabia's market is growing quickly, driven by national digital programmes and localisation requirements, and competes hard for senior people. The UAE is where product leadership and senior engineers from everywhere overlap, with costs to match. A realistic shape is a small senior core where the licence lives, a larger build team in Egypt, and a shared design system keeping them aligned.
TypeScript discipline
"Knows React" is a low bar. The signal that matters is whether a candidate reaches for types to encode business rules (a branded MinorUnits type, a discriminated union for transfer status, a schema shared between form and API) or treats TypeScript as a linter to be silenced with any. Ask them to model a transfer and watch what they do with the amount.
Design-system reuse
A team that builds its own buttons on every project will build its own money input on every project, and one of those will accept a float. Invest in a design system early, give it an owner, and treat it as a product with releases.
Interview signals
Ask how they would store a session token and why; what happens to the balance if a transfer fails after the optimistic update; how they make a form usable with a keyboard only; what 0.1 + 0.2 is in JavaScript and what they do about it. All of them separate people who have shipped money UIs from people who have shipped UIs.
Team shape and ownership
Give the front-end team ownership of the BFF. A team that owns the session, the API shape it consumes and the cookies it sets can be held responsible for front-end security; a team that only owns components cannot.
Decision framework
When React is the right call
Authenticated dashboards and portals with rich interaction. Onboarding and KYC flows with many steps and conditional logic. Trading and treasury screens with real-time data. Internal operations tools. Anything that embeds provider components shipped React-first.
When a server-rendered stack is better
Public, content-heavy pages (pricing, help centres, regulatory disclosures) are better served by a server-rendered or static stack, which can be Next.js or Astro with React islands, or no React at all. Simple CRUD back-office tools with a small user base ship faster on a server-rendered framework (Django, Rails, Laravel) with a little interactivity on top.
When native wins
Consumer banking in MENA is mobile-first, and the phone brings capabilities the web does not: biometrics, secure storage, device attestation, push notifications for transaction approval. React on the web is the right home for the dashboard, the merchant portal and the operations console; the consumer wallet is usually a native or Flutter app, with React Native reasonable for teams already deep in React.
Decision matrix
| Need | React SPA (Vite) with a BFF | Next.js or another React framework | Vue or Svelte | Angular | Native or Flutter app |
|---|---|---|---|---|---|
| Authenticated dashboard, rich interaction | Strong | Strong | Strong | Strong | Not on web |
| Public content, SEO, first paint | Weak | Strong | Strong with SSR | Moderate | Not applicable |
| Multi-step KYC with resume | Strong | Strong | Strong | Strong | Strong |
| Real-time trading screens | Strong | Strong (client parts) | Strong | Strong | Strong |
| Hosted payment components | Official React wrappers | Official React wrappers | Plain JS SDK | Plain JS SDK | Native SDKs |
| Hiring depth in MENA | Deepest | Deepest | Moderate | Moderate, enterprise-leaning | Flutter strong in Egypt |
| Device security (biometrics, attestation) | WebAuthn only | WebAuthn only | Limited | Limited | Strong |
| Long-term maintenance risk | Low with discipline | Moderate (framework churn) | Low | Low | Platform-dependent |
The matrix is not a scorecard to add up. For most regional fintechs the answer is React for the web surface, a mobile app for consumers, and a server-rendered stack for public content.
FAQ
Is React secure enough for banking applications?
React is as secure as the application built on it. It escapes rendered text by default, which removes the most common injection vector, and it has no opinion about sessions, cookies, CSP or dependencies, which is where banking applications actually get breached. The posture comes from the surrounding decisions: httpOnly cookies through a BFF, a strict CSP, a reviewed dependency list, hosted fields for cards, and no PII in third-party scripts.
Should we use Next.js or plain React for a fintech dashboard?
If the product has a meaningful public surface, or if first paint on authenticated pages matters and your data is reachable from the server, Next.js or another server-rendering React framework is worth its complexity. If the product is an authenticated dashboard with a stable user base and heavy interactivity, a Vite SPA behind a BFF is simpler to operate, simpler to secure and easier to reason about in an audit.
How do we handle money and currency formatting in React?
Carry amounts as integers of minor units from the input to the API, with the currency alongside. Validate at the form boundary with zod or similar, never put a decimal amount into a JavaScript number, and format for display only at the last moment with Intl.NumberFormat and an explicit locale, currency and numeral system. Decide once whether Arabic screens use Western or Eastern Arabic digits and apply that decision everywhere.
Can we keep PCI DSS scope small with a React checkout?
Yes, and it is the main reason to use provider-hosted fields. When the card number is typed into an iframe on the provider's origin, your React application never holds it, and your assessment can typically be the simpler SAQ A rather than SAQ A-EP. You still own the page that embeds the iframe, so you must control the scripts on it, prevent it from being framed, and keep analytics and replay tools away from it.
Is it hard to hire React developers in Egypt, Saudi Arabia and the UAE?
Finding React developers is not hard; it is the most common front-end skill in all three markets. Finding engineers who treat TypeScript as a modelling tool, understand server state, and have shipped a money form that handles minor units correctly is harder, and that is where interviews should focus. Egypt offers the deepest pool, Saudi Arabia the fastest-growing demand, and the UAE the most concentrated senior talent at the highest cost.
Key takeaways
- React is the fintech default because of its ecosystem rather than the library itself: forms, server-state caching, design systems and hosted payment components all ship React-first.
- Money is an integer of minor units with a currency. It enters as a string, is validated at the form boundary and is formatted only at display time.
- Server state belongs in a server-state library with explicit invalidation and rollback; an optimistic update may run ahead of the server for a moment, but it must never stay wrong.
- Every mutation that moves money carries an idempotency key generated once per submit attempt and reused on retries.
- Card data belongs in a provider-hosted iframe; that one decision keeps the React app out of the heaviest PCI DSS scope.
- Session tokens go in httpOnly cookies set by a BFF, with CSRF protection and a strict CSP; nothing sensitive goes in localStorage, analytics or session replay.
- Be honest about server rendering: use it where first paint and public content matter, accept client components where interaction is the point, and set bundle and vitals budgets in CI from day one.
- Hire for TypeScript discipline and money-handling judgement rather than React familiarity, and invest early in a shared design system.