At Prosopo, we're committed to making Procaptcha integration as seamless as possible for our clients. This commitment drives us to develop native integrations for all major frameworks and CMS platforms — a challenging but essential effort that enables our clients to implement Procaptcha natively within their preferred environments.
Recently, we completed integrations for four popular reactive frameworks: React, Vue, Svelte, and Angular. While these frameworks share fundamental reactive programming concepts, each employs distinct terminology and implementation approaches. Frequently switching between these frameworks during development led to occasional confusion with their specific terms and patterns.
To address this challenge, we created a cheatsheet that organizes and categorizes the core concepts across all four frameworks. This reference has proven usefulness for our development team, and we're now sharing it publicly with some explanations in hopes that it will benefit the wider development community. This article can also serve as a starting point for learning or comparing reactive frameworks.
Reactive frameworks introduce specialized template formats and language constructs that streamline development. To transform these framework-specific features into browser-compatible code, applications must pass through a compilation process. When working with TypeScript, this means one more compilation step:
There are 2 primary ways to employ these frameworks, and both include the compiler-related part.
React, Vue, and Svelte focus on client-side rendering and component architecture, without built-in server-side capabilities like routing or API handling. For comprehensive application development, each framework has a recommended full-stack companions:
Framework | Full-stack companions |
---|---|
React | The recommended list includes Next.js, React Router and Expo |
Vue | Nuxt - official one, made by the framework team |
Svelte | SvelteKit - official one, made by the framework team |
Angular | Built-in solution |
When you set up Next, Nuxt, or SvelteKit, it already includes both the respective reactive framework and the necessary build configurations.
Angular comes as a comprehensive framework that already integrates these server-side capabilities out-of-the-box. But it also means that you can't use it for frontend-only things.
Reactive frameworks are frequently used for frontend-only development or to enhance existing projects. One of their greatest strengths is versatility — React, Vue, and Svelte can be seamlessly integrated into virtually any web project, regardless of the backend technology or existing architecture.
Since these frameworks (except Angular) are designed as standalone libraries with minimal assumptions about their environment, they can be incorporated into any project using modern build tools like Vite.
Vite has emerged as the industry-leading bundling solution due to its exceptional speed, developer experience, and broad adoption—including integration as the default bundler in most of the companion frameworks mentioned above.
Adding reactive framework support to an existing Vite setup requires just a simple plugin installation:
After installing these plugins, both Vite's development server and build process will automatically handle the framework-specific compilation.
Note: As we mentioned above, Angular as an all-in-one solution uses a different development approach with its official Angular CLI tool.
Initializing a reactive application requires specifying a mount point - a DOM element where the framework will render your application. This element serves as the container for the entire component tree and must already exist before mounting.
While typically it's a dedicated div with an ID like "root" or "app", the mount point can be any HTML element in the document.
Such separation allows reactive applications to coexist with other content or even other framework instances on the same page, enabling gradual adoption strategies.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./app.tsx";
const rootHtmlElement = document.querySelector("#root");
ReactDOM.createRoot(rootHtmlElement)
.render(
<React.StrictMode>
<App/>
</React.StrictMode>
);
import {createApp} from "vue";
import App from "/app.vue";
const rootHtmlElement = document.querySelector("#root");
createApp(App)
.mount(rootHtmlElement);
import {mount} from "svelte";
import App from "./app.svelte";
const rootHtmlElement = document.querySelector("#root");
mount(App, {
target: rootHtmlElement,
});
import {bootstrapApplication} from '@angular/platform-browser';
import {AppComponent} from './app/app.component';
bootstrapApplication(AppComponent);
Reactive frameworks are built around the concept of components - self-contained, reusable building blocks that encapsulate markup, styling, and behavior in a single cohesive unit.
This component-based architecture promotes code organization, reusability, and maintainability. For this goal each framework introduces its own specialized file format.
These custom formats are processed by framework-specific compilers and are supported by modern IDEs, either out-of-the-box, or through dedicated extensions, providing syntax highlighting, code completion, and error checking.
React introduces JSX - a syntax extension for JavaScript that lets you write HTML-like markup inside a JavaScript file.
It supports both TypeScript and JavaScript for component development, with corresponding file extensions reflecting the language choice:
You can also use .ts and .js files for React code that doesn't contain JSX syntax, such as utility functions or hooks.
Function-based component (recommended approach)
// called once at inclusion
interface AppProperties {
};
function App(properties: AppProperties) {
// called on both creation and every render
return (<div></div>);
}
export {App};
Class-based component (deprecated approach)
import React from 'react';
// called once at inclusion
interface AppProperties {
};
class App extends React.Component<AppProperties, AppState> {
public constructor(properties: AppProperties) {
super(properties);
// called once per component instance
}
public override render(): React.ReactNode {
// called on every component render, the response is used in the Virtual DOM
// ttps://www.freecodecamp.org/news/what-is-the-virtual-dom-in-react/
return (<div></div>);
}
}
export {App};
Usage in another component:
import App from "./app.tsx";
(<App name="my app"/>)
Note: As you see, React offers two distinct patterns for defining components:
React.Component
The React team strongly recommends function components for all new development, as they align better with React's compositional model and current best practices.
Vue employs a specialized format called Single-File Components (SFCs). Unlike React's JSX approach that interleaves markup and logic, Vue's architecture maintains clear separation of concerns within a unified file structure:
<template>
<!-- called on every component render, the response is used in the Virtual DOM -->
<!-- https://vuejs.org/guide/extras/rendering-mechanism.html#virtual-dom -->
<div></div>
</template>
<script lang="ts">
// called once at inclusion
// interface AppProperties{};
</script>
<script setup lang="ts">
// called once per component instance
const properties = defineProps < AppProperties > ();
</script>
<style scoped>
</style>
Vue components also support both JavaScript and TypeScript. For JavaScript no additional configuration needed, while for TypeScript you need to add the lang="ts" attribute to the script tag, as shown in the code above.
Usage in another component:
<template>
<App name="my app"/>
</template>
<script setup lang="ts">
import App from "./app.vue";
</script>
Svelte adopts a similar approach to Vue, employing single-file components with clear code separation:
<script module lang="ts">
// called once at inclusion
// interface AppProperties {};
</script>
<script lang="ts">
// called once per component instance
const {name}: AppProperties = $props();
</script>
<!-- called once at compilation, then the template is turned into vanilla JS -->
<!-- https://svelte.dev/blog/virtual-dom-is-pure-overhead -->
<!-- https://dev.to/joshnuss/svelte-compiler-under-the-hood-4j20 -->
<div></div>
<style>
</style>
Svelte components also support both JavaScript and TypeScript. For JavaScript no additional configuration needed, while for TypeScript you need to add the lang="ts" attribute to the script tag, as shown in the code above.
Pay attention, that unlike Vue, in Svelte templates markup goes without any wrapping tag.
Usage in another component:
<script lang="ts">
import App from "./app.svelte";
</script>
<App name="my app"/>
Angular organizes components differently from other frameworks. While still combining markup, styles, and logic conceptually, Angular uses standard TypeScript files rather than introducing a custom file extension.
The framework leverages TypeScript decorators feature — particularly @Component
— to define metadata. This decorator-based approach forms the foundation of Angular's component system.
import {Component, Input} from "@angular/core";
// called once at inclusion
@Component({
selector: "app-root",
imports: [],
// keept up to date with the Incremental DOM technique https://blog.nrwl.io/understanding-angular-ivy-incremental-dom-and-virtual-dom-243be844bf36
template: "<div></<div>",
// templateUrl: "./app.component.html",
styles: "",
// styleUrl: "./app.component.css",
// inputs: Object.keys({} as AppProperties) as (keyof AppProperties)[],
})
export class AppComponent {
@Input({required: true})
name!: string;
constructor() {
// called once per component instance
}
}
Angular component templates can be defined either as inline HTML strings using the template property or as separate HTML files referenced through the templateUrl property.
Usage in another component:
import {AppComponent} from "./appComponent";
@Component({
selector: "another-component",
imports: [AppComponent],
template: `<app-component />`,
})
Reactive frameworks handle markup initialization and updates automatically, eliminating the need for manual DOM manipulation. While this reactivity happens seamlessly in the background, there are scenarios where we need to execute custom code at specific moments in a component's lifecycle.
Example of use cases
To accomplish these tasks, frameworks provide "lifecycle hooks" - special methods or functions that execute at well-defined moments during a component's existence. While each framework uses different terminology, the fundamental concept remains the same across all of them.
Function-based component:
import React, {useEffect} from "react";
function App(): React.ReactNode {
useEffect(() => {
// called once after the initial render
}, []); // empty dependency array means it only runs once
useEffect(() => {
// called after every render
}); // no dependency array means it runs after every render
}
Adding the useEffect() hook inside a functional component binds the listener to the chosen component's lifecycle event.
Class-based component:
import React from "react";
class App extends React.Component {
public override componentDidMount(): void {
// called once after the initial render
}
public override componentDidUpdate(): void {
// called after every UI re-render (second and next renders)
}
}
In the class-based components, hook listeners are added by overriding parent class methods.
In Vue's Composition API, lifecycle hook listeners are registered within the script setup block of a Single File Component.
<script setup lang="ts">
import {onMounted, onUpdated} from "vue";
onMounted(() => {
// called once after the initial render
});
onUpdated(() => {
// called after every re-render (second and next renders)
});
</script>
In Svelte, hooks listeners are also registered within the script block:
<script lang="ts">
import {onMount, tick} from 'svelte';
onMount(() => {
// called once after the initial render
});
$effect.pre(() => {
//called before every render
tick().then(() => {
// called after every render
});
});
</script>
Svelte component lifecycle Docs
In Angular, hooks are class methods with special names:
export class DemoComponent {
ngOnInit() {
// called once after the initial render
}
}
Angular component lifecycle Docs
Reactive variables are the core concept behind reactive frameworks. These specialized JavaScript values trigger automatic markup updates whenever data changes, eliminating the need for manual DOM manipulation.
When a reactive variable's value changes, the framework intelligently updates only the affected parts of the interface, creating a seamless connection between application state and what users see on screen.
This declarative approach allows us to focus on application logic itself rather than on keeping the interface in sync with changing data.
useState is a function that accepts an initial value and returns an array with two elements: a pointer on the current (and always actual) state value and a function to update it.
import {useState} from "react";
const [stateValue, setStateValue] = useState("initial");
const someAction = () => {
setStateValue("newValue");
}
// ...
<div>{stateValue} < /div>
Both ref() and reactive() functions create variables that trigger the UI updates when their values change, but they handle different data types and have different access patterns:
<script setup lang="ts">
import {ref, reactive} from "vue";
const stateValue = ref("initial");
const deepStateValue = reactive({count: 0})
const someAction = () => {
stateValue.value = "newValue";
deepStateValue.count = 1;
}
// ...
</script>
<template>
<div>{{ stateValue }}</div>
<div>{{ deepStateValue.count }}</div>
</template>
$state is a built-in template token (called a rune in Svelte) that creates reactive variables.
Unlike React's useState() or Vue's ref(), Svelte runes don't require imports as they're part of the language's syntax. They function similarly to built-in JavaScript keywords.
<script lang="ts">
let stateValue = $state("initial");
const shallowStateValue = $state.raw({
plain: "text",
});
const someAction = () => {
stateValue = "newValue";
};
</script>
<div>{stateValue}</div>
<div>{shallowStateValue.plain}</div>
signal() is an Angular's function that creates a getter/setter pair for reactive variable.
import {signal} from '@angular/core';
const stateValue = signal("initial");
const someAction = () => {
stateValue.set("newValue");
};
// ...
// <div>{{stateValue()}}</div>
Computed variables represent values derived based on other reactive variables. These computed values maintain a live connection to their dependencies, automatically recalculating only when needed and triggering UI updates with optimal performance. Each framework offers distinct approaches with different syntax but similar underlying concepts.
React's useMemo() function creates memoized values that only recalculate when dependencies change. This function requires explicitly declaring dependencies in an array:
import {useState, useMemo} from "react";
const [stateValue, setStateValue] = useState(1);
const computedValue = useMemo(
() => stateValue * 2, // computing function
[stateValue] // list of the reactive variables we depend on
);
// <div>{computedValue}</div>
Unlike React, Vue's computed() function automatically tracks dependencies used within the calculation function, eliminating the need for manual dependency lists.
<script setup lang="ts">
import {ref, computed} from "vue";
const stateValue = ref(1);
const computedValue = computed(
() => stateValue.value * 2 // computing function
);
// note: Vue implicitly picks up function dependencies
</script>
<template>
<div>{{ computedValue }}</div>
</template>
Svelte's $derived rune offers two distinct syntaxes: a concise expression format for simple calculations and a function-based approach for more complex logic.
<script lang="ts">
const stateValue = $state(1);
const computedValue = $derived(stateValue * 2); // computing expression
const complexComputedValue = $derived.by(() => {
return computeSomething(stateValue); // computing function
});
// note: Svelte implicitly picks up function dependencies
</script>
<div>{computedValue}</div>
<div>{complexComplutedValue}</div>
Like other Svelte runes, no imports are required as these are built into the language.
Angular's computed() function works exactly the same as Vue's computed() function:
import {signal, computed} from '@angular/core';
const stateValue = signal("initial");
const computedValue = computed(() => stateValue() + " prefix")
// note: Angular implicitly picks up function dependencies
// <div>{{computedValue()}}</div>
While computed values derive new data from reactive state, we often need to perform side effects when state changes - such as logging, API calls, or even extra DOM manipulations.
Each framework provides specialized APIs for executing code in response to reactive variable changes while maintaining proper cleanup and dependency tracking.
Like useMemo(), useEffect() requires explicit dependency arrays:
import {useEffect} from "react";
const [stateValue, setStateValue] = useState("initial");
useEffect(() => {
console.log('stateValue has changed', stateValue); // tracking function
},
[stateValue] // list of the reactive variables we depend on
);
Vue's watchEffect() automatically tracks reactive dependencies accessed within the callback function:
<script setup lang="ts">
import {watchEffect} from "vue";
const stateValue = ref("initial");
const {stop, pause, resume} = watchEffect(() => {
console.log('stateValue has changed', stateValue.value); // tracking function
});
// note: Vue implicitly picks up function dependencies
</script>
watchEffect() returns control objects that allow manual stopping, pausing, and resuming of the watcher - providing fine-grained control over effect execution.
Svelte's $effect rune also automatically tracks dependencies used within the function body:
<script lang="ts">
const stateValue = $state("initial");
$effect(() => {
console.log('stateValue has changed', stateValue.value); // tracking function
});
// note: Svelte implicitly picks up function dependencies
</script>
Unlike other frameworks, Angular effects always run at least once during initialization:
import {signal, effect} from '@angular/core';
const stateValue = signal("initial");
effect(() => {
// distinct behavior: this function is called at least once,
// so includes the initial value assignment
console.log("state value was set: " + stateValue());
});
// note: Angular implicitly picks up function dependencies
While reactive frameworks handle most DOM interactions automatically, sometimes direct access to DOM elements is necessary for tasks like focus management, measuring dimensions, or integrating with third-party libraries.
Each framework provides mechanisms to obtain references to rendered DOM elements while maintaining the integrity of their rendering systems.
These DOM references bridge the declarative world of components with imperative DOM APIs, enabling operations that can't be expressed through standard reactive patterns.
React provides a useRef() function to create mutable reference objects, which are matched with HTML elements using the ref attribute.
Function-based component
import {useRef} from 'react';
function MyComponent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (<input ref={inputRef} onClick={handleClick}/>);
}
Access to the actual DOM element happens through the .current property.
Class-based component
import React, {useRef} from 'react';
class App extends React.Component {
private readonly elementRef: React.RefObject<HTMLDivElement>;
constructor(properties) {
super(properties);
this.elementRef = React.createRef();
}
public override render(): React.ReactNode {
return <div ref={this.elementRef} onClick={this.focus.bind(this)}/>;
}
public focus(): void {
this.elementRef.current.focus();
}
}
Vue uses the ref attribute in templates and the useTemplateRef() function in the script to establish connections to DOM elements.
<script setup lang="ts">
import {useTemplateRef, onMounted} from 'vue'
// the first argument must match the ref value in the template
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input"/>
</template>
Template refs become available only after the component is mounted.
Svelte uses the bind:this directive to create a direct reference to DOM elements.
<script lang="ts">
let canvas: HTMLCanvasElement;
$effect(() => {
const ctx = canvas.getContext('2d');
drawStuff(ctx);
});
</script>
<canvas bind:this={canvas}></canvas>
The variable becomes populated with the actual DOM element once it's mounted.
Angular uses the @ViewChild decorator to query and access elements from the template. Access to the native element happens through the nativeElement
property.
import {afterRender, ElementRef, inject, ViewChild} from "@angular/core";
export class AppComponent {
wholeElementReference = inject(ElementRef);
@ViewChild("child-element-id") childElementReference!: ElementRef;
constructor() {
afterRender(() => {
this.wholeElementReference.nativeElement.classList.add("loaded");
this.childElementReference.nativeElement.classList.add("loaded2");
});
}
}
Reactive components are designed for reusability. Not only between pages of the same project, but even across different projects. Instead of duplicating code (which creates maintenance challenges), you can package components into standalone libraries that can be imported into any compatible application.
This approach provides a single source of truth, enables version management, and simplifies updates across multiple projects that use the same components.
Creating reusable component libraries requires different build configurations than standard applications. While applications bundle all dependencies together, component libraries need to properly externalize the framework to avoid duplication and versioning conflicts.
Each framework has specific tooling and patterns for creating publishable component packages.
Unlike the app bundling case, to make a reusable components library we need to compile only the component-related code, while keeping React as a peer (external) dependency, so it'll be picked up from the target application.
With Vite, using the React plugin:
Similar to React, a Vue components library needs to compile only the component-related code, while keeping Vue as a peer (external) dependency, so it'll be picked up from the target application.
With Vite, using the Vue plugin:
To be available for reusing, Svelte components must be packaged, which implies compiling TS-related resources, while keeping .svelte component files as is.
Note: In Svelte v5, you can't compile .svelte component into standalone JS with Svelte being externalized (like in case with React or Vue), so packaging is the only way to create a components' library.
Using the official @sveltejs/package:
Packaging does the following:
After that, you can publish your package at npmjs, and your Svelte components will be available for reusing.
To be available for reusing, components package must be made in the Angular package format.
Using the official ng-packagr:
Packaging does the following:
After that, you can publish your package from the dist folder at npmjs, and your Angular components will be available for reusing.