Skip to content

Using Lottie Animations for UI Components in React

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Lottie is a library created by Airbnb that allows developers to display animations created in Adobe After Effects on the web and in mobile apps. This allows developers to use more more dynamic and detailed animations for their apps than would normally be possible with, for example, CSS animations.

Button icons are one great example of where a lottie file can improve the animated user experience. Here I'll be integrating an animation for toggling a hamburger menu into a React app.

In order to use lottie files in my app, I'll first need to install the lottie-web package.

npm install lottie-web

Next, I'll need an animation file. Lottie files are exported from After Effects with the BodyMovin plugin, and are saved as JSON files. If you don't have your own After Effects animations, there are many free lottie files available online. I'm going to use this hamburger menu toggle animation by Christopher Deane.

The lottie-web library works by creating an object that loads the animation. That object takes a container element where the animation will be created, then exposes methods like play and setDirection for running the animation. To make this work in React, we'll have to use the useRef hook.

First, let's create a basic button component.

import React from 'react';

const MenuToggleButton = ({open, setOpen}) => {
  return (
    <button onClick={() => setOpen(!open)} />
  );
};

export default MenuToggleButton;

This button expects to receive the open status of the menu it controls as a boolean, and the state setting function to change that status on click. Next, we'll add our animation.

import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import lottie from 'lottie-web/build/player/lottie_light';
import animationData from './animationData.json';

const MenuToggleButton = ({ open, setOpen }) => {
  const animationContainer = useRef(null);
  const anim = useRef(null);

  useEffect(() => {
    if (animationContainer.current) {
      anim.current = lottie.loadAnimation({
        container: animationContainer.current,
        renderer: 'svg',
        loop: false,
        autoplay: false,
        animationData,
      });

      return () => anim.current?.destroy();
    }
  }, []);

  return (
    <button
      onClick={() => setOpen(!open)}
      ref={animationContainer}
    />
  );
};

MenuToggleButton.propTypes = {
  open: PropTypes.bool.isRequired,
  setOpen: PropTypes.func.isRequired
}

export default MenuToggleButton;

We're importing the lottie-web lightweight animation player, since the full package can be large, and we don't need all of it. The lottie file we're using is rather small, so I've chosen to import the JSON file directly rather than fetch it. How you implement that may depend on the size of your animation.

We've also created two refs: one for the containing button element, and one for the animation object itself. We then use useEffect to create the animation object if the containing element has been created. Our animation object has a few important settings: it is set to not loop and not autoplay. Because we want this animation to respond to user interaction, it wouldn't make a lot of sense to allow it to automatically start playing an endless open-and-close animation loop.

Lastly, we need to make the animation respond to clicking the button. We want the animation to play from one end to the other without looping, and then play back the animation in reverse when the button state is toggled back. We will set this inside the button's onClick.

<button
  onClick={() => {
    setOpen(!open);
    anim.current?.setDirection(open ? -1 : 1);
    anim.current?.play();
  }}
  ref={animationContainer}
/>

Live Demo

Our button is now fully functional, and our animation is ready to play when a user clicks it. To see it in action, check out this live example.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

The Future of Dates in JavaScript: Introducing Temporal cover image

The Future of Dates in JavaScript: Introducing Temporal

