Today I Learned

hashrocket A Hashrocket project

236 posts about #javascript surprise

NPM script hooks

When writing script commands in your package.json you can have additional scripting before & after your script with a special incantation of script commands.

Say you have a test script

{
	"scripts": {
  	"test": "SOME TEST COMMAND"
  }
}

If you want to add a prerunner you just need to append the text pre to your command.

{
	"scripts": {
  	"pretest": "SOME COMMAND THAT HAPPENS BEFORE",
  	"test": "SOME TEST COMMAND"
  }
}

As you may have expected by now you'll do the same to get scripting after your command.

{
	"scripts": {
  	"pretest": "COMMAND THAT HAPPENS BEFORE",
  	"test": "TEST COMMAND",
    "posttest": "COMMAND THAY HAPPENS AFTER"
  }
}

This all happens via a naming scheme, so just remember pre & post.

More info in the documentation.

Debug a Jest test

Need to debug a test in Jest but can't figure out how? Or possibly you have a react-native app and you can't figure out how to debug a component?

Run jest via the node command so that the flag of --inspect-brk can be added.

node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand

From the docs:

The --runInBand cli option makes sure Jest runs the test in the same process rather than spawning processes for individual tests. Normally Jest parallelizes test runs across processes but it is hard to debug many processes at the same time.

Then open your chromium based browser (chrome, brave, etc...) and go to about:inspect. This will open the dev tools where you can select 'Open dedicated DevTools for Node'.

chrome dev tools

Then you'll see the node dev tools window open.

node dev tools

Now just enter a debugger whereever you need and run the jest command from above.

Set an input's cursor position with Javascript

If you need to hijack an input's cursor position, you can use .setSelectionRange(). This function takes two zero-based index arguments: a starting position and end position and makes makes a text-selection based on those start and end points. With this understanding, we can pass in the same index for both the start and end point to manually move the cursors position without making a selection. For example:

// move the cursor to the end of an input
inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length)

//or move the cursor to a specific index
inputElement.setSelectionRange(3, 3)

//or make a selection with a different start/end
inputElement.setSelectionRange(3, 6)

Check if a form is valid

You can check if a form passes HTML validations with javascript using the checkValidity function:

<form name="login">
 <fieldset>
   <legend>Email</legend>
   <input type="email" name="email" required />
 </fieldset>

 <fieldset>
   <legend>Password</legend>
   <input type="password" name="password" required />
 </fieldset>

 <div style="margin-top: 1rem">
   <input type="submit" value="login" />
   <button type="button" id="validate">
    validate
   </button>
 </div>
</form>

<script>
  const button = document.querySelector("#validate")
  if (button) {
    button.addEventListener("click", function() {
      alert(document.forms.login.checkValidity() ? "Form is valid" : "INVALID");
    });
  }
</script>

Sort array of numbers

In javascript, the Array sort function will cast everything to a string. So when you have an array of numbers, you need to repetitively cast them to numbers:

[1080, 720, 480].sort()
=> [1080, 480, 720]

So you need have to write your own sort function (don't mess it up!)

[1080, 720, 480].sort((a, b) => Number(a) - Number(b))
=> [480, 720, 1080]




πŸ‰

Limit Jest Test Coverage to Specific Files

Jest allows you to view test coverage when it runs.

But even if I only run tests for a specified file or files, the --coverage output will include all files.

$ jest src/directoryToTest --coverage

If your app is large, this can generate a lot of output and be difficult to parse. If I only care about the coverage for files in directoryToTest, I can filter the output of --coverage with --collectCoverageFrom=:

$ jest src/directoryToTest --coverage --collectCoverageFrom="src/directoryToTest/**"

docs

Creating Custom Typescript Types

In TypeScript you can build your own custom object types. Custom types work just like any other type. You can use it like this:

type Vehicle = {
    make: string, 
    model: string, 
    capacity: number, 
}

//now we can use the vehicle type in a definition
const corolla: Vehicle = {
    make: "Toyota",
    model: "Corolla", 
    capacity: 5,
}

If you define a vehicle without any of the required types, TypeScript will provide an error stating which property is missing from the object. For example:

const corolla: Vehicle = {
    make: "Subaru",
    model: "Outback", 
}

This definition will provide an error Property 'capacity' is missing in type '{ make: string; model: string; }' but required in type 'Vehicle'., because the capacity property is missing.

Javascript private class functions

Today I learned that you can make a class function to be private by prefixing it with a #. Check this out:

class MyClass {
  myPublicFunc() {
    console.log("=> hey myPublicFunc");
    this.#myPrivateFunc();
  }

  #myPrivateFunc() {
    console.log("=> hey myPrivateFunc");
  }
}

