The beginning of the year, is a period full of motivation and New Year’s resolutions. But before we move forward, it’s worth taking a look back and summarizing the most important developments in the JavaScript ecosystem in 2022.
1. TypeScript in 2022
Over the course of this year, I have repeatedly said that TypeScript has matured, and as a result has become as dull as dishwater. Looking back at 2022, it turns out that even in such a mature and stable ecosystem, some interesting things can happen.
are types coming to JavaScript?
In early March, Microsoft, the company behind TypeScript, prepared a proposal to the JavaScript standard. Its content focused on enriching the syntax with types similar to those known from TypeScript. In order to maintain backward compatibility and not change the fundamentals of the language, Microsoft proposed that the interpreter would treat types the same as comments (ignore them completely). This will keep types out of the app logic but will make the code more readable and developers much more efficient by hooking up the language server and type checker to their IDE.
The behavior shown above may be deceptively similar to the current JSDoc behavior. However, the new proposal has significant advantages over this solution. First, Microsoft’s proposed format is much more concise and readable than JSDoc. Secondly, the capabilities of JSDoc are severely limited (it is difficult to define at least generics using it).
// —-----------------------------------
// JSDoc
// —-----------------------------------
/**
* @param {string} p1 - A string param.
* @param {string=} p2 - An optional param (Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @return {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4) {
/* ... */
}
// —-----------------------------------
// JavaScript with type annotations
// —-----------------------------------
function stringsStringStrings(p1: string, p2?: string, p3?: string, p4 = "test"): string {
/* ... */
}
Do types in JavaScript mean the end of TypeScript? It depends. In the early stages, types in JavaScript are likely to be much more primitive than those known from JavaScript. TypeScript also offers functionality based on code transpilation, such as enums, for example. In this respect, JavaScript is unlikely to catch up with TypeScript any time soon.
Since March, work on Proposal has not progressed much and it is stuck in Stage 1 (the whole process consists of 4 stages). However, Proposal is being taken care of by Microsoft, so I think the topic will still be back in 2023.
New TypeScript features
Over the past year, we’ve got 3 new versions of TypeScript: 4.7, 4.8, and 4.9. However, there’s no point in going into each version, because…. they’re as boring as dishwater. The developers working on TypeScript have spent a ton of time optimizing performance and better handling more edge cases. However, among the new features, one new functionality shines – the satisfies
operator.
To explain how the new operator works, I will use an example from Microsoft’s documentation. Suppose we need to add types to the following code:
// Each property can be a string or an RGB tuple.
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
// We want to be able to use array methods on 'red'...
const redComponent = palette.red.at(0);
// or string methods on 'green'...
const greenNormalized = palette.green.toUpperCase();
The first thing that may come to mind is defining the Color
type and using the Record
type. Unfortunately, in such case, we are forced to perform a dangerous cast operation:
type Color = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
type Palette = Record<Color, string | RGB>
const palette: Palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
// We want to be able to use array methods on 'red'...
const redComponent = (palette.red as RGB).at(0);
// or string methods on 'green'...
const greenNormalized = (palette.green as string).toUpperCase();
A workaround for this problem is a new satisfies operator that will validate the type at the time of assignment, but will not affect the type being evaluated by TypeScript. It sounds complicated, but a simple example gives a good idea of what is involved
type Color = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
type Palette = Record<Color, string | RGB>
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
} satisfies Palette;
// Both of these methods are still accessible!
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();
// —-----------------------------------
// Example errors caught by satisfies
// —-----------------------------------
const spelloPalette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255] // Such typos are now caught
} satisfies Palette;
// Missing properties are now caught
const missingColorPalette = {
red: [255, 0, 0],
bleu: [0, 0, 255]
} satisfies Palette;
const wrongColorTypePalette = {
red: [255, 0], // Such typos are now also caught
green: "#00ff00",
bleu: [0, 0, 255]
} satisfies Palette;
I have already found usages of the new operator in SvelteKit, and I am sure that it will be useful in more than one project.
The future of TypeScript
We will start next year with the release of TypeScript 5.0 in early March. It’s worth noticing that TypeScript does not follow Semantic Versioning. This means that the new major version should bring a bunch of big and exciting changes, but not necessarily breaking changes. Buzzing around the TypeScript repository, we can catch a glimpse of a few upcoming features. Firstly, TypeScript has been rewritten to modules (until now it was based on namespace), reducing the package by 25% and speeding up compilation by 10%. Decorators with functionality that does not go beyond the compiler are to become compatible with decorators in the 3rd stage of JavaScript standardization ( standardization consists of 4 stages, it is very rare that functionality in the 3rd stage is removed). TypeScript is also working on native import support for any file type. As you can see, there is a lot of it, and all signs on heaven and earth indicate that 2023 will definitely be a more interesting year for TypeScript than 2022.
2. React in 2022
This year we finally got to see it! Two years after the release of React 17, the next version of the framework from Meta has seen the light of day. Unlike the previous version, which from an API point of view did not introduce significant novelties, this one is full of exciting functionalities.
React 18 and Concurrent Mode
If you’ve never heard of concurrent mode in React then you’re probably scratching your head right now trying to combine concurrency with single-threaded JavaScript. As it turns out, concurrency here is all about the ability to queue renders, prioritize them and add the ability to abort a render in progress.
A flagship example of a case where we would benefit from concurrent mode is interacting with a search field over a long list of items. When a user presses a key on the keyboard, even the slightest delay in updating the text field gives the user a sense that something is wrong. In contrast, when it comes to search results, a slight delay is even natural. In such a situation, it is clear that there is a priority and non-priority renderer to deal with. Moreover, if the user is able to write faster than React renders components, then rendering intermediate search states is undesirable. In this case, rendering cancellation comes into play, which will cancel the previous render whenever a newer version of the component appears.
Some of you will probably point out that a similar effect could be achieved by with the debounce function. The difference between the pre-React 18 techniques and the concurrent mode is that in the first option the rendering is done synchronously and the user is not able to interact with the page during rendering. In concurrent mode, a lower-priority render will be interrupted when a higher-priority render appears in the queue. By aborting the lower-priority render, the page should appear much more responsive.
Of course, the possibilities of using concurrent mode do not end here. Thanks to this feature, it will be possible to render components off-screen with low priority (so that already visited pages can be cached, or to render in advance the pages that the user is most likely to visit next). Thanks to the use of prioritization, such actions will not affect the responsiveness of the interface, as they will simply be done at a lower priority.
Enough theory – let’s get to the meat. Low-priority updates must be wrapped in the startTransition
method, which we can access via the useTransition()
hook.
export default function App() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
function handleClick() {
startTransition(() => {
setCount((c) => c + 1);
});
}
const advancedCounter = useMemo(
() => <AdvancedCounter count={count} />,
[count]
);
return (
<>
{/*Immediately after clicking the button */}
{/*div changes opacity until low priority render finishes*/}
<div style={{ opacity: isPending ? 0.8 : 1 }}>
{/*Component below will update only after all priority renders finish.*/}
{/*If user spams increase button only last render will be visible to user.*/}
{advancedCounter}
<button onClick={handleClick}>Increase counter</button>
</div>
</>
);
}
The hook useDeferredValue
has also been added to the API. If this hook is called in a priority render it will return the value from the previous render. This way the reference will not change, which will make rendering faster as we will not re-render a part of the VirtualDOM tree. When the priority render is done, a low-priority render will be scheduled in which the hook will return the new value and the updated component will be rendered.
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((c) => c + 1);
}
const deferredCount = useDeferredValue(count);
const advancedCounter = useMemo(
() => <AdvancedCounter count={deferredCount} />,
[deferredCount]
);
return (
<>
<div>
{/*This component will update only after all priority renders finish.*/}
{/*If user spams increase button only last render will be visible to user.*/}
{advancedCounter}
<button onClick={handleClick}>Increase counter</button>
</div>
</>
);
}
What is coming up to React in 2023?
We are unlikely to see another major React release in 2023. This doesn’t change the fact that we know what the React team is currently working on. We can also expect that some of these functionalities will not wait for React 19 and will make their way into the next versions of React 18.
Probably the biggest and most anticipated functionality that is currently being developed is React Server Components. You can read more about them in the next section of this post. However, they are not the only functionality currently under development. The team is working on a <Offscreen />
component, which will allow rendering off the screen without attaching elements to DOM. Another functionality under development is a compiler that automatically adds useMemo
and useCallback
wherever needed.
3. Angular in 2022
There was really a lot going on in the Angular ecosystem in 2022. We started the year by saying goodbye to angular.js. Soon after, the first experimental implementation of Standalone Components was presented along with Angular 14. Throughout the year, the team from Google worked up a sweat and Angular 15 delivered us a stable version of this long-awaited functionality.
The end of angular.js support
Support for angular.js was to be withdrawn as early as July 2021. Shortly after the pandemic broke out due to the perturbations it caused, Google decided to give companies an additional six months of support. The additional time came to an end at the beginning of 2022. That’s when we officially said goodbye to angular.js, (also known as Angular 1.x). Of course, the framework continues to be available from npm, but Google has no further plans for security patches.
Angular 14 and15
The past year in the Angular ecosystem has passed under one banner: Standalone Components. We are not only talking about functionality per se, but also about the mass of work done around it. To make it possible to create applications without modules, the team prepared an inject()
function, an API for function guards, a new API for initializing applications and much more.
Angular modules have little in common with the modules known from JavaScript. Native modules allow you to split your application into multiple files and manage the APIs you provide. Angular modules are intended to provide dependency configuration for Dependency Injection. In Angular, every component or directive must be part of some module, making them the most atomic element of the framework. Interestingly, modules didn’t make their way into Angular until version 2.0.0-rc.5, and were a response to problems with publishing Angular libraries in npm. Due to the fact that the framework was already at the Release Candidate stage, the whole solution was created at an accelerated pace. As it usually happens with such solutions, it stayed with us for longer.
Over the years, good practices around Angular have evolved, and the SCAM (Single Component Angular Module) scheme is now the most common design pattern. The only drawback of this approach is a large amount of boilerplate code.
@Component({
selector: 'vived-my-component',
template:`
<div>
<h2>Today is {{today | date}} </h2>'
<CustomComponent />
</div>
`
})
export class MyComponent {
readonly today = new Date();
}
@NgModule({
imports: [ CommonModule, CustomComponentModule ],
declarations: [ MyComponent ],
exports: [ MyComponent ],
})
export class MyComponentModule { }
Starting with Angular 15, most modules can be generated automatically by adding a standalone parameter to the decorator. With this one simple trick, Angular has undergone a very effective diet.
@Component({
selector: 'vived-my-component',
standalone: true.,
imports: [ CommonModule, CustomComponent ]
template: `
<div>
<h2>Today is {{today | date}} </h2>'
<CustomComponent />
</div>
`
})
export class MyComponent {
readonly today = new Date();
}
Actually, almost all the functionality added to Angular in 2022, focused on slashing unnecessary boilerpate. For example, look at how the Router API was slimmed down.
// This is how you bootstrap application with modules
@NgModule({
declarations: [AppComponent],
imports: [
RouterModule.forRoot(
[
{ path: '/home', component: HomeComponent },
{ path: '**', redirectTo: '/home' },
],
{
preloadingStrategy: PreloadAllModules,
}
),
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
bootstrap: [AppComponent],
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule)
// This is how you bootstrap application without modules
bootstrapApplication(AppComponent, {
providers: [
provideRouter([
{ path: '/home', component: HomeComponent },
{ path: '**', redirectTo: '/home' },
], withPreloading(PreloadAllModules)),
provideHttpClient(withInterceptors([AuthInterceptor]))
]
});
Angular 16 and 17
Looking at the official Angular roadmap and GitHub issues, we can already guess what the Angular team will focus on in 2023. First of all, the past year revolved around server-side rendering, and this has not escaped the team’s attention. So in 2023 we can expect a return to the topic and some interesting RFCs in this area. Secondly, the next big topic is to allow the zone.js library to be cut out of Angular’s dependencies. As a reminder, this is a somewhat overblown library that patches most of the browser’s APIs and allows Angular to “magically” detect changes in the application.
4. Vue in 2022
In 2022, only one major version of Vue was released. Surprisingly, it was not another Vue 3, but the final version of Vue 2. In addition, we finally got a server-side rendering of the real thing in the form of Nuxt 3.
Vue 2.7
Vue 2.7 is the latest release of Vue 2 and will be supported until the end of 2023. In the nutshell, this version adds the most important functionality from Vue 3 to Vue 2. With this approach, incremental migration should become much simpler. Will this actually be the case? We’ll find out at the end of 2023, when we’ll see how many projects failed to make the migration in time.
The most significant functionality that came to Vue 2.7 is the Composition API, by many called React Hooks for Vue. This API consists of 3 components: Reactivity API (ref
and reactive
), Lifecycle Hooks (onMounted
/onUnmounted
) and Dependency Injection (provide
/inject
). Importantly, both the behavior and types of all new functionality are 100% compatible with Vue 3.
// Old Options API
<script>
export default {
data() {
return {
name: 'John',
};
},
mounted() {
console.log(`Hello ${this.name}`);
},
};
</script>
<template>
<p>Hello {{ name }}!</p>
</template>
// New Composition API
<script setup>
const name = ref(0);
onMounted(() => {
console.log(`Hello ${name.value}`);
});
</script>
<template>
<p>Hello {{ name }}!</p>
</template>
Vue 3.3
Vue 3.3 was supposed to be released at the end of 2022, but its release has been postponed until 2023. However, we can already take a look at what’s in store for Vue next year.
<Suspense />
. It will enable the rendering of a temporary component until all asynchronous components in the tree are loaded.
<Suspense>
<!-- component with nested async dependencies -->
<Dashboard />
<!-- loading state via #fallback slot -->
<template #fallback>
Loading...
</template>
</Suspense>
The second highly anticipated feature is Reactivity Transform, a series of improvements to the Composition API. All methods from the Reactive API (including ref
and computed
) will get versions preceded by $
. These will be macros that will allow easier access to data, at the cost of an additional compilation step.
// Before Vue 3.3
<script setup>
import { ref } from 'vue'
let count = ref(0)
console.log(count.value)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
// Vue 3.3
<script setup>
let count = $ref(0)
console.log(count)
function increment() {
count++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
During Vue Amsterdam, Evan You shared the team’s long-range plans. As he points out, for now, these plans are in their infancy and are still subject to change. Once Vue 3.3 is completed, the team is considering focusing on a new compilation strategy similar to that known from SolidJS. As a reminder, SolidJS is a framework that is deceptively similar to React, but boasts better performance due to the abandonment of the Virtual DOM. Optional cutting out the Virtual DOM from Vue could significantly slim down the package size and would save the memory needed to store the virtual component structure.
5. Server Side Rendering
Ah, what a year it’s been for server-side rendering! Almost every major framework received either a sizable update on this topic or a completely new library with a ton of new features (Vue – Nuxt 3, Svelte – SvelteKit, React – React Server Components, Remix, Next.js 13). There were also a lot of innovative solutions on the market, not connected to existing frameworks. Here I have to mention Qwik, which is trying to revolutionize the approach to hydratio, and Astro, which popularized the innovative Dynamic Islands architecture.
React Server Components and Next.js 13
It’s been more than two years since React Server Components were introduced to the world by Dan Abramov. If you don’t know the concept, in a nutshell, React would allow rendering a single component on the server side and sending HTML code to the client. To me, this approach strongly resembles on PHP, because it is possible to make database queries directly from Reac Components.
// Note.js - Server Component
import db from 'db';
async function Note(props) {
const note = await db.posts.get(props.id);
return (
<div>
<h2>{note.title}</h2>
<section>{note.body}</section>
</div>
);
}
Unfortunately, React Server Components also have their limitations. Components of this type cannot store state (useState
is forbidden), cannot be imported by client-side rendered components, and require a meta framework like Next.js, or Gatsby to work. This topic is extremely intricate and deserves a separate post on our blog. If you would like to learn more, I highly recommend this article by @chungwu.
Although React Server Components are still in the alpha phase, the first frameworks that use them (Next.js, Gatsby) have already been released. In particular, Next.js 13 resonated with the community, because it based its new architecture entirely on React Server Components. An interesting side effect of such an architecture is that as long as you don’t explicitly define a Client Componet, the entire application will render only on the server side.
Qwik
Qwik turns the paradigms of all modern server-side rendering tools upside down by undermining the legitimacy of the hydration process. Hydration is a client-side process during which a server-side rendered application loads JavaScript code and then repeats all initialization logic. Only after the hydration process is complete does the application become responsive. Thanks to this tactic, the client sees the target content right away and after a while can interact with it.
The alternative that Qwik offers is resumable execution. This paradigm saves the state of an application on the server side in such a way that, on the client side, code execution can be resumed immediately. Sound complicated? Fortunately, the complicated logic is stealthily hidden under an accessible API that hides all the magic from the programmer.
All right, but what if I really want to know how the magic works underneath? I won’t undertake to explain how the framework’s guts work, and it’s best if you reach for the documentation yourself. In a nutshell, the component code is compiled into separate packages, and the application state itself is stored in the DOM in “JSON on steroids.” The rendered HTML is sent to the client, along with 1KB of code from Qwik itself. This one immediately downloads the code of all the necessary components, but instead of linking them immediately to the DOM it waits for the user’s first interactions.
A the end I would like to add a little mention on why it’s worth following Qwik in the future. A true team of all-stars is responsible for its development – Miško Hevery (creator of anguar.js), Manu Almeida (creator of Gin and Stencil) and Adam Bradley (creator of Ionic and Stencil). If anyone is going to revolutionize Frontend, it’s a team of such veterans.
Astro
Astro is a framework that popularized the so-called island architecture. It assumes that on the page we create, among the sea of static content (e.g. navigation menu, article) there are small islands of interactive components (e.g. newsletter sign-up form). This approach allows us to render the entire application on the server side, and only a small component-island code is sent to the client.
Astro is equipped with a very advanced mechanism for defining when the code of dynamic components should be downloaded and when they should be hydrated. Among the available options we have to download only when the component is visible on the screen or hydrating only when the appropriate media query is met.
Astro also stands out from the competition because it is framework independent. It is the first library in a very long time that really does not favor any solution, and on top of that it allows you to mix them with each other.
—
import HomeLayout from '../layouts/HomeLayout.astro'
import MyReactComponent from '../components/MyReactComponent.jsx';
import MySvelteComponent from '../components/MySvelteComponent.svelte';
const data = await fetch('API_URL').then(r => r.json());
---
<HomeLayout>
<MyReactComponent client:load name={data.name}>
<MySvelteComponent avatar={data.avatar}/>
</MyReactComponent>
</HomeLayout>
6. Node.js, Deno and Bun in 2022
A year ago it seemed that the situation of JavaScript runtimes was rather stable. Node dominated with a strong hand, and Deno was meticulously expanding its niche. However, in the middle of 2022, there was a real earthquake as Bun entered the game with $7M in funding.
Bun
Bun is a drop-in alternative to Node.js and Deno. While Deno focuses its marketing on solving Node.js problems, Bun emphasizes performance. According to benchmarks published by the author of the new runtime environment, the advantage is really significant
Bun’s performance is caused by two main factors. First, unlike its competitors, it is written not in C++, but in the Zig language. If you haven’t heard of it yet, you have nothing to worry about, as it is still a fairly niche alternative to C++ and Rust.
The second factor affecting Bun’s performance is the use of the JavaScriptCore engine from Apple. As it turns out, JavaScriptCore is faster than V8. The difference is relatively small but it is definitely there.
Bun also makes quite a few improvements over Node.js . As with Deno, TypeScript is a natively supported language. Bun is also an alternative to NPM, which is up to 100x faster. Bun provides an API for creating macros (code generated during compilation with access to the AST tree and type metadata). To package applications, Bun uses it’s own bundler/transpiler which is incredibly fast. Frankly, all these functionalities sound much more interesting than the empty slogan: faster Node.js.
Deno
We didn’t have to wait long for Deno’s response to the arrival of a new player on the market. Just a few weeks after the release of the alpha version of Bun, the team behind Deno announced the npm compatibility and new http server with many performance improvements.
Supporting npm was a particularly controversial decision, given how long Deno authors claimed that it is the source of many of our ecosystem’s problems. However, it’s important to notice that npm in conjunction with Deno will work quite differently than in conjunction with Node.js. First, Deno will not require running npm install
. Second, Deno will not create a node_modules
directory, and downloaded packages will be stored globally. Third, npm imports will be marked with a special npm:
prefix:
import { chalk } from "npm:chalk@5";
Node.js
Node.js is not developing as rapidly as Bun or Deno. However, we can all agree that with such an oldie, we value stability and predictability rather than world-turning functionalities. Nevertheless, Node.js also delivered some interesting new features this year.
I’ll start with a feature that pleased everyone who has ever tried to write code that runs both on the server and in the browser. The fetch()
method based on the popular Undic
i library has arrived in Node 18. Admittedly, the implementation of fetch()
in browsers and Node.js are not 100% compatible, but the differences are so minor that most projects won’t even notice them.
fetch("http://example.com/api/endpoint")
.then((response) => {
// Do something with response
})
.catch(function (err) {
console.log("Unable to fetch -", err);
});
Another interesting new feature is the built-in test runner. This means that we will no longer need libraries such as Jest
or Vitest
to test Node apps.
test('synchronous passing test', (t) => {
// This test passes because it does not throw an exception.
assert.strictEqual(1, 1);
});
test('asynchronous passing test', async (t) => {
// This test passes because the Promise returned by the async
// function is not rejected.
assert.strictEqual(1, 1);
});