The Future of Dates in JavaScript: Introducing Temporal What is Temporaal? Temporal is a proposal currently at stage 3 of the TC39 process. It's expected to revolutionize how we handle dates in JavaScript, which has always been a challenging aspect of the language. But what does it mean that it's at stage 3 of the process? * The specification is complete * It has been reviewed * It's unlikely to change significantly at this point Key Features of Temporal Temporal introduces a new global object with a fresh API. Here are some important things to know about Temporal: 1. All Temporal objects are immutable 2. They're represented in local calendar systems, but can be converted 3. Time values use 24-hour clocks 4. Leap seconds aren't represented Why Do We Need Temporal? The current Date object in JavaScript has several limitations: * No support for time zones other than the user's local time and UTC * Date objects can be mutated * Unpredictable behavior * No support for calendars other than Gregorian * Daylight savings time issues While some of these have workarounds, not all can be fixed with the current Date implementation. Let's see some useful examples where Temporal will improve our lives: Some Examples Creating a day without a time zone is impossible using Date, it also adds time beyond the date. Temporal introduces PlainDate to overcome this. ` But what if we want to add timezone information? Then we have ZonedDateTime for this purpose. The timezone must be added in this case, as it also allows a lot of flexibility when creating dates. ` Temporal is very useful when manipulating and displaying the dates in different time zones. ` Let's try some more things that are currently difficult or lead to unexpected behavior using the Date object. Operations like adding days or minutes can lead to inconsistent results. However, Temporal makes these operations easier and consistent. ` Another interesting feature of Temporal is the concept of Duration, which is the difference between two time points. We can use these durations, along with dates, for arithmetic operations involving dates and times. Note that Durations are serialized using the ISO 8601 duration format ` Temporal Objects We've already seen some of the objects that Temporal exposes. Here's a more comprehensive list. * Temporal * Temporal.Duration` * Temporal.Instant * Temporal.Now * Temporal.PlainDate * Temporal.PlainDateTime * Temporal.PlainMonthDay * Temporal.PlainTime * Temporal.PlainYearMonth * Temporal.ZonedDateTime Try Temporal Today If you want to test Temporal now, there's a polyfill available. You can install it using: ` Note that this doesn't install a global Temporal object as expected in the final release, but it provides most of the Temporal implementation for testing purposes. Conclusion Working with dates in JavaScript has always been a bit of a mess. Between weird quirks in the Date object, juggling time zones, and trying to do simple things like “add a day,” it’s way too easy to introduce bugs. Temporal is finally fixing that. It gives us a clear, consistent, and powerful way to work with dates and times. If you’ve ever struggled with JavaScript dates (and who hasn’t?), Temporal is definitely worth checking out....

The 2025 Guide to JS Build Tools cover image

The 2025 Guide to JS Build Tools