const myClass = new MyClass();

myClass.myPublicFunc();
// => hey myPublicFunc
// => hey myPrivateFunc

myClass.#myPrivateFunc();
// Uncaught SyntaxError:
//   Private field '#myPrivateFunc' must be declared in an enclosing class

We can also use that trick to make static methods, fields or static fields as well.

Thanks Andrew Vogel for this tip 😁

Error Handling in Typescript

Exceptions caught in a try/catch are not guaranteed to be an instance of Error class, or a child of Error . The exception caught can be anything - an object of any type, string, number, null... you name it.

So in typescript, the compiler won't like:

try {
  //...
} catch(e) {
  console.log(e.message); // => Compiler will error with `Object is of type 'unknown`
}

The typescript compiler can't infer the type of e, so defaults the type to unknown. If you know your error will have a message, you can do something like this:

try {
  //...
} catch(e) {
  const error = e as { message: string };
  console.log(error.message);
}

Lock Device Screen Orientation with JavaScript

The window object has a great API for working with screens(mobile devices, etc) and their related metadata - window.screen and window.screen.orientation.

For Mobile Devices and Full Screen browsers, you can use the following methods to toggle orientation locks:

window.screen.orientation.lock("portrait-primary")

window.screen.orientation.unlock()

Note that when you call the lock API on a web browser that is not full screen, it will raise a DOMException similar to following:

DOMException: screen.orientation.lock() is not available on this device.

Acceptable orientation values can be found in the docs linked below

https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock

Logging data as a table in the console

I'm sure you're already familiar with console.log() to debug, but did you know that there is a similar console.table() that is great for displaying arrays and objects?

Here are some examples,

Assuming you had an array of names

img

Assuming you had a person object

img

Assuming you had an array of person objects

img

There is also an optional columns parameter, which takes an array containing the names of columns you want to include in the output.

console.table(data)
console.table(data, columns)

Receive Javascript messages from iFrame

Due to security reasons what can be received from an iFrame can be very limited. However there is a utility built for the purpose of safely communicating with things like an iFrame, pop-up, etc.

An iframe can utilize the Window.postMessage() function to post info that may be relevant to someone embedding the iFrame.

Then in the page where the embedded iFrame is located, the message event can be watched.

window.addEventListener("message", (event) => {
  if (event.origin !== "IFRAME URL")
    return;

  // ...
}, false);

By utilizing the event's origin you can be even safer. Above we're declaring that if the origin of tghe event did not come from the expected iframe's url... just stop.

If you're good with the origin check then looks at the event's data property. It will provide whatever was sent via postMessage().

Listen for onfocus events on document

If you want to set an event listener on document for an input's onFocus event, you'll find there is no "onFocus" event for document. Instead you can use the focusin event listener:

document.addEventListener("focusin", function(e) {
  console.log("an input received focus");
});

So if you had dynamic events you can achieve this:

const on = (eventName, elementSelector, handler) => {
  document.addEventListener(eventName, function (e) {
    for (var target = e.target; target && target != this; target = target.parentNode) {
      if (target.matches(elementSelector)) {
        handler.call(target, e);
        break;
      }
    }
  }, false);
}

on("focusin", "#email_address", function(e) {
  const currentTarget = this;
  console.log("Email address just received focus");
});

Set the id of the root document fragment

<template id="my-template">
  <div>
    <input type="hidden" value="1" name="amount" />
  </div>
</template>

You can set the id by querying for the div:

const deep = true;
const template = document.querySelector("#my-template");
const div = document.importNode(template.content, deep);
div.querySelector("div").id = "my-id";
document.body.appendChild(div);

Output:

  <div id="my-id">
    <input type="hidden" value="1" name="amount" />
  </div>

Add TypeScript support to forms

When working with form names, it's nice to have typescript support:

interface CustomerFormType extends HTMLFormElement {
  firstName: HTMLInputElement;
  lastName: HTMLInputElement;
}


declare global {
  interface Document {
    newCustomer: CustomerFormType;
  }
}

