How can I correctly use JavaScript string replace for dynamic text?

I’m trying to use JavaScript’s string replace to update dynamic text on my page, but the results are inconsistent. Sometimes only the first match is replaced, and other times special characters break the pattern. I need help understanding how to properly use replace with regular expressions, global flags, and variables so I can reliably update all matching parts of a string in my app.

JavaScript string.replace has a few gotchas that match exactly what you describe.

  1. Only first match replaced

If you use a plain string or a regex without the global flag, it replaces only the first match.

Wrong:
text.replace(‘foo’, ‘bar’)

Right:
text.replace(/foo/g, ‘bar’)

So if your dynamic text has multiple copies, use a RegExp with /g.

  1. Special characters break the pattern

If your search value comes from user input or any dynamic source, you need to escape regex metacharacters. Otherwise characters like ., *, +, ?, (, ), [, ], {, }, |, ^, $ will change the pattern.

Helper:

function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[]\]/g, ‘\$&’);
}

Then:

const search = escapeRegExp(userInput);
const regex = new RegExp(search, ‘g’);
const result = text.replace(regex, replacement);

  1. Use a function for dynamic replacements

If the replacement depends on the match, use a callback.

const text = ‘Hello NAME, your score is SCORE.’;
const map = { NAME: ‘Alex’, SCORE: ‘95’ };

const result = text.replace(/NAME|SCORE/g, m => map[m] || m);

That avoids multiple replace calls and keeps logic in one place.

  1. For simple no-regex replacements

If you do not need regex features and you want all occurrences, on modern browsers you can use replaceAll.

text.replaceAll(‘foo’, ‘bar’);

If you go that route, you do not need to escape special chars, because the first argument is treated as a plain string.

  1. Avoid mixing regex and replacement symbols by accident

Remember that in replacement strings, $1, $2, $& etc have special meaning.

If you need to insert a literal dollar sign, escape it.

text.replace(/x/g, ‘$$’); // inserts a single $

Quick patterns you might want:

Replace all plain occurrences of a dynamic string:
const pattern = escapeRegExp(search);
const result = text.replace(new RegExp(pattern, ‘g’), replacement);

Template style replacement like {{name}}:
const data = { name: ‘John’, age: 30 };

const result = template.replace(/{{\s*(\w+)\s*}}/g, (m, key) => {
return key in data ? String(data[key]) : m;
});

If your results look inconsistent, check:

• Are you mixing plain string and regex usage.
• Are you missing /g.
• Are you feeding unescaped dynamic values into new RegExp.

You’re basically running into three overlapping problems: how replace behaves, how regex behaves, and how user/dynamic input messes with both.

@cazadordeestrellas already nailed the classic stuff (/g, escaping, replaceAll), so I’ll try not to repeat all that and focus on patterns / gotchas around dynamic text on a page.


1. Decide first: regex or no regex

Half the “inconsistent” behavior I see in codebases is because people mix both styles randomly:

// Plain string mode
text.replace('foo', 'bar');   // only first match, no regex features

// Regex mode
text.replace(/foo/g, 'bar');  // regex, all matches

If what you want is template-ish replacement (like filling in labels, names, ids, etc.) then most of the time you do not need regex at all.

Use:

text.replaceAll(search, replacement);

This avoids all the “special characters break things” problem entirely, because search is treated as a literal string, not a pattern.

If you’re currently doing:

const pattern = new RegExp(userInput, 'g'); // <- unstable
text = text.replace(pattern, replacement);

and you don’t actually want regex power, toss that and just use replaceAll(userInput, replacement).


2. If you really must use regex, isolate it

Template engines, dynamic tokens, etc. usually have a fixed wrapper and a dynamic key. That means: keep the regex fixed and the data dynamic.

Bad pattern:

// Directly turning dynamic text into a regex
const userPattern = new RegExp(input, 'g');  // brittle
text = text.replace(userPattern, replacement);

Better pattern:

// Use a stable regex to detect the *structure*
const REG = /{{\s*([\w.-]+)\s*}}/g;

const result = template.replace(REG, (match, key) => {
  return key in data ? String(data[key]) : match;
});

Nothing dynamic goes in the regex, so no need to escape everyhing all the time. Your “dynamic” part is just the lookup data[key].

If your use case is “replace arbitrary phrases provided by the user in some larger text”, then yes, you have to use the escapeRegExp helper that @cazadordeestrellas showed. Just don’t sprinkle new RegExp(...) everywhere; wrap it in a util and reuse.


3. Don’t chain replace for many tokens

I see a lot of stuff like:

text = text.replace('NAME', userName);
text = text.replace('DATE', dateStr);
// etc.

That looks harmless, but:

  • It can accidentally re-hit previously inserted text.
  • It is inconsistent when your replacements contain your own tokens.
  • It’s slow and messy.

Use a single pass with a map and a regex that only encodes the allowed keys:

const map = {
  NAME: 'Alice',
  SCORE: '100',
  DATE: '2026-01-23'
};

const result = text.replace(/\b(NAME|SCORE|DATE)\b/g, m => map[m]);

If keys are dynamic:

const keys = Object.keys(map).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const re = new RegExp('\\b(' + keys.join('|') + ')\\b', 'g');
const result = text.replace(re, m => map[m]);

Yes, that looks “regexy”, but it’s predictable and avoids subtle inconsistencies from multiple replaces.


4. Be careful with $ in the replacement

This is a sneaky one that often looks like “inconsistent replacement”.

In the replacement string:

  • $& = whole match
  • $1, $2 = capture groups
  • $$ = literal $

So:

'price: X'.replace(/X/, '$5'); // -> 'price: ' + empty + ' (if no group 5)'
'price: X'.replace(/X/, '$$5'); // -> 'price: $5'   ✅

If your dynamic replacement is something like 'Total: $10' and you do:

text.replace(regex, 'Total: $10');

then $1, $10, etc can get interpreted as groups. To be safe for arbitrary text, use the function form:

text.replace(regex, () => replacementString);

Then the replacement is taken literally, no $ magic.


5. When “only the first match” happens even with /g

One less-obvious case: if you mutate the string you’re matching on, or use the same global regex in several contexts:

const re = /foo/g;
let m;

while ((m = re.exec(text))) {
  // if you change text length here, position in re gets weird
}

For simple .replace, this is usually fine, but if you reuse the same RegExp object across calls, refernce position is kept:

const re = /foo/g;

text1.replace(re, 'bar'); // re.lastIndex now > 0
text2.replace(re, 'bar'); // starts at lastIndex, first matches can be skipped

Fix: either use a literal /foo/g each time, or manually reset re.lastIndex = 0, or just use new RegExp(pattern, 'g') whenever you call replace.


6. A stable pattern for “dynamic text on page”

If your page has placeholders like:

<span data-key='name'>NAME</span>
<span data-key='score'>SCORE</span>

you might actually want to avoid String.prototype.replace altogether:

document.querySelectorAll('[data-key]').forEach(node => {
  const key = node.getAttribute('data-key');
  if (key in data) node.textContent = data[key];
});

No regex, no escaping, no /g. Just DOM operations. For many UI cases, this is a lot less fragile than hunting text with regex.


So, to get consistent behavior:

  • If you just want “replace all occurrences of this literal text”: use replaceAll.
  • If you want template-like behavior: use a fixed regex that describes your placeholder syntax and a callback.
  • If you must build regex from user input: always escape and strongly prefer the callback form to avoid $ weirdness.
  • Don’t reuse global regex objects across multiple replace calls without resetting lastIndex.

Once you pick one of those patterns and stick to it, the “sometimes only the first match, sometimes it breaks” thing usually disappears.

Skip the regex gymnastics for a moment and look at where you are doing the replacement and what you are replacing.


1. Text replacement vs DOM replacement

If your page text is “dynamic” because it sits in the DOM, using string.replace on large HTML chunks is usually the real source of inconsistency.

Instead of:

document.body.innerHTML = document.body.innerHTML.replace(/NAME/g, userName);

do:

document.querySelectorAll('[data-token]').forEach(node => {
  const key = node.dataset.token;
  if (key in data) {
    node.textContent = data[key];
  }
});

Pros:

  • No regex, no escaping
  • You only touch actual text nodes you care about
  • No accidental replacement inside tags, attributes or scripts

Cons:

  • Requires you to mark up elements with attributes like data-token
  • A bit more setup than a one-liner replace

This pattern is usually more robust for “update dynamic text on my page” than throwing patterns at entire HTML strings.


2. Keep “template format” and “data format” separate

Where I slightly disagree with leaning too much on replaceAll: the moment you want any structure (like {{name}} or %NAME%), it is cleaner to define a template syntax and never let raw user text define your pattern.

Example approach:

const template = 'Hello {{name}}, your score is {{score}}.';
const data = { name: 'Alex', score: 95 };

const result = template.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
  return Object.prototype.hasOwnProperty.call(data, key)
    ? String(data[key])
    : match; // leave untouched if missing
});

Advantages over a bunch of replaceAll calls:

  • One pass, predictable behavior
  • Safe when data values contain braces, regex chars or dollar signs
  • Missing keys do not explode the template

3. Avoid “self-collisions” in repeated replacements

A subtle inconsistency shows up when your replacement text contains the search text.

Example:

let text = 'A';
text = text.replace(/A/g, 'AB'); // 'AB'
text = text.replace(/B/g, 'BA'); // 'AB A' type cascades can happen in more complex chains

If your placeholder values can include other placeholders or the same substrings, multiple sequential .replace calls become non deterministic.

To avoid that:

  1. Do a single replace pass with a callback.
  2. Or first resolve data in an intermediate object, then render once.

4. Do not reuse global regex objects across calls

This one bites people and feels “inconsistent”, even if your code looks correct:

const re = /foo/g;

text1.replace(re, 'bar');
text2.replace(re, 'bar'); // may skip initial matches

re keeps lastIndex. Use either:

text1.replace(/foo/g, 'bar');
text2.replace(/foo/g, 'bar');

or:

function replaceFoo(str) {
  return str.replace(/foo/g, 'bar');
}

Fresh regex each time, no shared state.


5. About competitors’ points

  • @sterrenkijker is absolutely right about replaceAll being simpler for pure literal text, although I would still push a template-style approach once you have multiple tokens.
  • @cazadordeestrellas highlighted escaping and the $ pitfalls very well. I would extend that by saying: if you ever pass arbitrary text into new RegExp, consider it a code smell in UI templating unless you have a very specific search feature.

6. Pros & cons of using String.prototype.replace for dynamic UI text

Pros:

  • Built in, no extra dependencies
  • Flexible enough to implement templating, search highlighting, etc.
  • Can be efficient when used in a single pass with callbacks

Cons:

  • Easy to mix up regex and literal behavior
  • Global regex state and $ replacement semantics cause surprising bugs
  • Not great for operating directly on full HTML strings (risk of breaking markup)

Used carefully, String.prototype.replace is powerful for dynamic text on the page, but for anything more than a couple of straightforward substitutions, treating your content as structured (DOM or templates) and doing one controlled pass tends to eliminate the “inconsistent” outcomes you are seeing.