The 2025 Guide to JS Build Tools In 2025, we're seeing the largest number of JavaScript build tools being actively maintained and used in history. Over the past few years, we've seen the trend of many build tools being rewritten or forked to use a faster and more efficient language like Rust and Go. In the last year, new companies have emerged, even with venture capital funding, with the goal of working on specific sets of build tools. Void Zero is one such recent example. With so many build tools around, it can be difficult to get your head around and understand which one is for what. Hopefully, with this blog post, things will become a bit clearer. But first, let's explain some concepts. Concepts When it comes to build tools, there is no one-size-fits-all solution. Each tool typically focuses on one or two primary features, and often relies on other tools as dependencies to accomplish more. While it might be difficult to explain here all of the possible functionalities a build tool might have, we've attempted to explain some of the most common ones so that you can easily understand how tools compare. Minification The concept of minification has been in the JavaScript ecosystem for a long time, and not without reason. JavaScript is typically delivered from the server to the user's browser through a network whose speed can vary. Thus, there was a need very early in the web development era to compress the source code as much as possible while still making it executable by the browser. This is done through the process of *minification*, which removes unnecessary whitespace, comments, and uses shorter variable names, reducing the total size of the file. This is what an unminified JavaScript looks like: ` This is the same file, minified: ` Closely related to minimizing is the concept of source maps#Source_mapping), which goes hand in hand with minimizing - source maps are essentially mappings between the minified file and the original source code. Why is that needed? Well, primarily for debugging minified code. Without source maps, understanding errors in minified code is nearly impossible because variable names are shortened, and all formatting is removed. With source maps, browser developer tools can help you debug minified code. Tree-Shaking *Tree-shaking* was the next-level upgrade from minification that became possible when ES modules were introduced into the JavaScript language. While a minified file is smaller than the original source code, it can still get quite large for larger apps, especially if it contains parts that are effectively not used. Tree shaking helps eliminate this by performing a static analysis of all your code, building a dependency graph of the modules and how they relate to each other, which allows the bundler to determine which exports are used and which are not. Once unused exports are found, the build tool will remove them entirely. This is also called *dead code elimination*. Bundling Development in JavaScript and TypeScript rarely involves a single file. Typically, we're talking about tens or hundreds of files, each containing a specific part of the application. If we were to deliver all those files to the browser, we would overwhelm both the browser and the network with many small requests. *Bundling* is the process of combining multiple JS/TS files (and often other assets like CSS, images, etc.) into one or more larger files. A bundler will typically start with an entry file and then recursively include every module or file that the entry file depends on, before outputting one or more files containing all the necessary code to deliver to the browser. As you might expect, a bundler will typically also involve minification and tree-shaking, as explained previously, in the process to deliver only the minimum amount of code necessary for the app to function. Transpiling Once TypeScript arrived on the scene, it became necessary to translate it to JavaScript, as browsers did not natively understand TypeScript. Generally speaking, the purpose of a *transpiler* is to transform one language into another. In the JavaScript ecosystem, it's most often used to transpile TypeScript code to JavaScript, optionally targeting a specific version of JavaScript that's supported by older browsers. However, it can also be used to transpile newer JavaScript to older versions. For example, arrow functions, which are specified in ES6, are converted into regular function declarations if the target language is ES5. Additionally, a transpiler can also be used by modern frameworks such as React to transpile JSX syntax (used in React) into plain JavaScript. Typically, with transpilers, the goal is to maintain similar abstractions in the target code. For example, transpiling TypeScript into JavaScript might preserve constructs like loops, conditionals, or function declarations that look natural in both languages. Compiling While a transpiler's purpose is to transform from one language to another without or with little optimization, the purpose of a *compiler* is to perform more extensive transformations and optimizations, or translate code from a high-level programming language into a lower-level one such as bytecode. The focus here is on optimizing for performance or resource efficiency. Unlike transpiling, compiling will often transform abstractions so that they suit the low-level representation, which can then run faster. Hot-Module Reloading (HMR) *Hot-module reloading* (HMR) is an important feature of modern build tools that drastically improves the developer experience while developing apps. In the early days of the web, whenever you'd make a change in your source code, you would need to hit that refresh button on the browser to see the change. This would become quite tedious over time, especially because with a full-page reload, you lose all the application state, such as the state of form inputs or other UI components. With HMR, we can update modules in real-time without requiring a full-page reload, speeding up the feedback loop for any changes made by developers. Not only that, but the full application state is typically preserved, making it easier to test and iterate on code. Development Server When developing web applications, you need to have a locally running development server set up on something like http://localhost:3000. A development server typically serves unminified code to the browser, allowing you to easily debug your application. Additionally, a development server will typically have hot module replacement (HMR) so that you can see the results on the browser as you are developing your application. The Tools Now that you understand the most important features of build tools, let's take a closer look at some of the popular tools available. This is by no means a complete list, as there have been many build tools in the past that were effective and popular at the time. However, here we will focus on those used by the current popular frameworks. In the table below, you can see an overview of all the tools we'll cover, along with the features they primarily focus on and those they support secondarily or through plugins. The tools are presented in alphabetical order below. Babel Babel, which celebrated its 10th anniversary since its initial release last year, is primarily a JavaScript transpiler used to convert modern JavaScript (ES6+) into backward-compatible JavaScript code that can run on older JavaScript engines. Traditionally, developers have used it to take advantage of the newer features of the JavaScript language without worrying about whether their code would run on older browsers. esbuild esbuild, created by Evan Wallace, the co-founder and former CTO of Figma, is primarily a bundler that advertises itself as being one of the fastest bundlers in the market. Unlike all the other tools on this list, esbuild is written in Go. When it was first released, it was unusual for a JavaScript bundler to be written in a language other than JavaScript. However, this choice has provided significant performance benefits. esbuild supports ESM and CommonJS modules, as well as CSS, TypeScript, and JSX. Unlike traditional bundlers, esbuild creates a separate bundle for each entry point file. Nowadays, it is used by tools like Vite and frameworks such as Angular. Metro Unlike other build tools mentioned here, which are mostly web-focused, Metro's primary focus is React Native. It has been specifically optimized for bundling, transforming, and serving JavaScript and assets for React Native apps. Internally, it utilizes Babel as part of its transformation process. Metro is sponsored by Meta and actively maintained by the Meta team. Oxc The JavaScript Oxidation Compiler, or Oxc, is a collection of Rust-based tools. Although it is referred to as a compiler, it is essentially a toolchain that includes a parser, linter, formatter, transpiler, minifier, and resolver. Oxc is sponsored by Void Zero and is set to become the backbone of other Void Zero tools, like Vite. Parcel Feature-wise, Parcel covers a lot of ground (no pun intended). Largely created by Devon Govett, it is designed as a zero-configuration build tool that supports bundling, minification, tree-shaking, transpiling, compiling, HMR, and a development server. It can utilize all the necessary types of assets you will need, from JavaScript to HTML, CSS, and images. The core part of it is mostly written in JavaScript, with a CSS transformer written in Rust, whereas it delegates the JavaScript compilation to a SWC. Likewise, it also has a large collection of community-maintained plugins. Overall, it is a good tool for quick development without requiring extensive configuration. Rolldown Rolldown is the future bundler for Vite, written in Rust and built on top of Oxc, currently leveraging its parser and resolver. Inspired by Rollup (hence the name), it will provide Rollup-compatible APIs and plugin interface, but it will be more similar to esbuild in scope. Currently, it is still in heavy development and it is not ready for production, but we should definitely be hearing more about this bundler in 2025 and beyond. Rollup Rollup is the current bundler for Vite. Originally created by Rich Harris, the creator of Svelte, Rollup is slowly becoming a veteran (speaking in JavaScript years) compared to other build tools here. When it originally launched, it introduced novel ideas focused on ES modules and tree-shaking, at the time when Webpack as its competitor was becoming too complex due to its extensive feature set - Rollup promised a simpler way with a straightforward configuration process that is easy to understand. Rolldown, mentioned previously, is hoped to become a replacement for Rollup at some point. Rsbuild Rsbuild is a high-performance build tool written in Rust and built on top of Rspack. Feature-wise, it has many similiarities with Vite. Both Rsbuild and Rspack are sponsored by the Web Infrastructure Team at ByteDance, which is a division of ByteDance, the parent company of TikTok. Rsbuild is built as a high-level tool on top of Rspack that has many additional features that Rspack itself doesn't provide, such as a better development server, image compression, and type checking. Rspack Rspack, as the name suggests, is a Rust-based alternative to Webpack. It offers a Webpack-compatible API, which is helpful if you are familiar with setting up Webpack configurations. However, if you are not, it might have a steep learning curve. To address this, the same team that built Rspack also developed Rsbuild, which helps you achieve a lot with out-of-the-box configuration. Under the hood, Rspack uses SWC for compiling and transpiling. Feature-wise, it’s quite robust. It includes built-in support for TypeScript, JSX, Sass, Less, CSS modules, Wasm, and more, as well as features like module federation, PostCSS, Lightning CSS, and others. Snowpack Snowpack was created around the same time as Vite, with both aiming to address similar needs in modern web development. Their primary focus was on faster build times and leveraging ES modules. Both Snowpack and Vite introduced a novel idea at the time: instead of bundling files while running a local development server, like traditional bundlers, they served the app unbundled. Each file was built only once and then cached indefinitely. When a file changed, only that specific file was rebuilt. For production builds, Snowpack relied on external bundlers such as Webpack, Rollup, or esbuild. Unfortunately, Snowpack is a tool you’re likely to hear less and less about in the future. It is no longer actively developed, and Vite has become the recommended alternative. SWC SWC, which stands for Speedy Web Compiler, can be used for both compilation and bundling (with the help of SWCpack), although compilation is its primary feature. And it really is speedy, thanks to being written in Rust, as are many other tools on this list. Primarily advertised as an alternative to Babel, its SWC is roughly 20x faster than Babel on a single thread. SWC compiles TypeScript to JavaScript, JSX to JavaScript, and more. It is used by tools such as Parcel and Rspack and by frameworks such as Next.js, which are used for transpiling and minification. SWCpack is the bundling part of SWC. However, active development within the SWC ecosystem is not currently a priority. The main author of SWC now works for Turbopack by Vercel, and the documentation states that SWCpack is presently not in active development. Terser Terser has the smallest scope compared to other tools from this list, but considering that it's used in many of those tools, it's worth separating it into its own section. Terser's primary role is minification. It is the successor to the older UglifyJS, but with better performance and ES6+ support. Vite Vite is a somewhat of a special beast. It's primarily a development server, but calling it just that would be an understatement, as it combines the features of a fast development server with modern build capabilities. Vite shines in different ways depending on how it's used. During development, it provides a fast server that doesn't bundle code like traditional bundlers (e.g., Webpack). Instead, it uses native ES modules, serving them directly to the browser. Since the code isn't bundled, Vite also delivers fast HMR, so any updates you make are nearly instant. Vite uses two bundlers under the hood. During development, it uses esbuild, which also allows it to act as a TypeScript transpiler. For each file you work on, it creates a file for the browser, allowing an easy separation between files which helps HMR. For production, it uses Rollup, which generates a single file for the browser. However, Rollup is not as fast as esbuild, so production builds can be a bit slower than you might expect. (This is why Rollup is being rewritten in Rust as Rolldown. Once complete, you'll have the same bundler for both development and production.) Traditionally, Vite has been used for client-side apps, but with the new Environment API released in Vite 6.0, it bridges the gap between client-side and server-rendered apps. Turbopack Turbopack is a bundler, written in Rust by the creators of webpack and Next.js at Vercel. The idea behind Turbopack was to do a complete rewrite of Webpack from scratch and try to keep a Webpack compatible API as much as possible. This is not an easy feat, and this task is still not over. The enormous popularity of Next.js is also helping Turbopack gain traction in the developer community. Right now, Turbopack is being used as an opt-in feature in Next.js's dev server. Production builds are not yet supported but are planned for future releases. Webpack And finally we arrive at Webpack, the legend among bundlers which has had a dominant position as the primary bundler for a long time. Despite the fact that there are so many alternatives to Webpack now (as we've seen in this blog post), it is still widely used, and some modern frameworks such as Next.js still have it as a default bundler. Initially released back in 2012, its development is still going strong. Its primary features are bundling, code splitting, and HMR, but other features are available as well thanks to its popular plugin system. Configuring Webpack has traditionally been challenging, and since it's written in JavaScript rather than a lower-level language like Rust, its performance lags behind compared to newer tools. As a result, many developers are gradually moving away from it. Conclusion With so many build tools in today's JavaScript ecosystem, many of which are similarly named, it's easy to get lost. Hopefully, this blog post was a useful overview of the tools that are most likely to continue being relevant in 2025. Although, with the speed of development, it may as well be that we will be seeing a completely different picture in 2026!...