class CustomerForm extends Component<Props, State> {
  onSubmit = (e) => {
    e.preventDefault();
    const firstName = document.newCustomer.firstName.value;
    const lastName = document.newCustomer.lastName.value;
    console.log({firstName, lastName});
  };

  render() {
    return (
      <form name="newCustomer" onSubmit={this.onSubmit}>
        <input name="firstName" type="text" />
        <input name="lastName" type="text" />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

Add global variables in typescript

In this example, we need to put placeholder values on global.window to allow us to use Ruby on Rails' ActionCable websocket framework where no window exists:

// Fix to prevent crash from ActionCable
global.window.removeEventListener = () => {};
global.window.addEventListener = () => {};

But we need to add a type:

declare global {
	var window: {
		addEventListener(): void;
		removeEventListener(): void;
	};
}

Be Careful with JavaScript Numbers

Today I Learned that you need to be careful when working with numbers in JavaScript. This is because of the way that JavaScript implements the Number type.

The JavaScript Number type is a double-precision 64-bit binary format IEEE 754 value, like double in Java or C#. This means it can represent fractional values ...

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number

Take for example the following Ruby snippet:

> 2 / 5
=> 0

Our division here returns 0, which is what I expected. And if you want the remainder, you can get it with the modulus operator %

Now here's the same snippet in JavaScript, which returns a double-precision number that is not zero

> 2 / 5
0.5

If you're looking to do integer-like division in JavaScript, here's a few ways you can accomplish that:

> Math.floor(2 / 5)
0

> Math.trunc(2 / 5)
0

> (2 / 5) >> 0
0

Docs

  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Right_shift
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc

Here's a callback to another JavaScript number TIL πŸ˜…

https://til.hashrocket.com/posts/e04ffe1d76-because-javascript

Dynamically Render Client Side with Next.js

Next.js has a handy feature for rendering a child component client side, instead of on the server.

Consider the following example, where you have a component in src/components/Analytics.js with a default export.

import dynamic from "next/dynamic";

const DynamicAnalytics = dynamic(() => import("../components/Analytics"));

function Header(props) {
  return (
    <>
     <DynamicAnalytics />
     <OtherServerRenderedStuff {...props} />
    </>
  )
}

Named Exports

You can also use this dynamic importing feature with named exports. Consider the next example:

// src/components/Analytics.js
export function Analytics() {
  return (
    // ....
  )
}
import dynamic from "next/dynamic";

const DynamicAnalytics = dynamic(
  () => import("../components/Analytics").then(mod => mod.Analytics)
);

function Header(props) {
  return (
    <>
     <DynamicAnalytics />
     <OtherServerRenderedStuff {...props} />
    </>
  )
}

There are some gotcha's when using dynamic, so make sure to check out the docs and get familiar with the API.
https://nextjs.org/docs/advanced-features/dynamic-import

Convert array to object with Lodash

I was trying to see if I could find a lodash method that works the same way as index_by from ruby and I found keyBy:

> const memberships = [{groupId: '1', status: "active"}, {groupId: '2', status: "inactive"}]
> keyBy(memberships, "groupId")

{ '1': { groupId: '1', status: 'active' },
  '2': { groupId: '2', status: 'inactive' } }

Funny thing is that on a previous version of lodash this method was called indexBy, same as the ruby version.

Javascript Privates

You can utilize 'private' features in a javascript class via the # character prepending a name. They are referred to as 'hash names'.

These private fields must be declared ahead of time otherwise they will result in a syntax error.

For this example I replaced some declaratively 'private' attributes _closed & _balance with actual 'private' attributes #closed & #balance.

export class BankAccount {
  #closed
  #balance
  
  set balance(amount) {
    return this.#balance = amount;
  }

  get opened() {
    return !this.closed;
  }

  get closed() {
    return this.#closed == true;
  }
}

It appears that if you're at Node 12 or higher you can use this functionality.

You can read more about this at MDN

Nullish coalescing operator - ??

Ever need to have a back up default value when something is null or undefined in Javascript? Well a fairly recent addition to JS has got you covered. The Nullish coalescing operator ?? may help you on your logical path.

If the first half of the epresssion is 'nullish', it will return the latter.

const defaultValue = 'I WIN'
const someVariable = null
someVariable ?? defaultValue
=> 'I WIN'

compare this to using || in which the first half of expression needs to evaluate falsey to get into the latter portion.

As it is a newer part of the JS API make sure to check for browser support. There is also a polyfill available.

TypeScript Union Types

Today I got a chance to try out the TypeScript union type. It looks like this.

interface FlashMessageWithSuccess {
  state: 'success';
  message: string;
}

interface FlashMessageWithFailure {
  state: 'failure';
  message: string;
}

export type FlashMessageInterface =
  | FlashMessageWithSuccess
  | FlashMessageWithFailure;

This lets me tell consumers of FlashMessageInterface that it is allowed to have two shapes: one with a state key of success and one with a state key of failure. I can use this to change how I present the flash message.

Yarn Upgrade to Latest ⬆️

Sometimes I just want to blow away a JavaScript library's versioning in the lockfile and go to the latest. This happens with projects still in development or requiring very stable libraries– I want to be on latest now, rather than creeping up the semantic versioning ladder. Here's how this is done with Yarn.

$ yarn upgrade --latest react

Enjoy the latest features.

Classnames Computed Keys

Here's some fun code:

import React from 'react';
import cn from 'classnames'

const App = ({ appClass }) => <div className={cn({[appClass]: !!appClass})} />

export default App

Notice the object inside className– what's going on here?

This is a dynamic class name via ES2015+ computed keys. If appClass is provided as a truthy prop, the class is enabled; if it is not provided or provided as a falsy prop, the class is not enabled.

All Jest Describes Run First

When writing a Jest test, the setup code inside any top-level describe function gets run before any scenario. So, if you setup a world in one describe, and a competing world in another, the last one that is run wins.

This even applies when you've marked a describe as 'skip' via xdescribe. The code inside that setup will still be run.

This is a first pass at a TIL, because I'm not sure the behavior I'm seeing is expected, and if so, why it is expected. It certainly surprised me 😳. I think one solution is to limit your tests to one top-level describe which has the important benefit of being more readable, too.

Fix poor type support in immutablejs

Immutablejs doesn't work very well with typescript. But I can patch types myself for some perceived type safety:


import {Map,fromJS} from 'immutable';

interface TypedMap<T> extends Map<any, any> {
  toJS(): T;
  get<K extends keyof T>(key: K, notSetValue?: T[K]): T[K];
}

interface User {
  name: string;
  points: number;
}

const users = List() as List<TypedMap<User>>;

users.forEach(u => {
  const aString = u.name
  const aNumber = u.points
});

Typescript Bang

The "non-null assertion operator" or "!" at the end of an operand is one way to tell the compiler you're sure the thing is not null, nor undefined. In some situations, the compiler can't determine things humans can.

For example, say we have a treasure map with some gold in it.

type TreasureMap = Map<string, number>

const map: TreasureMap = new Map()
const treasure = 'gold'
map.set(treasure, 10)

And we're cranking out some code that tells us if

function isItWorthIt(map: TreasureMap) { 
  return map.has(treasure) && map.get(treasure) > 9
}

Obviously we are checking if the map has gold first. If it doesn't, the execution returns early with false. If it does, then we know we can safely access the value.

But the compiler gets really upset about this:

Object is possibly 'undefined'. ts(2532)

In this case, the compiler doesn't keep map.has(treasure) in context when evaluating map.get(treasure). There is more than one way to solve this, but for now we can simply "assert" our superiority over the compiler with a bang.

return map.has(treasure) && map.get(treasure)! > 9

Typscript Docs - non-null-assertion-operator

Enzyme debug() 🐞

Debugging React unit tests can be tricky. Today I learned about the Enzyme debug() function. Here's the signature:

.debug([options]) => String

This function:

Returns an HTML-like string of the wrapper for debugging purposes. Useful to print out to the console when tests are not passing when you expect them to.

Using this in a log statement will dump a ton of valuable data into your test runner's output:

console.log(component.debug());

debug() docs

`isNaN` vs `Number.isNaN` (hint: use the latter)

Chalk this up to JavaScript is just weird. The isNaN function returns some surprising values:

> isNaN(NaN)
true

> isNaN({})
true

> isNaN('word')
true

> isNaN(true)
false

> isNaN([])
false

> isNaN(1)
false

What's going on here? Per MDN, the value is first coerced to a number (like with Number(value)) and then if the result is NaN it returns true.

Number.isNaN is a little bit more literal:

> Number.isNaN(NaN)
true

> Number.isNaN({})
false

> Number.isNaN('word')
false

> Number.isNaN(true)
false

> Number.isNaN([])
false

> Number.isNaN(1)
false

So, if you really want to know if a value is NaN use Number.isNaN

I learned about this via Lydia Hallie's Javascript Questions

Destructure into an existing array

This one's got my head spinning. Let's say you have an existing array:

const fruits = ['banana', 'apple', 'kumquat']

You can destructure right into this array.

{name: fruits[fruits.length]} = {name: 'cherry'}

//fruits is now ['banana', 'apple', 'kumquat', 'cherry']

Generally, I would think of the {name: <some var id>} = ... syntax as renaming the value that you are destructuring, but now I think of it more as defining a location that the value will be destrcutured to.

If you try to declare a new variable in the same destructuring however you will get an error if you use const:

const {name: fruits[fruits.length], color} = {name: 'cherry', color: 'red'}
// Uncaught SyntaxError: Identifier 'fruits' has already been declared

Or the new variable will go onto the global or window object if you don't use const:

{name: fruits[fruits.length], color} = {name: 'cherry', color: 'red'}
global.color
// 'red'

Output directories in Parcel v1 and Parcel v2

Parcel stated a nice piece of philosophy in their v2 README.

Instead of pulling all that configuration into Parcel, we make use of their own configuration systems.

This shows up in the difference in how output directories are handled in v1 and v2.

In v1, dist is the default output directory and can overridden with the -d flag for the build command, like this:

npx parcel build index.js
// writes to dist/index.js
npx parcel build index.js -d builds
// writes to builds/index.js

In v2, parcel reads the main key from your package.json file and uses that to configure the output path and file.

With the configuration:

// package.json
{
...
"main": "v2_builds/index.js"
...
}

parcel outputs to the specified location.

npx parcel build index.js
// writes to v2_builds/index.js

Production mode tree shaking in webpack by default

I've been experimenting with noconfig webpack (version 4.43) recently and was pleased to see tree shaking is on by default.

If I have a module maths.js:

export const add = (a, b) => a + b;

export const subtract = (a, b) => a - b;

And in my index.js file I import only add:

import { add } from './maths'

Then when I run webpack in production mode with -p, choose to display the used exports with --display-used-exports and choose to display the provided exports with --display-provided-exports then I get an output for the maths module that indicates tree shaking is taking place:

$ npx webpack -p --display-used-exports --display-provided-exports
    | ./src/maths.js 78 bytes [built]
    |     [exports: add, subtract]
    |     [only some exports used: add]

The output [only some exports used: add] indicates that subtract has not been included in the final output.

Prefer lodash-es when using webpack

The lodash package needs to be able to support all browsers, it uses es5 modules. The lodash-es package uses es6 modules, allowing for it to be tree shaked by default in webpack v4.

This import declaration:

import {join} from 'lodash';

brings in the entire lodash file, all 70K.

This import declaration:

import {join} from 'lodash-es';

brings in just the join module, which is less than 1K.

With both lodash builds you can just import the function directly:

import join from 'lodash/join';

But when using multiple lodash functions in a file you may prefer the previous import declarations to get it down to one line:

import {chunk, join, sortBy} from 'lodash-es';

If you have these declarations throughout your app, consider aliasing lodash to lodash-es in your webpack config as a quick fix.

Include vs. Includes πŸ€·β€β™‚οΈ

A hallmark of switching back and forth between Ruby and JavaScript: I mistype the respective array inclusion function a lot. In JavaScript, it's includes, and in Ruby, it's include (with a question mark). Each language does not care what I meant.

The way I remember which function to use: in Ruby, I say: "does this array include this item?". Then when I'm writing JavaScript, I remember I'm not writing Ruby. This technique is... lacking.

Chris Erin's pneumonic is better: "JavaScript has an 's' in it, so it includes. Ruby does not, so it is include."

AddEventListener doesn't duplicate named functions

When adding an event listener to a target in javascript, using the EventTarget.addEventListener function allows you to add a listener without clobbering any others that may be registered to that target. This is really nice and all, but if you have a script that may or may not be reloaded multiple times you can end up with this function getting called quite a bit. If you're like me and you use anonymous functions like they're going out of style, then you can end up attaching multiple handlers, because they aren't recognized as the same function, eg

const div = document.createElement('div');
div.addEventListener('click', event => (fetch('mutate/thing'));

This could end up running the fetch the same number of times that the script is loaded. If instead you do:

function mutateMyThings(event) { fetch('mutate/thing') };
div.addEventListener('click', mutateMyThings);

Then you don't have to worry about it, since adding a named function will automatically only add it once.

Smooth animations with Math.sin()

Remember geometry class? Well, I don't.

But I do know that when you're making a synthesizer, a sine wave produces a nice smooth sound.

And as it turns out, a sine wave has a smooth curve. So, you can use Math.sin() to animate smooth motions like so:

const domNode = document.querySelector('#waveEmoji')
function animate() {
  const a = Math.sin(Date.now() / speed) * range + offset;
  domNode.style = `transform: rotate(${a}deg);`;
  requestAnimationFrame(animate);
}
animate();

Edit Sine Wave

Variable arguments and map in JS can hurt

Given some array of numbers as strings:

const someNumStrings = ['1', '2', '05', '68'];

If you want them to be numbers, then you might be tempted to do something like:

someNumStrings.map(parseInt)

Which would be fine if parseInt didn't allow multiple arguments, and if map only sent in the single element. But that's not how it works, so what you end up getting is a bit of a mess.

[1, NaN, 0, NaN]

The parseInt function takes a radix as the second argument (and realistically anything else you want to pass to it won't cause it to explode). The Array.map method takes a callback (in this case parseInt) and gives that little sucker all the data you could want! In this case, the index is being passed as the radix, and parseInt doesn't care about the others.

TL;DR: map(el => parseInt(el)) >>>>>>>>>> map(parseInt) and if you ever intentionally encode the radix as the index of the element you're parsing... may god have mercy on your soul.

Import Absolute Paths in Typescript Jest Tests

In order to avoid this:

// project/__tests__/stuff/someDistantCousin.test.ts
import { thing } from '../../src/stuff/someDistantCousin'
import { wrapFunction } from '../testUtils/firebase'

And to write this instead:

import { thing } from 'src/stuff/someDistantCousin'
import { wrapFunction } from 'tests/testUtils/firebase'

There are 2 things to configure:

  1. Jest Config (i.e. jest.config.js, package.json, etc...)
  2. Typscript Config: tsconfig.json

jest.config.js

module.exports = {
  moduleNameMapper: {
    'src/(.*)': '<rootDir>/src/$1',
    'tests/(.*)': '<rootDir>/__tests__/$1',
  },
}

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "src/*": ["src/*"],
      "tests/*": ["__tests__/*"]
    }
  }
}

Do not prefix TypeScript interface names

TLDR: If you're writing TypeScript, you're using intellisense, and it will yell at you if you try to use an interface as a variable. So there is no need anymore for naming conventions like IMyInterface or AMyAbstractClass.

When I first started using TypeScript in 2018 I saw a lot of people's code prefixing interfaces with "I":

interface ICar {
	color: string
	horsepower: number
}

function drive(car: ICar) { /*...*/ }

However, that is a convention borrowed from the past (other languages). It used to be mentioned in the TypeScript handbook to not prefix, but that generated more argument than resolution. The handbook code examples, however, do not use prefixes.

I believe modern dev environments have relieved us from the need to include type and contextual information into the names of many code things, not just interfaces.

Now we get proper type and context from things like intellisense and language servers, whether it's inferred or strictly typed. This frees us to have names for things that can be more descriptive of the processes and data in question.

Control your magic strings in Firebase projects

Firebase's real-time database JavaScript API makes it really easy to wind up with a ton of magic strings in your codebase. Since you access data via "refs" like:

firebase.database.ref(`customers/1`).update({ name: 'Mike' })

In that example, "customers/1" is a magic string that points to some data in the database. Magic strings are well known as something to avoid in software development. And here we are updating a database with one, yikes!

These can easily be managed via patterns. I've been abstracting these into their own modules in an "API-like" pattern. For example:

// api/customers.ts
import { protectedRef } from 'protected-ref'

export const rootRef = 'customers'
export const getCustomerRef = (customerID: string) => protectedRef(rootRef, customerID)
export const updateCustomer = (customerID: string, updateDocument: CustomerUpdateDocument) => getCustomerRef(customerID).update(updateDocument)

And then use it like:

import { updateCustomer } from '../api/customers'

updateCustomer('1', { name: 'Mike' })

Also protected-ref is a firebase package that manifested from this TIL: https://til.hashrocket.com/posts/hyrbwius3s-protect-yourself-against-firebase-database-refs