Skip to content
tinytip

Learn something new in 30 seconds tinytip is a growing collection of tiny tips for frontend developers

Latest tips

React Context without default value

April 18, 2024
#react

Creating a React Context using createContext() requires a default value. If you cannot provide a useful default value, you have to make the context nullable and then, when using the context value, verify that it is not null.

const context = createContext<User | null>(null);

export function useUser() {
const value = useContext(context);
if (value === null) throw new Error("UserContext not provided");
return value;
}

Instead of repeating this boilerplate code every time, you can move this logic into a small utility function:

// Represents our empty value
const EMPTY = Symbol();

export function createRequiredContext<T>(): [Provider<T>, () => T] {
// Context, initialized with EMPTY
const context = createContext<T | typeof EMPTY>(EMPTY);

// Provider with EMPTY excluded (only T values are allowed)
const Provider = context.Provider as Provider<T>;

// Hook that throws an error if the value is EMPTY
const useStrictContext = () => {
const value = useContext(context);
if (value !== EMPTY) return value;
throw new Error("Missing context provider");
};

return [Provider, useStrictContext];
}

In our approach, we utilize a unique symbol, denoted as EMPTY, to differentiate between a default value and an actual value. This strategy provides the flexibility to create nullable contexts when necessary. You can then call the createRequiredContext function to create a new context:

const [UserProvider, useUser] = createRequiredContext<User>();

Replace any with generics, not with unknown (in many cases)

February 25, 2024
#typescript

You sure know that any in TypeScript is bad and should be avoided. But what should you replace it with?

When the type is unknown throughout the application: If the type is unknown in the entire application, such as when receiving data from an untrusted third-party API, the unknown type is your best choice. It's the type-safe counterpart of any and informs TypeScript that the type is unknown until you validate it at runtime. You can validate the type using methods like Array.isArray() or the typeof operator:

function foo(value: unknown) {
if (Array.isArray(value)) {
// We know it's an array
console.log("Number of items: " + value.length);
} else if (typeof value === "string") {
// We know it's a string
console.log(value.toLowerCase());
}
}

When the type is unknown in a specific context: Sometimes, your type might be unknown in one part of the application (where you don't care about the type) but known in another part. If you try to replace any with unknown in this case, you will likely encounter errors.

const addValidationFn = (fn: ((value: unknown) => boolean)) => { /* ... */ };

// => Doesn't work, you will get: Type 'unknown' is not assignable to type 'number'
addValidationFn((value: number) => { /* ... */ });

In this case, TypeScript is correct. Inside addValidationFn, you could call the provided function with any value because you've specified that the type is unknown, and any value is assignable to unknown. Therefore, providing a function that expects a number is not valid.

In such scenarios, you should use generics:

const addValidationFn = <V,>(fn: ((value: V) => boolean)) => { /* ... */ };

// => This works
addValidationFn((value: number) => { /* ... */ });

Generics allow you to capture the type information while maintaining type safety. In the above example, V is a placeholder for any type, and it will be determined when addValidationFn is called.

Show DOM nodes in React DevTools

October 14, 2023
#react

The React Developer Tools extension for your browser allows you to inspect your React application and see the component tree. By default, the components tree only renders your React components but not the DOM nodes. However, you can change the default filter and show DOM nodes as well.

Follow these steps to show DOM nodes in the components tree:

  1. Navigate the React Components tab in the DevTools
  2. Click on the settings icon in the top right corner of the tree panel
  3. Navigate to the Components tab in the settings overlay
  4. Disable the toggle in the Hide components where... section.

Query selectors are applied to the whole document, unless you use :scope

July 3, 2023
#javascript

Here's a quiz for you: Given the HTML below, what element will be returned by the querySelector() call? Note that the .wrapper is outside the .main element.

<div class="wrapper">
<div class="main">
<div class="content"></div>
</div>
</div>
const main = document.querySelector(".main");
const content = main.querySelector(".wrapper .content");
// What's the result???

Don't cheat! Think about it for a second.


If you call querySelector() or querySelectorAll() on an element, you will only get elements within that element. However, the query itself will be applied to the whole document. So the above code will return the <div class="content"></div> node.

You can include the :scope pseudo-class to apply the query to the element itself:

const main = document.querySelector(".main");
const content = main.querySelector(":scope .wrapper .content");
// Result: null

This is also useful to query direct children of an element:

const main = document.querySelector(".main");
const content = main.querySelector(":scope > .child");
// --> Does not include nested .child elements

Scope your CSS imports with meta.load-css in Sass

June 27, 2023
#scss

In contrast to the old @import rule to import SCSS files in Sass, the newer @use rule can only be used at the top level of a file, before any other statements. Using it inside a CSS selector is not allowed. For example:

.dark-theme {
// Works
@import "./dark-theme.scss";

// Does NOT work
@use "./dark-theme.scss";
}

But there is still a way for scoped imports. The meta.load-css function allows you to import a SCSS file inside a CSS selector and scope it to that selector:

@use "sass:meta";

.dark-theme {
@include meta.load-css("./dark-theme.scss");
}

Unlike @import, meta.load-css does not affect the parent selector & in the imported file. Given the following file:

// dark-theme.scss
button {
& + & {
margin-inline-start: 1rem;
}
}

Importing it with @import would result in a CSS selector that is technically valid but likely not what you want:

// Using @import results in:
.dark-theme button + .dark-theme button {
margin-inline-start: 1rem;
}

Using meta.load-css however does not break the parent selector:

// Using meta.load-css results in:
.dark-theme button + button {
margin-inline-start: 1rem;
}