JavaScript Errors: An Introductory Primer cover image

JavaScript Errors: An Introductory Primer

JavaScript Errors are an integral part of the language, and its runtime environment. They provide valuable feedback when something goes wrong during the execution of your script. And once you understand how to use and handle Errors, you'll find them a much better debugging tool than always reaching for console.log. Why Use Errors? When JavaScript throws errors, it's usually because there's a mistake in the code. For example, trying to access properties of null or undefined would throw a TypeError. Or trying to use a variable before it has been declared would throw a ReferenceError. But these can often be caught before execution by properly linting your code. More often, you'll want to create your own errors in your programs to catch problems unique to what you're trying to build. Throwing your own errors can make it easier for you to interrupt the control flow of your code when necessary conditions aren't met. Why would you want to use Error instead of just console.logging all sorts of things? Because an Error will force you to address it. JavaScript is optimistic. It will do its best to execute despite all sorts of issues in the code. Just logging some problem might not be enough to notice it. You could end up with subtle bugs in your program and not know! Using console.log won't stop your program from continuing to execute. An Error, however, interrupts your program. It tells JavaScript, "we can't proceed until we've fixed this problem". And then JavaScript happily passes the message on to you! Using Errors in JavaScript Here's an example of throwing an error: ` When an Error is thrown, nothing after that throw in your scope will be executed. JavaScript will instead pass the Error to the nearest error handler higher up in the call stack. If no handler is found, the program terminates. Since you probably don't want your programs to crash, it's important to set up Error handling. This is where something like try / catch comes in. Any code you write inside the try will attempt to execute. If an Error is thrown by anything inside the try, then the following catch block is where you can decide how to handle that Error. ` In the catch block, you receive an error object (by convention, this is usually named error or err, but you could give it any name) which you can then handle as needed. Asynchronous Code and Errors There are two ways to write asynchronous JavaScript, and they each have their own way of writing Error handling. If you're using async / await, you can use the try / catch block as in the previous example. However, if you're using Promises, you'll want to chain a catch to the Promise like so: ` Understanding the Error Object The Error object is a built-in object that JavaScript provides to handle runtime errors. All types of errors inherit from it. Error has several useful properties. - message: Probably the most useful of Error's properties, message is a human-readable description of the error. When creating a new Error, the string you pass will become the message. - name: A string representing the error type. By default, this is Error. If you're using a built-in sub-class of Error like TypeError, it will be that instead. Otherwise, if you're creating a custom type of Error, you'll need to set this in the constructor. - stack: While technically non-standard, stack is a widely supported property that gives a full stack trace of where the error was created. - cause: This property allows you to give more specific data when throwing an error. For example, if you want to add a more detailed message to a caught error, you could throw a new Error with your message and pass the original Error as the cause. Or you could add structured data for easier error analysis. Creating an Error object is quite straightforward: ` In addition to the generic Error, JavaScript provides several built-in sub-classes of Error: - EvalError: Thrown when a problem occurs with the eval() function. This only exists for backwards compatibility, and will not be thrown by JavaScript. You shouldn't use eval() anyway. - InternalError: Non-standard error thrown when something goes wrong in the internals of the JavaScript engine. Really only used in Firefox. - RangeError: Thrown when a value is not within the expected range. - ReferenceError: Thrown when a value doesn't exist yet / hasn't been initialized. - SyntaxError: Thrown when a parsing error occurs. - TypeError: Thrown when a variable is not of the expected type. - URIError: Thrown when using a global URI handling function incorrectly. Each of these error types inherits from the Error object and generally adds no additional properties or methods, but they do change the name property to reflect the error type. Making Your Own Custom Errors It's sometimes useful to extend the Error object yourself! This lets you add properties to particular Errors you throw, as well as easily check in catch blocks if an Error is of a particular type. To extend Error, use a class. ` In this example, CustomError extends the Error class. It changes the name to CustomError and gives it the new property foo: 'bar'. You can then throw your CustomError, check if the error in your catch block is an instance of the CustomError, and access the properties associated with your CustomError. This gives you a lot more control over how Errors are structured and validated, which could greatly aid with debugging because your errors won't all just be Errors. Common Confusions There are many ways that using Errors can go subtly wrong. Here are some of the common issues to keep in mind when working with Error. Failure to Catch When an Error is thrown, the program will cease executing anything else in its scope and start working its way back up the call stack until it finds a catch block to deal with the Error. If it never finds a catch, the program will crash. So it's important to make sure you actually catch your errors, or else you might terminate your program unintentionally for small and recoverable issues. It's especially helpful to think about catching errors when you're executing code from external libraries which you don't control. You may import a function to handle something for you, and it unexpectedly throws an Error. You should anticipate this possibility and ask yourself: "Should this code go inside of a try / catch block?" to prevent an error like this from crashing your code. Network Requests Don't Throw on 400 and 500 Statuses You might want to make a request to an API, and then handle an error if the request fails. ` Maybe you made a bad request and got back a 400. Maybe you're not properly authenticated and got a 401 or 403. Maybe the endpoint is invalid and you get a 404. Or maybe the server is having a bad day and you get a 500. In none of those cases will you get an Error! From JavaScript's point of view, your request worked. You sent some data to a place, and the place sent you something back. Mission accomplished! Except it's not. You need to deal with these HTTP error statuses. So if you want to handle responses that aren't OK, you need to do it explicitly. To fix the previous example: ` You Can Throw Anything You should throw new Errors. But you could throw 'literally anything'. There's nothing forcing you to only throw an Error. However, it's a lot harder to handle your errors if there's no consistency in what to expect in your catch blocks. It's a best practice to only throw an Error and not any other kind of JavaScript object. This problem becomes especially clear in TypeScript, when the default type of an error in a catch block is not Error, but unknown. TypeScript has no way to know if an error passed into the catch is going to actually be an Error or not, which can make it more frustrating to write error handling code. For this reason, it's often a good idea to check what exactly you've received before trying to handle it. ` (Alas, you cannot throw 🥳. That's a SyntaxError. But throw new Error('🥳') is still perfectly valid!) Conclusion Wielding JavaScript Errors is a big upgrade from console.logging all the things and hoping for the best. And it's not very hard to do it well! By using Error, your apps will be much more explicit in how you expect things to work, and how you expect things might not work. And when something does go wrong, you'll be more likely to notice and better equipped to figure out the problem....

