Scientyfic World

How to Use Regex in React: Form Validation, Search Highlighting and Common Pitfalls

To use regex in React, define the regular expression once outside your component, test the input value with pattern.test(value) when it changes or when the field loses focus, and store...

Share:

Get an AI summary of this article

How to use regex in react blog banner

To use regex in React, define the regular expression once outside your component, test the input value with pattern.test(value) when it changes or when the field loses focus, and store the result as an error message in state to show below the field. Regular expressions are a pattern language built into JavaScript, so there’s nothing React-specific to install; what matters is where you run them and a few pitfalls that cause bugs.

This guide covers practical patterns for email, phone and password validation, a complete validated form, safe search highlighting, and the regex mistakes that most often break React apps. I tested every pattern and component with React 19.3 in a Chromium-based browser and in Node.js; the results are included.

Regex Basics in 60 Seconds

const zip = /^\d{5}$/;          // literal: compiled once, best for fixed patterns
zip.test('90210');              // true
zip.test('9021');               // false

const word = 'react';
const dynamic = new RegExp(word, 'i');   // constructor: for patterns built at runtime
'I like React'.match(dynamic)[0];        // 'React'
TokenMeansTokenMeans
^ / $Start / end of the string\d / \w / \sDigit / word character / whitespace
[abc] / [^abc]One of / none of these characters+ / * / ?One or more / zero or more / optional
{3,16}Between 3 and 16 times(?=...)Lookahead: must be followed by
(?<name>...)Named capture groupi / g / u / vFlags: ignore case / global / Unicode / Unicode sets

Always anchor validation patterns with ^ and $; without them, /\d{5}/ also accepts “abc123456xyz”. MDN’s regular expressions guide is a good full reference.

Practical Validation Patterns

// Reusable patterns. Compile them once at module level, not inside components.
export const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;                       // pragmatic, not RFC-complete
export const PHONE_E164 = /^\+[1-9]\d{6,14}$/;                              // +14155550123
export const STRONG_PASSWORD = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{12,}$/;
export const USERNAME = /^[a-z0-9_]{3,16}$/i;

I ran each pattern against valid and invalid inputs:

PatternAcceptedRejected
EMAIL[email protected], [email protected]ada@example, ada @example.com, @example.com
PHONE_E164+14155550123, +9198765432104155550123 (no country code), +0123456789
STRONG_PASSWORDCorrect-Horse-9password123, Short1!
USERNAMEada_99ab (too short), Ada Lovelace (space)

Two cautions. No regex fully validates email addresses (the official grammar is enormous), so use a simple pattern to catch typos and confirm the address by sending an email. And phone formats vary by country: for real apps, a library such as libphonenumber-js is more reliable than a hand-written pattern.

A Validated Form in React

import { useState } from 'react';
import { EMAIL, STRONG_PASSWORD } from './patterns';

const rules = {
  email: (v) => (EMAIL.test(v) ? '' : 'Enter a valid email address'),
  password: (v) => (STRONG_PASSWORD.test(v) ? '' : '12+ characters with upper and lower case, a number and a symbol'),
};

export function SignupForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [touched, setTouched] = useState({});

  const errors = Object.fromEntries(Object.entries(rules).map(([k, check]) => [k, check(values[k])]));
  const valid = Object.values(errors).every((e) => !e);

  const update = (e) => setValues({ ...values, [e.target.name]: e.target.value });
  const blur = (e) => setTouched({ ...touched, [e.target.name]: true });   // show errors after leaving the field

  return (
    <form onSubmit={(e) => { e.preventDefault(); /* send values to your API */ }} noValidate>
      <input name="email" value={values.email} onChange={update} onBlur={blur} aria-invalid={!!(touched.email && errors.email)} />
      {touched.email && errors.email && <p className="err-email">{errors.email}</p>}
      <input name="password" type="password" value={values.password} onChange={update} onBlur={blur} />
      {touched.password && errors.password && <p className="err-password">{errors.password}</p>}
      <button disabled={!valid}>Sign up</button>
    </form>
  );
}

What happened in my test:

InputShown to the userSubmit button
Email “ada@example”, then leave the field“Enter a valid email address”Disabled
Email “[email protected]”, password “short1!”Password rule messageDisabled
Password “Correct-Horse-9”No errorsEnabled

Errors appear only after the user leaves a field (onBlur), which is less annoying than flagging every keystroke, and errors are derived from the current values rather than stored separately, so they can’t get out of sync. The patterns live in their own module, so they’re compiled once. Always repeat the same checks on the server: client-side validation is for user experience, not security. For larger forms, a library such as React Hook Form with a schema (for example Zod) saves a lot of code.

The pattern attribute: a v-flag trap

HTML inputs also accept a pattern attribute, but browsers now compile it with the v (Unicode sets) flag, which is stricter about special characters inside [...]. I tested two versions:

<input pattern="[\w-]+" />    <!-- invalid with the v flag: the browser ignores the whole pattern -->
<input pattern="[\w\-]+" />   <!-- escaped hyphen: works -->

With [\w-]+, the browser logged “Pattern attribute value [\w-]+ is not a valid regular expression” and then treated “has space” as valid, because an invalid pattern is ignored entirely. The escaped version correctly rejected it. Escape -, (, ), [, {, | and similar characters inside character classes in pattern attributes.

Highlighting Search Matches Safely

// Highlight every match of the user's search text, safely
export function Highlight({ text, query }) {
  if (!query) return text;
  const safe = RegExp.escape(query);                    // ES2025: escapes . * + ? ( ) [ ] etc.
  const parts = text.split(new RegExp(`(${safe})`, 'gi'));   // the capture group keeps the matches in the array
  return parts.map((part, i) => (i % 2 === 1 ? <mark key={i}>{part}</mark> : part));
}

Searching for $5.99 (sale) in “Prices: $5.99 (sale) and $5×99 (typo)” highlighted only the exact text. Without escaping, . would match any character and the parentheses would create a group, and input such as ( would throw “Invalid regular expression”. RegExp.escape() is new (ES2025): it worked in my Chromium 156 browser and in Node.js 24, but not in Node.js 23. Where it’s missing, use this fallback, which gave the same result in my test:

const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

Splitting with a capture group keeps the matched text in the array at odd positions, and rendering parts as React children (not with dangerouslySetInnerHTML) keeps the output safe from injected HTML.

Regex Pitfalls That Break React Apps

1. The g flag makes test() stateful

const withG = /react/g;
withG.test('react');   // true
withG.test('react');   // false  (!)
withG.test('react');   // true
// lastIndex moves after each match, so the next search starts mid-string

A global regex reused across renders or list items gives alternating results. Don’t use the g flag with test(); use it only with matchAll, replaceAll or split.

2. Catastrophic backtracking (ReDoS)

Nested quantifiers can make a regex take exponential time on input that almost matches:

Input length (letters + “!”)/^(a+)+$//^a+$/
2033 ms0.01 ms
24100 ms0.01 ms
281,610 ms0.01 ms

Each extra character roughly doubles the time, and while the regex runs the page is frozen. Avoid nested quantifiers such as (a+)+ or (\w*)*, limit input length before testing, and never build patterns from untrusted input without escaping them.

3. Creating regexes inside render

new RegExp(...) inside a component body runs on every render. Keep fixed patterns at module level, and wrap dynamic ones in useMemo keyed on their source.

4. Forgetting anchors or the u flag

Unanchored patterns accept extra text around a match, and without the u or v flag, characters such as emoji are treated as two code units.

Parsing with Named Groups

const log = 'ERROR 2026-10-31 db timeout; WARN 2026-10-30 slow query';
for (const m of log.matchAll(/(?<level>ERROR|WARN) (?<date>\d{4}-\d{2}-\d{2}) (?<msg>[^;]+)/g)) {
  console.log(m.groups.level, m.groups.date, m.groups.msg);
}
// ERROR 2026-10-31 db timeout
// WARN 2026-10-30 slow query

Named groups make extraction code readable, which helps when you parse log lines, dates or IDs in a component. For parsing HTML, though, use the DOM rather than regex (see what HTML parsing means).

Conclusion

Regex in React is plain JavaScript regex used in the right places: patterns defined once outside components, checked on change or blur, and turned into error messages in state. Anchor your patterns, keep the g flag away from test(), avoid nested quantifiers that can freeze the page, escape user input with RegExp.escape or a fallback before building a pattern from it, and escape special characters in HTML pattern attributes. Validate again on the server, and reach for a form or schema library when forms grow.

FAQs

How do I use regex in React?

Define the pattern outside your component, test the input value with pattern.test(value) in an onChange or onBlur handler (or while rendering), and keep the resulting error message in state to display under the field.

What is a good regex for email validation in React?

A pragmatic pattern such as /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/ catches common typos. No regex fully validates email addresses, so also confirm the address by sending an email and validate on the server.

Why does my regex test() return true and then false?

The regex has the g (global) flag. With g, test() remembers lastIndex and starts the next search from there. Remove the g flag when using test().

How do I escape user input for a regex in JavaScript?

Use RegExp.escape(input), available in current browsers and Node.js 24+. Elsewhere, replace special characters with a fallback such as s.replace(/[.*+?^${}()|[\]\\]/g, “\\$&”).

Should I use the HTML pattern attribute or regex in JavaScript?

The pattern attribute is simple and works without JavaScript, but browsers compile it with the v flag and silently ignore invalid patterns. JavaScript validation gives you custom messages and timing; use both, and always validate on the server.

Can a regex freeze my React app?

Yes. Patterns with nested quantifiers like (a+)+ can take exponential time on certain inputs. In my test, a 29-character string took 1.6 seconds. Avoid nested quantifiers and limit input length.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts