javascript

The 5 most amazing new JavaScript features in 2024

2024 has been an incredible year of brand new JS feature upgrades with ES15 and promising proposals.

From sophisticated async features to syntactic array sugar and modern regex, JavaScript coding has become easier and faster than ever.

1. Native array group-by is here

Object.groupBy():

JavaScript
const fruits = [ { name: 'pineapple🍍', color: '🟡' }, { name: 'apple🍎', color: '🔴' }, { name: 'banana🍌', color: '🟡' }, { name: 'strawberry🍓', color: '🔴' }, ]; const groupedByColor = Object.groupBy( fruits, (fruit, index) => fruit.color ); console.log(groupedByColor);

Literally the only thing keeping dinosaur Lodash alive — no more!

I was expecting a new instance method like Array.prototype.groupBy but they made it static for whatever reason.

Then we have Map.groupBy to group with object keys:

JavaScript
const array = [1, 2, 3, 4, 5]; const odd = { odd: true }; const even = { even: true }; Map.groupBy(array, (num, index) => { return num % 2 === 0 ? even: odd; }); // => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

Almost no one ever groups arrays this way though, so will probably be far less popular.

2. Resolve promise from outside — modern way

With Promise.withResolvers().

It’s very common to externally resolve promises and before we had to do it with a Deferred class:

JavaScript
class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } const deferred = new Deferred(); deferred.resolve();

Or install from NPM — one more dependency.

But now with Promise.withResolvers():

JavaScript
const { promise, resolve, reject } = Promise.withResolvers();

See how I use it to rapidly promisify an event stream — awaiting an observable:

JavaScript
// data-fetcher.js // ... const { promise, resolve, reject } = Promise.withResolvers(); export function startListening() { eventStream.on('data', (data) => { resolve(data); }); } export async function getData() { return await promise; } // client.js import { startListening, getData } from './data-fetcher.js'; startListening(); // ✅ listen for single stream event const data = await getData();

3. Buffer performance upgrades

Buffers are tiny data stores to store temporary data your app generates.

They make it incredible easy to transfer and process data across various stages in a pipeline.

Pipelines like:

  • File processing: Input file buffer process new buffer output file
  • Video streaming: Network response buffer display video frame
  • Restaurant queues: Receive customer queue/buffer serve customer
