Context-aware output encoding is the primary defense. When you put untrusted data into HTML, escape the five characters that matter (< > & " '). React, Angular, and Vue do this by default for text, which is why frameworks cut XSS dramatically. The danger returns the moment you reach for dangerouslySetInnerHTML, v-html, or innerHTML.
If you genuinely need to render user HTML (a rich-text comment, for example), run it through a sanitizer like DOMPurify that strips script tags and event handlers while keeping safe markup. Do not write your own blocklist; attackers have decades of bypasses for naive filters.
Content-Security-Policy is the second layer. A header like Content-Security-Policy: script-src 'self' tells the browser to refuse inline scripts and only run JavaScript from your own domain, so an injected <script> simply does not execute. Set HttpOnly on session cookies so that even if a script runs, it cannot read the cookie via document.cookie.