The Quirks And Gotchas of PHP cover image

The Quirks And Gotchas of PHP

The Quirks And Gotchas of PHP If you come from a JavaScript background, you'll likely be familiar with some of its famous quirks, such as 1 + "1" equaling "11". Well, PHP has its own set of quirks and gotchas, too. Some are oddly similar to JavaScript's, while others can surprise a JavaScript developer. Let's start with the more familiar ones. 1. Type Juggling and Loose Comparisons Like JavaScript, PHP has two types of comparison operators: strict and loose. The loose comparison operator in PHP uses ==, while the strict comparison operator uses ===. Here's an example of a loose vs. strict comparison in PHP: ` PHP is a loosely typed language, meaning it will automatically convert variables from one type to another when necessary, just like JavaScript. This is not only when doing comparisons but also, for example, when doing numeric operations. Such conversions can lead to some unexpected results if you're not careful: ` As you can see, the type system has gotten a bit stricter in PHP 8, so it won't let you commit some of the "atrocities" that were possible in earlier versions, throwing a TypeError instead. PHP 8 introduced many changes that aim to eliminate some of the unpredictable behavior; we will cover some of them throughout this article. 1.1. Truthiness of Strings This is such a common gotcha in PHP that it deserves its own heading. By default, PHP considers an empty string as false and a non-empty string as true: ` But wait, there's more! PHP also considers the string "0" as false: ` You might think we're done here, but no! Try comparing a string such as "php" to 0: ` Until PHP7, any non-numeric string was converted to 0 when cast to an integer to compare it to the other integer. That's why this example will be evaluated as true. This quirk has been fixed in PHP 8. For a comprehensive comparison table of PHP's truthiness, check out the PHP documentation. 1.2. Switch Statements Switch statements in PHP use loose comparisons, so don't be surprised if you see some unexpected behavior when using them: ` The New Match Expression in PHP 8 PHP 8 introduced the match expression, which is similar to switch but uses strict comparisons (i.e., === under the hood) and returns a value: ` Unlike switch, there is no "fall-through" behavior in match, and each branch must return a value, making match a great alternative when you need a more precise or concise form of branching—especially if you want to avoid the loose comparisons of a traditional switch. 1.3 String to Number Conversion In earlier versions of PHP, string-to-number conversions were often done silently, even if the string wasn’t strictly numeric (like '123abc'). In PHP 7, this would typically result in 123 plus a Notice: ` In PHP 8, you’ll still get int(123), but now with a Warning, and in other scenarios (like extremely malformed strings), you might see a TypeError. This stricter behavior can reveal hidden bugs in code that relied on implicit type juggling. Stricter Type Checks & Warnings in PHP 8 - Performing arithmetic on non-numeric strings: As noted, in older versions, something like "123abc" + 0 would silently drop the non-numeric part, often producing 123 plus a PHP Notice. In PHP 8, such operations throw a more visible Warning or TypeError, depending on the exact scenario. - Null to Non-Nullable Internal Arguments: Passing null to a function parameter that’s internally declared as non-nullable will trigger a TypeError in PHP 8. Previously, this might have been silently accepted or triggered only a warning. - Internal Function Parameter Names: PHP 8 introduced named arguments but also made internal parameter names part of the public API. If you use named arguments with built-in functions, be aware that renaming or reordering parameters in future releases might break your code. Always match official parameter names as documented in the PHP manual. Union Types & Mixed Since PHP 8.0, we can declare union types, which allows you to specify that a parameter or return value can be one of multiple types. For example: ` Specifying the union of types your function accepts can help clarify your code’s intent and reveal incompatibilities if your existing code relies on looser type checking, preventing some of the conversion quirks we’ve discussed. 2. Operator Precedence and Associativity Operator precedence can lead to confusing situations if you’re not careful with parentheses. For instance, the . operator (string concatenation similar to + in JavaScript) has left-to-right associativity, but certain logical operators have lower precedence than assignment or concatenation, leading to puzzling results in PHP 7 and earlier: ` PHP 8 has fixed this issue by making the + and - operators take a higher precedence. 3. Variable Variables and Variable Functions Now, we're getting into unfamiliar territory as JavaScript Developers. PHP allows you to define variable variables and variable functions. This can be a powerful feature, but it can also lead to some confusing code: ` In this example, the variable $varName contains the string 'hello'. By using $$varName, we're creating a new variable with the name 'hello' and assigning it the value 'world'. Similarly, you can create variable functions: ` 4. Passing Variables by Reference You can pass variables by reference using the & operator in PHP. This means that any changes made to the variable inside the function will be reflected outside the function: ` While this example is straightforward, not knowing the pass-by-reference feature can lead to some confusion, and bugs can arise when you inadvertently pass variables by reference. 5. Array Handling PHP arrays are a bit different from JavaScript arrays. They can be used as both arrays and dictionaries, and they have some quirks that can catch you off guard. For example, if you try to access an element that doesn't exist in an array, PHP will return null instead of throwing an error: ` Furthermore, PHP arrays can contain both numerical and string keys at the same time, but numeric string keys can sometimes convert to integers, depending on the context> ` In this example: - "1" (string) and 1 (integer) collide, resulting in the array effectively having only one key: 1. - true is also cast to 1 as an integer, so it overwrites the same key. And last, but not least, let's go back to the topic of passing variables by reference. You can assign an array element by reference, which can feel quite unintuitive: ` 6 Checking for Variable Truthiness (isset, empty, and nullsafe operator) In PHP, you can use the empty() function to check if a variable is empty. But what does "empty" mean in PHP? The mental model of what's considered "empty" in PHP might differ from what you're used to in JavaScript. Let's clarify this: The following values are considered empty by the empty() function: - "" (an empty string) - 0 (0 as an integer) - 0.0 (0 as a float) - "0" (0 as a string) - null - false - [] (an empty array) This means that the following values are not considered empty: - "0" (a string containing "0") - " " (a string containing a space) - 0.0 (0 as a float) - new stdClass() (an empty object) Keep this in mind when using empty() in your code, otherwise, you might end up debugging some unexpected behavior. Undefined Variables and isset() Another little gotcha is that you might expect empty() to return true for undefined variables too - they contain nothing after all, right? Unfortunately, empty() will throw a notice in such case. To account for undefined variables, you may want to use the isset() function, which checks if a variable is set and not null: ` The Nullsafe Operator If you have a chain of properties or methods that you want to access, you may tend to check each step with isset() to avoid errors: ` In fact, because isset() is a special language construct and it doesn't fully evaluate an undefined part of the chain, it can be used to evaluate the whole chain at once: ` That's much nicer! However, it could be even more elegant with the nullsafe operator (?->) introduced in PHP 8: ` If you’ve used optional chaining in JavaScript or other languages, this should look familiar. It returns null if any part of the chain is null, which is handy but can also hide potential logic mistakes — if your application logic expects objects to exist, silently returning null may lead to subtle bugs. Conclusion While PHP shares a few loose typing quirks with JavaScript, it also has its own distinctive behaviors around type juggling, operator precedence, passing by reference, and array handling. Becoming familiar with these nuances — and with the newer, more predictable features in PHP 8 — will help you avoid subtle bugs and write clearer, more robust code. PHP continues to evolve, so always consult the official documentation to stay current on best practices and language changes....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co