JavaScript
const fs = require('fs'); const { Transform } = require('stream'); const inputFile = 'input.txt'; const outputFile = 'output.txt'; const inputStream = fs.createReadStream(inputFile, 'utf-8'); const transformStream = new Transform({ transform(chunk) { // ✅ tranform chunks from buffer }, }); const outputStream = fs.createWriteStream(outputFile); // ✅ start pipeline inputStream.pipe(transformStream).pipe(outputStream);

With buffers, each stage process data at different speeds independent of each other.

But what happens when the data moving through the pipeline exceeds the buffers capacity?

Before we’d have to copy all the current data’s buffer to a bigger buffer.

Terrible for performance, especially when there’s gonna be a LOT of data in the pipeline.

ES15 gives us a solution to this problem: Resizable array buffers.

JavaScript
const resizableBuffer = new ArrayBuffer(1024, { maxByteLength: 1024 ** 2, }); // ✅ resize to 2048 bytes resizableBuffer.resize(1024 * 2);

4. Asynchronous upgrades

Atomics.waitAsync(): Another powerful async coding feature in ES2024:

It’s when 2 agents share a buffer…

And agent 1 “sleeps” and waits for agent 2 to complete a task.

When agent 2 is done, it notifies using the shared buffer as a channel.

JavaScript
const sharedBuffer = new SharedArrayBuffer(4096); const bufferLocation = new Int32Array(sharedBuffer); // ✅ initial value at buffer location bufferLocation[37] = 0x1330; async function doStuff() { // ✅ agent 1: wait on shared buffer location until notify Atomics.waitAsync(bufferLocation, 37, 0x1330).then((r) => {} /* handle arrival */); } function asyncTask() { // ✅ agent 2: notify on shared buffer location const bufferLocation = new Int32Array(sharedBuffer); Atomics.notify(bufferLocation, 37); }

You’d be absolutely right if you thought this similar to normal async/await.

But the biggest difference: The 2 agents can exist in completely different code contexts — they only need access to the same buffer.

And: multiple agents can access or wait on the shared buffer at different times — and any one of them can notify to “wake up” all the others.

It’s like a P2P network. async/await is like client-server request-response.

JavaScript
const sharedBuffer = new SharedArrayBuffer(4096); const bufferLocation = new Int32Array(sharedBuffer); bufferLocation[37] = 0x1330; // ✅ received shared buffer from postMessage() const code = ` var ia = null; onmessage = function (ev) { if (!ia) { postMessage("Aux worker is running"); ia = new Int32Array(ev.data); } postMessage("Aux worker is sleeping for a little bit"); setTimeout(function () { postMessage("Aux worker is waking"); Atomics.notify(ia, 37); }, 1000); }`; async function doStuff() { // ✅ agent 1: exists in a Worker context const worker = new Worker( 'data:application/javascript,' + encodeURIComponent(code) ); worker.onmessage = (event) => { /* log event */ }; worker.postMessage(sharedBuffer); Atomics.waitAsync(bufferLocation, 37, 0x1330).then( (r) => {} /* handle arrival */ ); } function asyncTask() { // ✅ agent 2: notify on shared buffer location const bufferLocation = new Int32Array(sharedBuffer); Atomics.notify(bufferLocation, 37); }

5. Regex v flag & set operations

A new feature to make regex patters much more intuitive.

Finding and manipulating complex strings using expressive patterns — with the help of set operations:

JavaScript
// A and B are character class, like [a-z] // difference: matches A but not B [A--B] // intersection: matches both A & b [A&&B] // nested character class [A--[0-9]]

To match ever-increasing sets of Unicode characters, like:

  • Emojis: 😀, ❤️, 👍, 🎉, etc.
  • Accented letters: é, à, ö, ñ, etc.
  • Symbols and non-Latin characters: ©, ®, €, £, µ, ¥, etc

So here we use Unicode regex and the v flag to match all Greek letters:

JavaScript
const regex = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;

Final thoughts

Overall 2024 was significant leap for JavaScript with several features essential for modern development.

Empowering you to write cleaner code with greater conciseness, expressiveness, and clarity.

Incredible infinite scroll with JavaScript

Infinite scroll: Loading more and more content as the user scrolls down to the end.

No need for pagination + increases time spent on the site

With simple JavaScript we can recreate this easily:

We start with the basic HTML:

HTML
<div id="load-trigger-wrapper"> <!-- Grid of images> <div id="image-container"></div> <!-- Intersection Observer observes this --> < <div id="load-trigger"></div> </div> <!-- Number of loading images --> <div id="bottom-panel"> Images: &nbsp;<b><span id="image-count"></span> &nbsp;</b>/ &nbsp;<b><span id="image-total"></span></b> </div>

Now it’s time to detect scrolling to the end with the Intersection Observer API:

JavaScript
const loadTrigger = document.getElementById('load-trigger'); // ... const observer = detectScroll(); // ... // Detect when function detectScroll() { const observer = new IntersectionObserver( // Callback also runs after observe() (entries) => { for (const entry of entries) { // ... loadMoreImages(); // ... } }, // Set "rootMargin" because of #bottom-panel height // 30px upwards from the bottom { rootMargin: '-30px' } ); // Start watching #load-trigger div observer.observe(loadTrigger); return observer; }

Now let’s show the initial skeleton images:

JavaScript
const imageClass = 'image'; const skeletonImageClass = 'skeleton-image'; // ... // This function would make requests to an image server function loadMoreImages() { const newImageElements = []; // ... for (let i = 0; i < amountToLoad; i++) { const image = document.createElement('div'); // 👇 Display each image with skeleton-image class image.classList.add(imageClass, skeletonImageClass); // Include image in container imageContainer.appendChild(image); // Store in temp array to update with actual image when loaded newImageElements.push(image); } // ... }
CSS
.image, .skeleton-image { height: 50vh; border-radius: 5px; border: 1px solid #c0c0c0; /* Three per row, with space for margin */ width: calc((100% / 3) - 24px); /* Initial color before loading animation */ background-color: #eaeaea; /* Grid spacing */ margin: 8px; /* Fit into grid */ display: inline-block; } .skeleton-image { transition: all 200ms ease-in; /* Contain ::after element with absolute positioning */ position: relative; /* Prevent overflow from ::after element */ overflow: hidden; } .skeleton-image::after { content: ""; /* Cover .skeleton-image div*/ position: absolute; top: 0; right: 0; bottom: 0; left: 0; /* Setup for slide-in animation */ transform: translateX(-100%); /* Loader image */ background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.2) 20%, rgba(255, 255, 255, 0.5) 60%, rgba(255, 255, 255, 0)); /* Continue animation until image load*/ animation: load 1s infinite; } @keyframes load { /* Slide-in animation */ 100% { transform: translateX(100%) } }

Update skeleton images

We get colors instead of images.

JavaScript
function loadMoreImages() { // ... // Create skeleton images and stored them in "newImageElements" variable // Simulate delay from network request setTimeout(() => { // Colors instead of images const colors = getColors(amountToLoad); for (let i = 0; i < colors.length; i++) { const color = colors[i]; // 👇 Remove skeleton loading indicator and show color newImageElements[i].classList.remove(skeletonImageClass); newImageElements[i].style.backgroundColor = color; } }, 2000); // ... } function getColors(count) { const result = []; let randUrl = undefined; while (result.length < count) { // Prevent duplicate images while (!randUrl || result.includes(randUrl)) { randUrl = getRandomColor(); } result.push(randUrl); } return result; } function getRandomColor() { const h = Math.floor(Math.random() * 360); return `hsl(${h}deg, 90%, 85%)`; }

Stop infinite scroll

This is a demo so we’ll have a artificial number of images like 50.

JavaScript
const imageCountText = document.getElementById('image-count'); // ... let imagesShown = 0; // ... function loadMoreImages() { // ... const amountToLoad = Math.min(loadLimit, imageLimit - imagesShown); // Load skeleton images... // Update skeleton images... // Update image count imagesShown += amountToLoad; imageCountText.innerText = imagesShown; if (imagesShown === imageLimit) { observer.unobserve(loadTrigger); } }

Optimize performance with throttling

By using a throttle function to only allow new loadings within a certain time.

JavaScript
let throttleTimer; // Only one image batch can be loaded within a second const throttleTime = 1000; // ... function throttle(callback, time) { // Prevent additional calls until timeout elapses if (throttleTimer) { console.log('throttling'); return; } throttleTimer = true; setTimeout(() => { callback(); // Allow additional calls after timeout elapses throttleTimer = false; }, time); }

By calling throttle() in the Intersection Observer’s callback with a time of 1000, we ensure that loadMoreImages() is never called multiple times within a second.

JavaScript
function detectScroll() { const observer = new IntersectionObserver( (entries) => { // ... throttle(() => { loadMoreImages(); }, throttleTime); } } }, // ... ); // ... }

New JavaScript pipeline operator: transform anything into a one-liner

With the pipeline operator you’ll stop writing code like this:

JavaScript
const names = ['USA', 'Australia', 'CodingBeauty']; const lowerCasedNames = names.map((name) => name.toLowerCase() ); const hyphenJoinedNames = lowerCasedNames.join('-'); const hyphenJoinedNamesCapitalized = capitalize( hyphenJoinedNames ); const prefixedNames = `Names: ${hyphenJoinedNamesCapitalized}`; console.log(prefixedNames); // Names: Usa-australia-codingbeauty

and start writing code like this:

JavaScript
// Hack pipes: | and > ['USA', 'Australia', 'CodingBeauty'] |> %.map((name) => name.toLowerCase()) |> %.join('-') |> capitalize(%) |> `Names: ${%}` |> console.log(%); // Names: Usa-australia-codingbeauty

So refreshingly clean — and elegant! All those temporary variables are gone — not to mention the time it took to come up with those names *and* type them (not everyone types like The Flash, unfortunately).

You may have heard this partially true quote attributed to Phil Karlton: “There are only two hard things in computer science: cache invalidation and naming things“.

Using the JavaScript pipeline operator clears out the clutter to boost readability and write data transforming code (basically all code) in a more intuitive manner.

Verbosity should be avoided as much as possible, and this works so much better to compact code than reusing short-named variables:

JavaScript
let buffer = await sharp('coding-beauty-v1.jpg').toBuffer(); let image = await jimp.read(buffer); image.grayscale(); buffer = await image.getBufferAsync('image/png'); buffer = await sharp(buffer).resize(250, 250).toBuffer(); image = await jimp.read(buffer); image.sepia(); buffer = await image.getBufferAsync('image/png'); await sharp(buffer).toFile('coding-beauty-v1.png');

Hopefully, almost no one codes like this on a regular basis. It’s a pretty horrible technique when done in a large scale; a perfect example showing why we embrace immutability and type systems.

Unlike the pipeline operator, there’s no certainty that the variable always contains the value you set at any given point; you’ll need to climb up the scope to look for re-assignments. We can have used the _ at an earlier point in the code; the value it has at various points in the code is simply not guaranteed.

Now we’re just using an underscore, so without checking out the right-hand side of those re-assignments you can’t quickly know what the type of the variable is, unless you have a smart editor like VS Code (although I guess you could say that doesn’t matter since they’re supposed to be “temporary” — at least until they’re not!).

All in all, poor readability. Fragile and Unstable. 5 times harder for someone new to understand. Also, some would say underscores are “ugly”, especially in languages like JavaScript where they hardly show up.

JavaScript
// setup function one() { return 1; } function double(x) { return x * 2; } let _; _ = one(); // is now 1. _ = double(_); // is now 2. Promise.resolve().then(() => { // This does *not* print 2! // It prints 1, because '_' is reassigned downstream. console.log(_); }); // _ becomes 1 before the promise callback. _ = one(_);

Okay, so why don’t we just avoid this infestation of temporary underscores, and nest them into one gigantic one-liner?

JavaScript
await sharp( jimp .read( await sharp( await jimp .read( await sharp('coding-beauty-v1.jpg').toBuffer() ) .grayscale() .getBufferAsync('image/png') ) .resize(250, 250) .toBuffer() ) .sepia() .getBufferAsync('image/png') ).toFile('coding-beauty-v2.png');

It’s a mess. The underscore is gone, but who in the world can understand this at a glance? How easy is it to tell how the data flows throughout this code, and make any necessary adjustments.

Understanding, at a glance — this is what we should strive for with every line of code we write.

The pipeline operator greatly outshines every other method, giving us both freedom from temporary variables and readability. It was designed for this.

JavaScript
// We don't need to wrap 'await' anymore await sharp('coding-beauty-v1.jpg').toBuffer() |> jimp.read(%) |> %.grayscale() |> await %.getBufferAsync('image/png') |> await sharp(%).resize(250, 250).toBuffer() |> await jimp.read(%) |> %.sepia() |> await %.getBufferAsync('image/png') |> await sharp(%).toFile('coding-beauty-v2.png');

Here the % only exists within this particular pipeline.

Method chaining?

Who hasn’t used and combined heavily popular array methods like map, filter, and sort? Very hard to avoid in applications involving any form of list manipulation.

JavaScript
const numbers = '4,2,1,3,5'; const result = numbers .split(',') .map(Number) .filter((num) => num % 2 === 0) .map((num) => num * 2) .sort(); // [4, 8]

This is actually great. There aren’t any temporary variables or unreadable nesting here either and we can easily follow the chain from start to finish.

The formatting lets us easily add more methods at any point in the chain; feature-packed editor like VS Code can easily swap the processing order of two methods, with the Ctrl + Up and Ctrl + Down shortcuts.

There’s a reason why libraries like core http and jQuery are designed like this:

JavaScript
const http = require('http'); http .createServer((req, res) => { console.log('Welcome to Coding Beauty'); }) .on('error', () => { console.log('Oh no!'); }) .on('close', () => { console.log('Uuhhm... bye!'); }) .listen(3000, () => { console.log('Find me on port 3000'); });

The problem with method chaining is that we can’t use it everywhere. If the class wasn’t designed like that we’re stuck and out in the cold.

It doesn’t work very well with generator methods, async/await and function/method calls outside the object, like we saw here:

JavaScript
await sharp( // 3-method chain, but not good enough! jimp .read( await sharp( // Same here await jimp .read( await sharp('coding-beauty-v1.jpg').toBuffer() ) .grayscale() .getBufferAsync('image/png') ) .resize(250, 250) .toBuffer() ) .sepia() .getBufferAsync('image/png') ).toFile('coding-beauty-v2.png');

But all this and more work with the pipeline operator; even object literals and async import function.

JavaScript
await sharp('coding-beauty-v1.jpg').toBuffer() |> jimp.read(%) |> %.grayscale() |> await %.getBufferAsync('image/png') |> await sharp(%).resize(250, 250).toBuffer() |> await jimp.read(%) |> %.sepia() |> await %.getBufferAsync('image/png') |> await sharp(%).toFile('coding-beauty-v2.png');

Could have been F# pipes

We would have been using the pipeline operator very similarly to F# pipes, with the above turning out like this instead:

JavaScript
(await sharp('coding-beauty-v1.jpg').toBuffer()) |> (x) => jimp.read(x) |> (x) => x.grayscale() |> (x) => x.getBufferAsync('image/png') |> await |> (x) => sharp(x).resize(250, 250).toBuffer() |> await |> (x) => jimp.read(x) |> await |> (x) => x.sepia() |> (x) => x.getBufferAsync('image/png') |> await |> (x) => sharp(x).toFile('coding-beauty-v2.png'); |> await

There was an alternative design. But you can already see how this makes for an inferior alternative: Only single-function arguments are allowed and the operation is more verbose. Unless the operation is already a single-argument function call.

It’s weird handling of async/await was also a key reason why it got rejected — along with memory usage concerns. So, forget about F# pipes in JS!

Use the pipeline operator right now

Yes you can — with Babel.

Babel has a nice habit of implementing features before they’re officially integrated in the language; it did this for top-level await, optional chaining, and many others. The pipeline operator couldn’t be an exception.

Just use the @babel/plugin-proposal-pipeline-operator plugin and you’re good to.

It’s optional of course — but not for long.

Prettier the code formatter is already prepared.

Even though we can’t say the same about VS Code or Node.js.

Right now there’s even speculation that % won’t be the final symbol pass around in the pipeline; let’s watch and see how it all plays out.

Final thoughts

It’s always great to see new and exciting features come to the language. With the JavaScript pipeline operator, you’ll cleanse your code of temporary variables and cryptic nesting, greatly boost code readability, efficiency, and quality.

Svelte 5 is React on steroids😲

Svelte 5 just got released and it’s packed with amazing new upgrades.

It’s like React now but leaner, easier, and yes — faster.

Just look at this:

❌ Before Svelte 5:

Creating state:

You could just create state variables with let:

HTML
<script> let count = 0; let site = 'codingbeautydev.com' </script>

✅ Now with Svelte 5…

useState😱

HTML
<script> let count = $state(0); let site = $state('codingbeautydev.com'); </script>

vs React:

JavaScript
export function Component() { const [count, setCount] = useState(0); const [site, setSite] = useState('codingbeautydev.com'); // ... }

But see how the Svelte version is still much less verbose — and no need for a component name.

Watching for changes in state?

In React this is useEffect:

JavaScript
export function Component() { const [count, setState] = useState(0); useEffect(() => { if (count > 9) { alert('Double digits?!'); } }, [count]); const double = count * 2; // ... }

❌ Before Svelte 5:

You had to use a cryptic unnatural $: syntax to watch for changes — and to create derived state too.

HTML
<script> let count = $state(0); $:() => { if (count > 9) { alert('Double digits?!'); } }; $: double = count * 2; </script>

✅ Now:

We have useEffect in Svelte with $effect 👇

And a more straightforward way to create auto-updated derived state:

HTML
<script> let count = $state(0); $effect(() => { if (count > 9) { alert('Double digits?!'); } }); const double = $derived(count * 2); </script>

Svelte intelligently figures out dependencies to watch for, unlike React.

And what about handling events and updating the state?

In React:

JavaScript
export function Component() { // 👇 `setState` function from `useState` const [count, setCount] = useState(0); return ( // event handlers are good old JS functions <button onClick={() => setCount((prev) => prev + 1)}> Increase </button> ); }

❌ Before:

Svelte used to treat events specially and differently from props.

HTML
<script> let count = $state(0); </script> Count: {count} <br /> <!-- 👇 special on: directive for events --> <button on:click={() => count++}> Increase </button>

✅ Now:

Svelte is now following React’s style of treating events just like properties.

HTML
<script> let count = $state(0); </script> Count: {count} <br /> <!-- 👇onclick is just a regular JS function now --> <button onclick={() => count++}> Increase </button>

Custom component props

In React:

Props are a regular JS object the component receives:

JavaScript
// 👇 `props` is object export function Person(props) { const { firstName, lastName } = props; return ( <h1> {firstName} {lastName} </h1> ); }

❌ Before Svelte 5:

You had to use a weird export let approach to expose component properties:

HTML
<script> export let firstName; export let lastName; </script> <h1> {firstName} {lastName} </h1>

Now in Svelte 5:

There’s a $props function that returns an object like in React!

HTML
<script> const { firstName, lastName } = $props(); </script> <h1> {firstName} {lastName} </h1>

Custom component events

In React:

Events are just props so they just need to accept a callback and call it whenever:

JavaScript
import React, { useState } from 'react'; const Counter = ({ onIncrease }) => { const [increase, setIncrease] = useState(0); const handleIncrease = () => { onIncrease(increase); }; return ( <div> Increase: {count} <button onClick={handleIncrease}>Increase</button> </div> ); }; export default Counter;

❌ Before Svelte 5:

You’d have to use this complex createEventDispatcher approach:

HTML
<script> import { createEventDispatcher } from 'svelte'; let increase = $state(0); const dispatchEvent = createEventDispatcher(); </script> <button on:click={() => { dispatchEvent('increase', increase); }} > Increase </button>

✅ Now:

Events are now props like in React:

HTML
<script> import { createEventDispatcher } from 'svelte'; let increase = $state(0); const { onIncrease } = $props(); </script> <button onclick={() => onIncrease(count) } > Increase </button>

Components: classes -> functions

Yes, Svelte has gone the way of React here too.

Remember when we still had to extend Component and use render() to create a class component?

JavaScript
import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = { count: 0 }; } incrementCount = () => { this.setState((prevState) => ({ count: prevState.count + 1 })); }; render() { return ( <div> <h1>Counter: {this.state.count}</h1> <button onClick={this.incrementCount}>Increment</button> </div> ); } } export default Counter;

And then hooks came along to let us have much simpler function components?

Svelte has now done something similar, by making components classes instead of functions by default.

In practice this won’t change much of how you write Svelte code — we never created the classes directly anyway — but it does tweak the app mounting code a little:

JavaScript
import { mount } from 'svelte'; import App from './App.svelte' // ❌ Before const app = new App({ target: document.getElementById("app") }); // ✅ After const app = mount(App, { target: document.getElementById("app") }); export default app;

Final thoughts

It’s great to see Svelte improve with inspiration from other frameworks.

Gaining the intuitiveness of the React-style design while staying lean and fast.

Web dev keeps moving.

This new React library will make you dump Redux forever

The new Zustand library changes everything for state management in web dev.

The simplicity completely blows Redux away. It’s like Assembly vs Python.

Forget action types, dispatch, Providers and all that verbose garbage.

Just use a hook! 👇

JavaScript
import { create } from 'zustand'; const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }));

Effortless and intuitive with all the benefits of Redux and Flux — immutability, data-UI decoupling…

With none of the boilerplate — it’s just an object.

Redux had to be patched with hook support but Zustand was built from the ground up with hooks in mind.

JavaScript
function App() { const store = useStore(); return ( <div> <div>Count: {store.count}</div> <button onClick={store.increment}>Increment</button> </div> ); } export default App;

Share the store across multiple components and select only what you want:

JavaScript
function Counter() { // ✅ Only `count` const count = useStore((state) => state.count); return <div>Count: {count}</div>; } function Controls() { // ✅ Only `increment` const increment = useStore((state) => state.increment); return <button onClick={increment}>Increment</button>; }

Create multiple stores to decentralize data and scale intuitively.

Let’s be real, that single-state stuff doesn’t always make sense. And it defies encapsulation.

It’s often more natural to let a branch of components have their localized global state.

JavaScript
// ✅ More global store to handle the count data const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); // ✅ More local store to handle user input logic const useControlStore = create((set) => ({ input: '', setInput: () => set((state) => ({ input: state.input })), })); function Controls() { return ( <div> <CountInput /> <Button /> </div> ); } function Button() { const increment = useStore((state) => state.increment); const input = useControlStore((state) => state.input); return ( <button onClick={() => increment(Number(input))}> Increment by {input} </button> ); } function CountInput() { const input = useControlStore((state) => state.input); return <input value={input} />; }

Meet useShallow(), a powerful way to get derived states — instantly updates when any of original states change.

JavaScript
import { create } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; const useLibraryStore = create((set) => ({ fiction: 0, nonFiction: 0, borrowedBooks: {}, // ... })); // ✅ Object pick const { fiction, nonFiction } = useLibraryStore( useShallow((state) => ({ fiction: state.fiction, nonFiction: state.nonFiction, })) ); // ✅ Array pick const [fiction, nonFiction] = useLibraryStore( useShallow((state) => [state.fiction, state.nonFiction]) ); // ✅ Mapped picks const borrowedBooks = useLibraryStore( useShallow((state) => Object.keys(state.borrowedBooks)) );

And what if you don’t want instant updates — only at certain times?

It’s easier than ever — just pass a second argument to your store hook.

JavaScript
const user = useUserStore( (state) => state.user, (oldUser, newUser) => compare(oldUser.id, newUser.id) );

And how about derived updates based on previous states, like in React’s useState?

Don’t worry! In Zustand states update partially by default:

JavaScript
const useStore = create((set) => ({ user: { username: 'tariibaba', site: 'codingbeautydev.com', color: 'blue💙', }, premium: false, // `user` object is not affected // `state` is the curr state before the update unsubscribe: () => set((state) => ({ premium: false })), }));

It only works at the first level though — you have to handle deeper partial updates by yourself:

JavaScript
const useStore = create((set) => ({ user: { username: 'tariibaba', site: 'codingbeautydev.com', color: 'blue💙', }, premium: false, updateUsername: (username) => // 👇 deep updates necessary to retain other object properties set((state) => ({ user: { ...state.user, username } })), }));

If you don’t want it just pass the object directly with true as the two arguments.

JavaScript
const useStore = create((set) => ({ user: { username: 'tariibaba', site: 'codingbeautydev.com', color: 'blue💙', }, premium: false, // Clear data with `true` resetAccount: () => set({}, true), }));

Zustand even has built-in support for async actions — no need for Redux Thunk or any external library.

JavaScript
const useStore = create((set) => ({ user: { username: 'tariibaba', site: 'codingbeautydev.com', color: 'blue💙', }, premium: false, // ✅ async actions updateFavColor: async (color) => { await fetch('https://api.tariibaba.com', { method: 'PUT', body: color, }); set((state) => ({ user: { ...state.user, color } })); }, }));

It’s also easy to get state within actions, thanks to get — the 2nd param in create()‘s callback:

JavaScript
// ✅ `get` lets us use state directly in actions const useStore = create((set, get) => ({ user: { username: 'tariibaba', site: 'codingbeautydev.com', color: 'blue💙', }, messages: [], sendMessage: ({ message, to }) => { const newMessage = { message, to, // ✅ `get` gives us `user` object from: get().user.username, }; set((state) => ({ messages: [...state.messages, newMessage], })); }, }));

It’s all about hooks in Zustand, but if you want you can read and subscribe to values in state directly.

JavaScript
// Get a non-observed state with getState() const count = useStore.getState().count; useStore.subscribe((state) => { console.log(`new value: ${state.count}`); });

This makes it great for cases where the property changes a lot but you only need the latest value for intermediate logic, not direct UI:

JavaScript
export default function App() { const widthRef = useRef(useStore.getState().windowWidth); useEffect(() => { useStore.subscribe((state) => { widthRef.current = state.windowWidth; }); }, []); useEffect(() => { setInterval(() => { console.log(`Width is now: ${widthRef.current}`); }, 1000); }, []); // ... }

Zustand outshines Redux and Mobx and all the others in almost every way. Use it for your next project and you won’t regret it.

The secret code Google uses to monitor everything you do online

Google now has at least 3 ways to track your search clicks and visits that they hide from you.

Have you ever tried to copy a URL directly from Google Search?

When I did that a few months ago, I unexpectedly got something like this from my clipboard.

Plain text
https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwjUmK2Tk-eCAxXtV0EAHX3jCyoQFnoECAkQAQ&url=https%3A%2F%2Fcodingbeautydev.com%2Fblog%2Fvscode-tips-tricks&usg=AOvVaw0xw4tT2wWNUxkHWf90XadI&opi=89978449

I curiously visited the page and guess what? It took me straight to the original URL.

This cryptic URL turned out to be a middleman that would redirect you to the actual page.

But what for?

After some investigation, I discovered that this was how Google Search had been recording our clicks and tracking every single visited page.

They set custom data- attributes and a mousedown event on each link in the search results page:

HTML
<a jsname="UWckNb" href="https://codingbeautydev.com/blog/vscode-tips-tricks" data-jsarwt="1" data-usg="AOvVaw0xw4tT2wWNUxkHWf90XadI" data-ved="2ahUKEwjUmK2Tk-eCAxXtV0EAHX3jCyoQFnoECAkQAQ"> </a>

A JavaScript function would change the href to a new URL with several parameters including the original URL, as soon as you start clicking on it.

JavaScript
import express from 'express'; const app = express(); app.get('/url', (req, res) => { // Record click and stuff... res.redirect(req.query); }); app.listen(3000);

So even though the browser would show the actual URL at the bottom-left on hover, once you clicked on it to copy, the href would change instantly.

Why mousedown over click? Probably because there won’t be a click event when users open the link in a new tab, which is something that happens quite often.

And so after right-clicking to copy like I did, mousedown would fire and the href would change, which would even update that preview URL at the bottom-left.

The new www.google.com/url page would log the visit and move out of the way so fast you’d barely notice it — unless your internet moves at snail speed.

They use this data for tools like Google Analytics and Search Console so site owners can improve the quality of their search results and pages by analyzing click-rate — something probably also using as a Search ranking factor. Not to mention recording clicks on Search ads to rake all the billions of yearly ad revenue.

Google Search Console. Source: Search Console Discover report now includes Chrome data

But Google got smarter.

They realized this URL tracking method had a serious issue for a certain group. For their users with slower internet speeds, the annoying redirect technique added a non-trivial amount of delay to the request and increased bounce rate.

So they did something new.

Now, instead of that cryptic www.google.com/url stuff, you get… the same exact URL?

With the <a> ping attribute, they have now successfully moved their tracking behind the scenes.

The ping attribute specifies one or more URLs that will be notified when the user visits the link. When a user opens the link, the browser asynchronously sends a short HTTP POST request to the URLs in ping.

The keyword here is asynchronously — www.google.com/url quietly records the click in the background without ever notifying the user, avoiding the redirect and keeping the user experience clean.

Browsers don’t visually indicate the ping attribute in any way to the user — a specification violation.

When the ping attribute is present, user agents should clearly indicate to the user that following the hyperlink will also cause secondary requests to be sent in the background, possibly including listing the actual target URLs.

HTML Standard (whatwg.org)

Not to mention a privacy concern, which is why browsers like Firefox refuse to enable this feature by default.

In Firefox Google sticks with the mousedown event approach:

There are many reasons not to disable JavaScript in 2023, but even if you do, Google will simply replace the href with a direct link to www.google.com/url.

HTML
<a href="/url?sa=t&source=web&rct=j&url=https://codingbeautydev.com/blog/vscode-tips-tricks..."> 10 essential VS Code tips and tricks for greater productivity </a>

So, there’s really no built-in way to avoid this mostly invisible tracking.

Even the analytics are highly beneficial for Google and site owners in improving result relevancy and site quality, as users we should be aware of the existence and implications of these tracking methods.

As technology becomes more integrated into our lives, we will increasingly have to choose between privacy and convenience and ask ourselves whether the trade-offs are worth it.

Nobody wants to use these Array methods😭

There’s so much more to arrays than map()filter()find(), and push() .

But most devs are completely clueless about this — several powerful methods they’re missing out on.

Check these out:

1. copyWithin()

Array copyWithin() copies a part of an array to another position in the same array and returns it without increasing its length.

JavaScript
const array = [1, 2, 3, 4, 5]; // copyWithin(target, start, end) // replace arr with start..end at target // a. target -> 3 (index) // b. start -> 1 (index) // c. end -> 3 (index) // start..end -> 2, 3 const result = array.copyWithin(3, 1, 3); console.log(result); // [1, 2, 3, 2, 3]

end parameter is optional:

JavaScript
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // "end" not specified so last array index used // target -> 0 (index) // start..end -> 6, 7, 8, 9, 10 const result = array.copyWithin(0, 5); // [6, 7, 8, 9, 10, 6, 7, 8, 9, 10] console.log(result);
JavaScript
const array = [1, 2, 3, 4, 5]; // Copy numbers 2, 3, 4, and 5 (cut off at index 4) const result = array.copyWithin(3, 1, 6); console.log(result); // [1, 2, 3, 2, 3]

2. at() and with()

at() came first and with() came a year after that in 2023.

They are the functional and immutable versions of single-element array modification and access.

JavaScript
const colors = ['pink', 'purple', 'red', 'yellow']; console.log(colors.at(1)); // purple console.log(colors.with(1, 'blue')); // ['pink', 'blue', 'red', 'yellow'] // Original not modified console.log(colors); // ['pink', 'purple', 'red', 'yellow']

The cool thing about these new methods is how they let you get and change element values with negative indexing.

3. Array reduceRight() method

Works like reduce() but the callback goes from right to left instead of left to right:

JavaScript
const letters = ['b', 'e', 'a', 'u', 't', 'y']; const word = letters.reduce((word, letter) => word + letter, ''); console.log(word); // beauty // Reducer iterations // 1. ('', 'y') => '' + 'y' = 'y' // 2. ('y', 't') => 'y' + 't' = 'yt'; // 3. ('yt', 'u') => 'ytu'; // ... // n. ('ytuae', 'b') => 'ytuaeb'; const wordReversed = letters.reduceRight((word, letter) => word + letter, ''); console.log(wordReversed); // ytuaeb

Here’s another great scenario for reduceRight():

JavaScript
const thresholds = [ { color: 'blue', threshold: 0.7 }, { color: 'orange', threshold: 0.5 }, { color: 'red', threshold: 0.2 }, ]; const value = 0.9; const threshold = thresholds.reduceRight((color, threshold) => threshold.threshold > value ? threshold.color : color ); console.log(threshold.color); // red

4. Array findLast() method

New in ES13: find array item starting from last element.

Great for cases where where searching from end position produces better performance than with find()

Example:

JavaScript
const memories = [ // 10 years of memories... { date: '2020-02-05', description: 'My first love' }, // ... { date: '2022-03-09', description: 'Our first baby' }, // ... { date: '2024-01-25', description: 'Our new house' }, ]; const currentYear = new Date().getFullYear(); const query = 'unique'; const milestonesThisYear = events.find( (event) => new Date(event.date).getFullYear() === currentYear && event.description.includes(query) );

This works but as our target object is closer to the tail of the array, findLast() should run faster:

JavaScript
const memories = [ // 10 years of memories... { date: '2020-02-05', description: 'My first love' }, // ... { date: '2022-03-09', description: 'Our first baby' }, // ... { date: '2024-01-25', description: 'Our new house' }, ]; const currentYear = new Date().getFullYear(); const query = 'unique'; const milestonesThisYear = events.findLast( (event) => new Date(event.date).getFullYear() === currentYear && event.description.includes(query) );

Another use case for findLast() is when we have to specifically search the array from the end to get the correct element.

For example, if we want to find the last even number in a list of numbers, find() would produce a totally wrong result:

JavaScript
const nums = [7, 14, 3, 8, 10, 9]; // gives 14, instead of 10 const lastEven = nums.find((value) => value % 2 === 0); console.log(lastEven); // 14

But findLast() will start the search from the end and give us the correct item:

JavaScript
const nums = [7, 14, 3, 8, 10, 9]; const lastEven = nums.findLast((num) => num % 2 === 0); console.log(lastEven); // 10

5. toSorted(), toReversed(), toSpliced()

ES2023 came fully packed with immutable versions of sort(), reverse(), and splice().

Okay maybe splice() isn’t used as much as the others, but they all mutate the array in place.

JavaScript
const original = [5, 1, 3, 4, 2]; const reversed = original.reverse(); console.log(reversed); // [2, 4, 3, 1, 5] (same array) console.log(original); // [2, 4, 3, 1, 5] (mutated) const sorted = original.sort(); console.log(sorted); // [1, 2, 3, 4, 5] (same array) console.log(original); // [1, 2, 3, 4, 5] (mutated) const deleted = original.splice(1, 2, 7, 10); console.log(deleted); // [2, 3] (deleted elements) console.log(original); // [1, 7, 10, 4, 5] (mutated)

Immutability gives us predictable and safer code; debugging is much easier as we’re certain variables never change their value.

Arguments are exactly the same, with splice() and toSpliced() having to differ in their return value.

JavaScript
const original = [5, 1, 3, 4, 2]; const reversed = original.toReversed(); console.log(reversed); // [2, 4, 3, 1, 5] (copy) console.log(original); // [5, 1, 3, 4, 2] (unchanged) const sorted = original.toSorted(); console.log(sorted); // [1, 2, 3, 4, 5] (copy) console.log(original); // [5, 1, 3, 4, 2] (unchanged) const spliced = original.toSpliced(1, 2, 7, 10); console.log(spliced); // [1, 7, 10, 4, 5] (copy) console.log(original); // [5, 1, 3, 4, 2] (unchanged)

6. Array lastIndexOf() method

The lastIndexOf() method returns the last index where a particular element can be found in an array.

JavaScript
const colors = ['a', 'e', 'a', 'f', 'a', 'b']; const index = colors.lastIndexOf('a'); console.log(index); // 4

We can pass a second argument to lastIndexOf() to specify an index in the array where it should stop searching for the string after that index:

JavaScript
const colors = ['a', 'e', 'a', 'f', 'a', 'b']; // Get last index of 'a' before index 3 const index1 = colors.lastIndexOf('a', 3); console.log(index1); // 2 const index2 = colors.lastIndexOf('a', 0); console.log(index2); // 0 const index3 = colors.lastIndexOf('f', 2); console.log(index3); // -1

7. Array flatMap() method

The flatMap() method transforms an array using a given callback function and then flattens the transformed result by one level:

JavaScript
const arr = [1, 2, 3, 4]; const withDoubles = arr.flatMap((num) => [num, num * 2]); console.log(withDoubles); // [1, 2, 2, 4, 3, 6, 4, 8]

Calling flatMap() on the array does the same thing as calling map() followed by a flat() of depth 1, but it`s a bit more efficient than calling these two methods separately.

JavaScript
const arr = [1, 2, 3, 4]; // flat() uses a depth of 1 by default const withDoubles = arr.map((num) => [num, num * 2]).flat(); console.log(withDoubles); // [1, 2, 2, 4, 3, 6, 4, 8]

Final thoughts

They are not that well-known (yet) but they have their unique uses and quite powerful.

10 powerful tools for faster web development

10 fantastic web dev tools to level up your productivity and achieve your coding goals faster than ever.

From colorful styling effects to faster typing, these tools will boost your workflow and make a lot of things easier.

1. React Glow

Create a playful glow that dances with the user’s mouse, adding a touch of magic to your interface:

Whenever I see effects like this on a page, it just gives me an aura of sophistication and quality.

2. Vite

Ditch Create React App and make development a breeze with Vite.

Say hello to lightning-fast hot module replacement and built-in TypeScript support.

And absolutely no need to worry about outdated dependencies or vulnerable packages – unlike with Create React App:

On the other hand, this is what I get for using dinosaur Create React App in one my Electron projects:

3. TODO Highlight for VS Code

With TODO highlight I can quickly add TODO comments anywhere in my codebase and keep track of all them effortlessly.

You quickly add a TODO with comment prefixed with TODO:

Then view in the Output window after running the TODO-Highlight: List highlighted annotations command:

I find it a great alternative to storing them in a to-do list app, especially when they’re something very low-level and contextual, for example: “TODO: Use 2 map()’s instead of reduce()”.

4. Rough Notation

Powerful JS library for creating and animating colorful annotations on a web page with a hand-drawn look and feel.

When I see this I see a human touch in the deliberate imperfections; it stands out.

So there’s underline, box, circle, highlight, strike-through… many many annotation styles to choose from with duration and color customization options.

5. JavaScript (ES6) Code Snippets for VS Cod

VS Code extension fully loaded with heaps of time-saving JavaScript code snippets for ES6.

Snippets like imp and imd:

A demo of how to use the JavaScript (ES6) Code Snippets extension.

6. background-removal-js

This free, browser-based JavaScript library lets you easily remove backgrounds from your images all while keeping your data private.

7. VanJS

This is a super small and simple library for building user interfaces.

It uses plain JavaScript and the built-in DOM functionality, just like React. But unlike React it doesn’t need any special syntax for defining UI elements.

JavaScript
// Reusable components can be just pure vanilla JavaScript functions. // Here we capitalize the first letter to follow React conventions. const Hello = () => div( p("👋Hello"), ul( li("🗺️World"), li(a({href: "https://codingbeautydev.com/"}, "💻Coding Beauty")), ), ) van.add(document.body, Hello()) // Alternatively, you can write: // document.body.appendChild(Hello())

8. Mailo

This drag-and-drop builder makes crafting beautiful, responsive emails a breeze. No more coding headaches, just watch your emails light up inboxes across all devices.

9. Color Names

Dive into a treasure trove of over 30,000 color names, meticulously gathered from across the web and lovingly enriched by thousands of passionate users.

Find the perfect shade to ignite your creativity and bring your designs to life

10. Flowbite Icons

Unleash a treasure trove of 450+ stunning SVG icons, all open-source and ready to bring your web projects to life.

Whether you prefer bold solids or crisp outlines, these icons seamlessly integrate with Flowbite and Tailwind CSS, making customization a breeze.

Final thoughts

Use these awesome tools to level up your productivity and developer quality of life.

Interviewer: You are clueless about for loops😂

Interviewer: What does func() print in this code?

JavaScript
const arr = [1, 2, 3, 4]; for (let i = 0; i < arr.length; i++) { func(arr[i]); arr.splice(0, 1); } function func(item) { console.log(item); }

A huge grin appeared on Jimmy’s face.

After all the many rounds of interviews he’d finally gotten to this last question, and it was easier than he ever thought it would be.

Get this right and the job was assured.

“It prints 1, 2, 3, and 4”, he confidently proclaimed.

Unfortunately this turned out to be a huge mistake, and the interview was ended immediately.

“But how?! What else would it print? I got it correctly!”, He fumed at the interviewer.

“No, you didn’t. Your foundation of JavaScript is clearly not solid enough”.

“Okay okay wait, I get it now — splice() mutates the array on every iteration right?

I see it now! I forgot that element indexes change when you remove elements.

Let me work this out:”

Plain text
iter 1: [1, 2, 3, 4], index: 0, print: 1 iter 2: [2, 3, 4], index: 1, print: 3 iter 3: [3, 4], index: 2, print: undefined iter 4: [4], index: 3, print: undefined

“Oh so it really prints 1, 3, undefined, and undefined”. He looked up in desperate reassurance.

It was the interviewer’s turn to grin.

“Wrong again”.

Alright, enough of this nonsense.

“What the hell is wrong with you?”, He boomed as he shot up from his seat.

“I’ve worked my ass off for your stupid company and I haven’t even gotten the job!

And now you’re giving me this simple question for what? To insult my intelligence? Test my confidence? You think I don’t how splice() works? Or for loops, like what do you take me for?”

“You think I give a damn about your useless organization? You think I gave up my $200K job at Google for this crap…”, the ranting and raving went on and on…

The interviewer’s smile never wavered as the candidate continued his tirade, his words increasingly filled with frustration. He stood, clenching and unclenching his fists, the tension palpable.

“I don’t need this! I’ve mastered JavaScript, built complex applications, and solved problems you couldn’t even comprehend. And now this? A trick question? Who even writes code like that in production? This is absurd!”

The interviewer calmly leaned back in his chair and crossed his arms, still maintaining that infuriating, knowing smile.

“Are you finished?”, the interviewer asked after a long pause, the candidate breathing heavily.

Jimmy glared at him, his heart racing, his fists clenched, ready for another volley. But the interviewer’s calm demeanor somehow doused the fire in his chest.

“My friend, the issue is not with splice() or how the loop behaves.

The core of the problem lies in how you’re thinking about the array’s length in the for loop”

“What the hell are you talking about?” he sat back down.

“Let me show you”, he came closer to Jimmy’s laptop.

JavaScript
const arr = [1, 2, 3, 4]; for (let i = 0; i < arr.length; i++) { console.log('here'); arr.splice(0, 1); }

“Do you see how many times the loop ran?”

“Wait… wha… how are there only 2 iterations?”, Jimmy couldn’t believe his eyes.

“You see what I said about you lacking a fundamental understanding of JavaScript? You are clueless about for loops”

“Don’t you realize that the length property is recalculated every time the loop runs?”

Jimmy’s face flushed as the realization sank in.

“So I hope you can see what really happens in the code:”

JavaScript
const arr = [1, 2, 3, 4]; for (let i = 0; i < arr.length; i++) { func(arr[i]); arr.splice(0, 1); } function func(item) { console.log(item); }

It prints just 1 and 3:

“People like you are so used to looping to a constant number. You didn’t even consider the possibility that arr.length could change, like here again:”

JavaScript
let n = 10; for (let i = 0; i < n; i++) { console.log(n--); }

Jimmy nodded slowly, his anger replaced by quiet contemplation.

“I should have seen it”.

“We all have rough moments. Take this as a chance to reflect and improve.”

Jimmy shook his hand, humbled. This interview was still over so he rose up to leave.

“Alright, so um… do I still get the job?”

The interviewer burst into a hysterical feat of laughter.

“Don’t push it my man”.

The 5 most transformative JavaScript features from ES11

JavaScript has come a long way in the past 10 years with brand new feature upgrades in each one.

Let’s take a look at the 5 most significant features that arrived in ES11; and see the ones you missed.

1. Modularization on the fly: Dynamic imports

ES11 was the awesome year when import could now act as function, like require().

An async function.

Keeping imports at the top level was no longer a must; We could now easily resolve the module’s name at compile time.

Loading modules optionally and only when needed for high-flying performance…

JavaScript
async function asyncFunc() { if (condition) { const giganticModule = await import('./gigantic-module'); } }

Loading modules based on user or variable input…

JavaScript
import minimist from 'minimist'; const argv = minimist(process.argv.slice(2)); viewModule(argv.name); async function viewModule(name) { const module = await import(name); console.log(Object.keys(module)); }

It’s also great for using ES modules that no longer support require():

JavaScript
// ❌ require() of ES modules is not supported const chalk = require('chalk'); console.log(chalk.blue('Coding Beauty')); (async () => { // ✅ Runs successfully const chalk = (await import('chalk')).default; console.log(chalk.blue('Coding Beauty')); })();

2. Promise.allSettled()

But we already had Promise.all() to wait for all the Promises in a list to resolve?

JavaScript
const responses = await Promise.all([ fetch(endpoint1), fetch(endpoint2), ]);

So what was the point of this, right?

But no, allSettled() turns out to be quite different from all().

Promise.all(): If even a single Promise in the list fails, everything fails.

But you see the problem: We’d have no idea if one failed and which succeeded.

What if you want to retry the failed errors until they do succeed? You’re stuck.

Until you turn to Promise.allSettled():

❌ Before ES11:

JavaScript
// ❌ Promise.all() async function fetchData() { const apiUrl = 'api.tariibaba.com'; const endpoint1 = `${apiUrl}/route1`; const endpoint2 = `${apiUrl}/route2`; try { const responses = await Promise.all([ fetch(endpoint1), fetch(endpoint2), ]); } catch (err) { // ❌ Which failed & which succeeded? We have no idea console.log(`error: ${err}`); } // ... }

✅ After ES11:

Promise.allSettled(): Wait for every Promise to fail or resolve.

JavaScript
// ✅ Promise.allSettled() async function fetchData() { const apiUrl = 'api.tariibaba.com'; const endpoint1 = `${apiUrl}/route1`; const endpoint2 = `${apiUrl}/route2`; try { const promises = await Promise.allSettled([ fetch(endpoint1), fetch(endpoint2), ]); const succeeded = promises.filter( (promise) => promise.status === 'fulfilled' ); const failed = promises.filter( (promise) => promise.status === 'rejected' ); // ✅ now retry failed API requests until succeeded? } catch (err) { // We don't need this anymore! console.log(`error: ${err}`); } // ... }

3. Optional chaining

?. is all over the place now but it all started from ES11.

We’ve been checking vars for null since the dawn of time but it gets pretty cumbersome when we’re null-checking nested properties.

❌ Before ES11:

JavaScript
const a = { b: { c: { d: { site: 'codingbeautydev.com', name: null, }, }, }, }; // ❌ Must check every property for null if (a && a.b && a.b.c && a.b.c && a.b.c.d.site) { console.log(a.b.c.d.site); }

✅ After ES11:

With the optional chaining operator:

JavaScript
// `?.` auto-checks every property for null // if any prop in the chain is null, short-circuit // and return null if (a?.b?.c?.d?.site) { console.log(a.b.c.d.site); }

4. Nullish coalescing

?? was one of the most impactful JavaScript additions.

A powerful to set a variable to a default value and use it at the same time.

❌ Before ES11:

We repeat the variable unnecessarily:

JavaScript
console.log(str1 ? str1 : 'codingbeautydev.com'); // coding is cool console.log(str2 ? str2 : 'codingbeautydev.com'); // codingbeautydev.com

✅ After ES12:

?? keeps things clean and readable:

JavaScript
const str1 = 'coding is cool'; const str2 = null; console.log(str1 ?? 'codingbeautydev.com'); // coding is cool console.log(str2 ?? 'codingbeautydev.com'); // codingbeautydev.com

It coalesces.

Left and right combine to form a non-null whole:

5. Go big or go home: Big Ints

The name BigInt gives it away: loading up on humongous integer values:

JavaScript
const bigInt = 240389470239846028947208942742089724204872042n; const bigInt2 = BigInt( '34028974029641089471947861048917649816048962' ); console.log(typeof bigInt); console.log(bigInt); console.log(typeof bigInt2); console.log(bigInt2); console.log(bigInt * bigInt2);

Because normal integers can’t:

JavaScript
// ✖️ Stored as double const normalInt = 240389470239846028947208942742089724204872042; const normalInt2 = 34028974029641089471947861048917649816048962; console.log(typeof normalInt); console.log(normalInt); console.log(typeof normalInt2); console.log(normalInt2); // ✖️ Precision lost console.log(normalInt * normalInt2);

Final thoughts

These are the juicy new JavaScript features that arrived in the ES12.

Use them to boost your productivity as a developer and write cleaner code with greater conciseness, expressiveness and clarity.