Thursday, June 8, 2023

Using Zustand, TypeScript and Immer together

In React land, Redux nightmare finally came to an end with the inception of Zustand, which throws away all the chores we had to previously do, leaving us with a simple workflow of minimal code. On top of that, TypeScript allows us to relax our brains with some degree of strong typing.

When dealing with our Zustand store, just like Redux, we have to manipulate a chunk of immutable data, which becomes more and more annoying as we deepen the object structure. And, when it comes to dealing immutable data, Immer is everyone’s best friend – just slap a produce call and you can go home earlier.

Now here comes the question: how to use Immer inside our Zustand store?

Turns out Zustand provides a few middlewares – functions which lie between the data and your manipulation functions –, and among them an Immer middleware. Using the middleware has two advantages:

  • you don’t need to explicitly call produce, because the state argument already comes to you as a WritableDraft;
  • you don’t need to explicitly type the state argument, because it already comes to you properly typed.

To put everything together, with automatic type inference, we also need the combine middleware, so we don’t need to write the store type by hand. Here is the full template that can be used as the starting point of your store:

import {create} from 'zustand';
import {combine} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';

interface Person {
	name: string;
	age: number;
}

const useStore = create(immer(
	combine({
		people: [] as Person[],
	},
	(set, get) => ({
		add(name: string, age: number) {
			set(state => {
				state.people.push({name, age});
			});
		},
		remove(name: string) {
			set(state => {
				state.people = state.people.filter(p => p.name === name);
			});
		},
	})),
));

export default useStore;

Notice how you can simply mutate the state inside the actions.

Saturday, June 3, 2023

TypeScript map for objects

In another round of React experiments, after another Vue frustration with Volar not typing event arguments, I was again into the state normalization land. When dealing with objects with IDs as keys, every operation requires a tedious reduce, in contrast to the intuitive map we use with arrays.

So I wrote a map for objects:

function mapObj<T>(obj: Record<string, T>, callback: (elem: T, key: string) => T): Record<string, T> {
	return Object.keys(obj).reduce((accum, key) => {
		accum[key] = callback(obj[key], key);
		return accum;
	}, {} as Record<string, T>);
}

It’s fully typed, and it was surprisingly easy to write. TypeScript is a great language.

Saturday, April 15, 2023

Testing GitHub pull requests before merging

My personal projects on GitHub oftentimes. It’s very flattering when other people get interested in your personal work, and actively use it. GitHub pull request is how other people suggest changes to our code.

Pull requests can be merged in GitHub with just one click, but it’s also possible – and recommended – trying the changes locally first. The usual way to do this is creating a local branch, then pulling the modified code onto it.

As an example, let’s take pull request #75 of WinSafe. We create another branch called foo and check the pull request onto it by using:

git fetch origin pull/75/head:foo
git checkout foo

Simple enough, and for some reason it took me years to try this for the first time today.

Thursday, February 9, 2023

Fetch calls with global Jotai object

Zustand is a great React state management library, but it has the drawback of not having a standard way to define getters, which must be defined as ordinary hooks.

Written by the same author, Jotai seems to be the answer to this situation. However, while Zustand provides a way to change the state outside React components, Jotai does not. Today I was able to devise a way to work around this, by simply applying the hooks concept, answering my own question:

const errorAtom = atom(''); // storage

const errorReadAtom = atom(get => { // read-only (computed)
	return 'Error: ' + get(errorAtom);
});

const writeErrorAtom = atom(null, (get, set, arg: string): void => { // setter
	set(errorAtom, arg);
});

function useGet(): (url: string) => Promise {
	const [, setError] = useAtom(errorAtom);
	return async (url: string): Promise => {
		const resp = await fetch(url);
		const json = await resp.json();
		setError('Hello');
		return json as T;
	};
}

Usage:

function App() {
	const doGet = useGet();

	async function click(): void {
		const data = await doGet('/foo');
	}

	return <button onClick={click}>Click</button>;
}

So, with Jotai I finally have the complete solution to state management in React.

Tuesday, December 6, 2022

The three rules of lifetime elision in Rust

While reviewing some Rust fundamentals, I stumbled across this excellent video about lifetimes. What caught my attention the most was the “three rules” of lifetime elision – a topic I had some idea about, but I’ve never seen clearly explained.

For reference, they are:

  • Each parameter that is a reference gets its own lifetime parameter;
  • If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters;
  • If there are multiple input lifetime parameters, but one of them is &self or &mut self, the lifetime of self is assigned to all output lifetime parameters.

Wednesday, October 5, 2022

Zustand computed values

This week I’ve faced some situations with Vue’s reactive that alarmed me. I’m finding the hard way what I’ve read a couple times: reactive proxies can behave unpredictably in some situations. Too bad I’m in the middle of a project which began in Vue 3. I’m strongly considering rewriting it in React now – yes, it will be an insane amount of work, including back-end changes to return normalized objects.

But React brings its own problems. The biggest of all is certainly the raw state management.

Among all state management tools I’m evaluating, Zustand is showing to be the most promising. It’s ticking all the boxes, and the only open question so far is computed state. The best I could do was to use custom hooks, but they look rather ugly and verbose:

import create from 'zustand';
import {combine} from 'zustand/middleware';

/**
 * The store, which holds the state and the actions.
 */
const useBearStore = create(
	combine({
		bears: 0,
	},
	(set, get) => ({
		increasePopulation(): void {
			set(state => ({ bears: state.bears + 1 }));
		},
		removeAllBears(): void {
			set({ bears: 0 });
		},
	})),
);

export default useBearStore;

/**
 * Custom hook that returns a computed value.
 */
export function useBearCountPlusOne(): number {
	const bears = useBearStore(s => s.bears);
	return bears + 1;
}

Usage example in a component:

import useBearStore, {useBearCountPlusOne} from './useBearStore';

function App() {
	const increasePopulation = useBearStore(s => s.increasePopulation);
	const populationPlusOne = useBearCountPlusOne();

	return <>
		<div>Population + 1: {populationPlusOne}</div>
		<button onClick={() => increasePopulation()}>
			Increase population
		</button>
	</>;
}

If I find a way to rework these loose hooks, I think I found my way.

Wednesday, September 28, 2022

Multiple className values in React

React offers basically zero support for CSS. Personally I’m fond of CSS Modules along with SCSS instead of the slow CSS-in-JS solutions out there. Still, we need a way to manage multiple classes in className property.

In order to mitigate this problem, I wrote a TypeScript function to deal with sequential or conditional class names situations:

/**
 * Generates the className attribute with the given class names.
 */
export function cn(
	...items: (
		string
		| null
		| undefined
		| (string | null | undefined)[]
		| Record<string, boolean>
	)[]
): string {
	let fin = ' ';
	for (const item of items) {
		if (typeof item === 'string') {
			fin += item + ' ';
		} else if (Array.isArray(item)) {
			for (const s of item) {
				if (typeof s === 'string') fin += s + ' ';
			}
		} else if (typeof item === 'object') {
			for (const key in item) {
				if (item[key]) fin += key + ' ';
			}
		}
	}
	return fin.substring(0, fin.length - 1);
}

Example using some argument possibilities:

import {cn} from '@/funcs';
import c from './App.module.css';
	
function App() {
	return <>
		<div className={cn( c.first )} />
		<div className={cn( c.first, c.second )} />
		<div className={cn( [c.first, c.second], c.third )} />
		<div className={cn( c.first, {[c.second]: true} )} />
	</>;
}

This covers all situations I ever faced, and it's basically the same syntax of classnames.

The spread operator version was added in February 9, 2023.

The multi-combination of parameters was added in December 30, 2024.

Thursday, July 21, 2022

Sizes of Windows integral types

While developing WinSafe, it’s very common to convert the Windows integral data types to their Rust equivalent. Care must be taken, however, when it comes to pointer size, which varies according to the architecture. Since WinSafe is aimed to both 32 and 64-bit Windows, I must pay attention.

For reference, below is the table I’m using to figure out the sizes:

Signed C Signed Rust Unsigned C Unsigned Rust 32-bit 64-bit
CHAR
 
 
i8 UCHAR
BYTE
BOOLEAN
u8 8 bit (1 byte)
SHORT
 
 
i16 USHORT
WCHAR
WORD
u16 16 bit (2 byte)
BOOL
INT
LONG
 
i32  
UINT
ULONG
DWORD
u32 32 bit (4 byte)
INT_PTR
LONG_PTR
LPARAM
 
isize UINT_PTR
ULONG_PTR
WPARAM
SIZE_T
usize 32 bit (4 byte) 64 bit (8 byte)
LARGE_INTEGER
LONG64
LONGLONG
 
 
i64 ULARGE_INTEGER
ULONG64
ULONGLONG
DWORD64
DWORDLONG
QWORD
u64 64 bit (8 byte)

The table above is an extension of this one.

Tuesday, July 19, 2022

Default props in React function components

Having default props in a React component is a rather common situation. The most popular way to accomplish this is to pass the default values to a defaultProps property on the function component. However, this property will be deprecated in the future.

Spoiler: due to the sheer amount of code written with it, it never will be deprecated. It’s more likely that a warning will show in the console.

Anyway, in order to keep things clean and guard from this future warning, I came up with a clean pure TypeScript solution to this problem:

interface Props {
	name: string;
	surname?: string;
	age?: number;
}

const defaultProps = {
	surname: 'Doe',
};

function MyComponent(props0: Props) {
	const props = {...defaultProps, ...props0};

	return <div>{props.surname}</div>;
}

The code above provides the correct behavior and proper TypeScript validation. It ended becoming an answer on StackOverflow.

Since the spread order may be a a bit hard to remember, this function does the trick:

export function defProp<P, D>(props: P, defaultProps: D): P & D {
	return {...defaultProps, ...props};
}

function MyComponent(props0: Props) {
	const props = defProp(props0, defaultProps);

	// ...
}

Thursday, July 7, 2022

Extending built-in TypeScript objects in Vite

Checking whether an array is empty is a rather common operation. The usual way to do this in TypeScript is by checking the length property, but it’s very verbose:

if (myArray.length === 0) {
	// ...
}

For some reason, the JavaScript standard doesn’t specify an isEmpty() method. Fortunately, we can extend native JavaScript objects to add any methods we want. While this is a great feature, I was unable to make it work in a Vue + TypeScript + Vite project. I want the autocomplete of my VSCode to properly display it.

To make the TypeScript compiler recognize it, you must declare your method extensions as global by wrapping the declaration in a declare global block.

At first I tried to write the declaration in the “src/env.d.ts” file, but it didn’t work. Turns out you need to place them in a different file, as explained here. So I finally wrote them in a “src/extensions.d.ts” for the typings, and then the implementations themselves must be in a file that will be actually called in your application:

/**
* Extensões de objetos nativos
* As implementações estão em src/util/func/extends.ts
*/
export {};
declare global {
interface Array<T> {
/** Checks if indexOf() !== -1. */
contains(elem: T): boolean;
/** Checks if findIndex() !== -1. */
containsIf(callback: (elem: T, index: number) => boolean): boolean;
/** Returns a Map with the elements grouped by the given field. */
groupIntoMap<K extends keyof T>(fieldName: K): Map<T[K], T[]>;
/** Returns true if the array contains no elements. */
isEmpty(): boolean;
/** Returns true if the array contains at least 1 element. */
isNotEmpty(): boolean;
/** Returns the last element in the array, if any. */
last(): T | undefined;
/** Removes the elements to which indexOf() !== -1. */
remove(elem: T): void;
/** Removes the elements to which the callback returns true. */
removeIf(callback: (elem: T, index: number) => boolean): void;
/** Replaces the elements to which the callback returns true. */
replaceIf(callback: (elemt: T, index: number) => boolean, newVal: T): void;
/** Returns a new array with no repeated elements. */
unique(): T[];
}
interface Array<T extends Record<K, PropertyKey>> {
/** Returns an object where the key is the specified field, with all items grouped under it. */
groupIntoObject<K extends keyof {[P in keyof T as T[P] extends PropertyKey ? P : never]: any;}>(fieldName: K): Record<T[K], T[]>; // eslint-disable-line @typescript-eslint/no-explicit-any
}
interface Date {
/** Adds (or subtracts, if negative) the given number of days to the date. */
addDays(numDays: number): void;
/** Retorna: Seg 01/01/2023 17:00. */
fmtDataHoraCurta(): string;
/** Returns the number of days in the month to which the date belongs. */
getDaysInTheMonth(): number;
/** Returns the first day of next (or previous, if negative) month. */
getNextMonth(numMonths: number) : Date;
/** Returns the first day of next (or previous, if negative) year. */
getNextYear(numYears: number) : Date;
/** Retorna o nome da semana em português. */
getNomeDiaDaSemana(): string;
/** Retorna o nome da semana em português, abreviado para 3 letras. */
getNomeDiaDaSemanaAbrev(): string;
/** Retorna o nome do dia da semana em português. */
getNomeMes(): string;
}
interface HTMLCollectionOf<T> {
/** Calls the function on each element. */
forEach(callback: (elem: T, index: number) => void): void;
/** Returns true if the collection contains no elements. */
isEmpty(): boolean;
/** Returns true if the collection contains at least 1 element. */
isNotEmpty(): boolean;
}
interface Map<K, V> {
/** Returns true if the map contains no elements. */
isEmpty(): boolean;
/** Returns true if the map contains at least 1 element. */
isNotEmpty(): boolean;
/** Returns the number of keys. */
keyCount(): number;
/** Returns a new array populated with the results of calling a function with each key/value. */
mapIntoArray<U>(callback: (key: K, value: V, index: number) => U): U[];
/** Returns a new Map by mapping the keys. */
mapKeys<U>(callback: (key: K, value: V) => U): Map<U, V>;
/** Returns a new Map by mapping the values under each key. */
mapValues<U>(callback: (key: K, value: V) => U): Map<K, U>;
/** Returns an accumulated value created with the results of calling a function with each key/value. */
reduce<U>(callback: (accumulator: U, key: K, value: V, index: number) => U, initialValue: U): U;
/** Sorts the keys of the Map, in-place; returns a reference to the same Map. */
sortKeys(compareFn: (a: K, b: K) => number): Map<K, V>;
}
interface Number {
/** Formata o número com padrão brasileiro: ponto separador de milhar e vírgula. */
fmtBr(casasDecimais = 0): string;
/** Converts the number to string adding zeros at left, so the number has, at least, the given length. */
lpad(minLength: number): string;
/** Correctly rounds the number to N decimal places, returning a float; zeros are kept. */
round(decimalPlaces: number): number;
}
interface ObjectConstructor {
/** Returns true if the object has no keys with values. */
isEmpty(o: object): boolean;
/** Returns true if the object contains at least 1 key with a value. */
isNotEmpty(o: object): boolean;
/** Returns a new array populated with the results of calling a function with each key/value. */
mapIntoArray<K extends PropertyKey, V, U>(o: Record<K, V>, callback: (key: K, value: V, index: number) => U): U[];
/** Returns a new object with every value mapped under the same key. */
mapValues<K extends PropertyKey, V, U>(o: Record<K, V>, callback: (key: K, value: V, index: number) => U): Record<K, U>;
/** Returns an accumulated value created with the results of calling a function with each key/value. */
reduce<K extends PropertyKey, V, U>(o: Record<K, V>, callback: (accumulator: U, key: K, value: V, index: number) => U, initialValue: U): U;
/** Removes the elements to which the callback returns true. */
removeIf<K extends PropertyKey, V>(o: Record<K, V>, callback: (key: K, value: V, index: number) => boolean): void;
}
interface String {
/** Returns a new string with the first character in uppercase. */
capitalize(): string;
/** Checks if the string contains the substring. */
contains(substr: string): boolean;
/** Returns true if the string containts no characters, or just spaces. */
isBlank(): boolean;
/** Returns true if the string contains no characters. */
isEmpty(): boolean;
/** Returns true if the strings corresponds to an integer, either positive or negative. */
isInteger(): boolean;
/** Returns true if the string contains at least 1 non-space character. */
isNotBlank(): boolean;
/** Returns true if the string containst at least 1 character. */
isNotEmpty(): boolean;
/** Returns true if the string corresponds to a positive integer. */
isPositiveInteger(): boolean;
/** Returns the last character in the string, if any. */
last(): string | undefined;
}
interface URLSearchParams {
/** Returns a new array by iterating each key and value. */
map<U>(callback: (key: string, value: string, index: number) => U): U[];
}
}
view raw extends.d.ts hosted with ❤ by GitHub
/**
* Extensões de objetos nativos
* As declarações TypeScript estão em src/extends.d.ts
*/
export {};
Object.defineProperty(Array.prototype, 'contains', {
enumerable: false,
writable: false,
configurable: false,
value: function contains<T>(elem: T): boolean {
return this.indexOf(elem) !== -1;
},
});
Object.defineProperty(Array.prototype, 'containsIf', {
enumerable: false,
writable: false,
configurable: false,
value: function containsIf<T>(callback: (elem: T, index: number) => boolean): boolean {
return this.findIndex(callback) !== -1;
},
});
Object.defineProperty(Array.prototype, 'groupIntoMap', {
enumerable: false,
writable: false,
configurable: false,
value: function groupIntoMap<T, K extends keyof T>(fieldName: K): Map<T[K], T[]> {
const groups = new Map<T[K], T[]>();
for (const elem of this) {
if (!groups.has(elem[fieldName])) {
groups.set(elem[fieldName], [elem]);
} else {
groups.get(elem[fieldName])!.push(elem);
}
}
return groups;
},
});
Object.defineProperty(Array.prototype, 'groupIntoObject', {
enumerable: false,
writable: false,
configurable: false,
value: function groupIntoObject< // https://stackoverflow.com/a/71068491
T extends Record<K, PropertyKey>,
K extends keyof {[P in keyof T as T[P] extends PropertyKey ? P : never]: any;}, // eslint-disable-line @typescript-eslint/no-explicit-any
>(fieldName: K): Record<T[K], T[]> {
return this.reduce((accum: Record<T[K], T[]>, item: T) => {
if (accum[item[fieldName]] === undefined) {
accum[item[fieldName]] = [item];
} else {
accum[item[fieldName]].push(item);
}
return accum;
}, {} as Record<T[K], T[]>);
},
});
Object.defineProperty(Array.prototype, 'isEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isEmpty(): boolean {
return this.length === 0;
},
});
Object.defineProperty(Array.prototype, 'isNotEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isNotEmpty(): boolean {
return this.length > 0;
},
});
Object.defineProperty(Array.prototype, 'last', {
enumerable: false,
writable: false,
configurable: false,
value: function last<T>(): T | undefined {
return this.length === 0 ? undefined : this[this.length - 1];
},
});
Object.defineProperty(Array.prototype, 'remove', {
enumerable: false,
writable: false,
configurable: false,
value: function remove<T>(elem: T): void {
for (;;) {
const idx = this.indexOf(elem);
if (idx === -1) break;
this.splice(idx, 1);
}
},
});
Object.defineProperty(Array.prototype, 'removeIf', {
enumerable: false,
writable: false,
configurable: false,
value: function removeIf<T>(callback: (elem: T, index: number) => boolean): void {
let idx = 0;
while (idx < this.length) {
if (callback(this[idx], idx)) {
this.splice(idx, 1);
} else {
++idx;
}
}
},
});
Object.defineProperty(Array.prototype, 'replaceIf', {
enumerable: false,
writable: false,
configurable: false,
value: function replaceIf<T>(
callback: (elem: T, index: number) => boolean,
newVal: T,
): void {
for (let i = 0; i < this.length; ++i) {
if (callback(this[i], i)) {
this[i] = newVal;
}
}
},
});
Object.defineProperty(Array.prototype, 'unique', {
enumerable: false,
writable: false,
configurable: false,
value: function unique<T>(): T[] {
const uniques = [] as T[];
for (const elem of this) {
if (!uniques.contains(elem))
uniques.push(elem);
}
return uniques;
},
});
//------------------------------------------------------------------------------
Object.defineProperty(Date.prototype, 'addDays', {
enumerable: false,
writable: false,
configurable: false,
value: function addDays(numDays: number): void {
this.setUTCDate(this.getUTCDate() + numDays);
},
});
Object.defineProperty(Date.prototype, 'fmtDataHoraCurta', {
enumerable: false,
writable: false,
configurable: false,
value: function fmtDataHoraCurta(): string {
return this.getNomeDiaDaSemanaAbrev().capitalize()
+ ' ' + this.getDate().toString().padStart(2, '0')
+ '/' + (this.getMonth() + 1).toString().padStart(2, '0')
+ '/' + this.getFullYear()
+ ' ' + this.getHours().toString().padStart(2, '0')
+ ':' + this.getMinutes().toString().padStart(2, '0');
},
});
Object.defineProperty(Date.prototype, 'getDaysInTheMonth', {
enumerable: false,
writable: false,
configurable: false,
value: function getDaysInTheMonth(): number {
return new Date(this.getFullYear(), this.getMonth() + 1, 0).getDate();
},
});
Object.defineProperty(Date.prototype, 'getNextMonth', {
enumerable: false,
writable: false,
configurable: false,
value: function getNextMonth(numMonths: number): Date {
return new Date(this.getFullYear(), this.getMonth() + numMonths, 1);
},
});
Object.defineProperty(Date.prototype, 'getNextYear', {
enumerable: false,
writable: false,
configurable: false,
value: function getNextMonth(numYears: number): Date {
return new Date(this.getFullYear() + numYears, this.getMonth(), 1);
},
});
Object.defineProperty(Date.prototype, 'getNomeDiaDaSemana', {
enumerable: false,
writable: false,
configurable: false,
value: function getNomeDiaDaSemana(): string {
return ['domingo', 'segunda', 'terça', 'quarta', 'quinta', 'sexta', 'sábado'][this.getDay()];
},
});
Object.defineProperty(Date.prototype, 'getNomeDiaDaSemanaAbrev', {
enumerable: false,
writable: false,
configurable: false,
value: function getNomeDiaDaSemanaAbrev(): string {
return ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 'sáb'][this.getDay()];
},
});
Object.defineProperty(Date.prototype, 'getNomeMes', {
enumerable: false,
writable: false,
configurable: false,
value: function getNomeMes(): string {
return ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho',
'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'][this.getMonth()];
},
});
//------------------------------------------------------------------------------
Object.defineProperty(HTMLCollection.prototype, 'forEach', {
enumerable: false,
writable: false,
configurable: false,
value: function forEach<T>(callback: (elem: T, index: number) => void): void {
for (let i = 0; i < this.length; ++i)
callback(this[i], i);
},
});
Object.defineProperty(HTMLCollection.prototype, 'isEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isEmpty(): boolean {
return this.length === 0;
},
});
Object.defineProperty(HTMLCollection.prototype, 'isNotEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isNotEmpty(): boolean {
return this.length > 0;
},
});
//------------------------------------------------------------------------------
Object.defineProperty(Map.prototype, 'isEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isEmpty(): boolean {
return this.size === 0;
},
});
Object.defineProperty(Map.prototype, 'isNotEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isNotEmpty(): boolean {
return this.size > 0;
},
});
Object.defineProperty(Map.prototype, 'keyCount', {
enumerable: false,
writable: false,
configurable: false,
value: function keyCount(): number {
return Array.from(this.keys()).length;
},
});
Object.defineProperty(Map.prototype, 'mapIntoArray', {
enumerable: false,
writable: false,
configurable: false,
value: function mapIntoArray<K, V, U>(callback: (key: K, value: V, index: number) => U): U[] {
const result = [] as U[];
let count = 0;
for (const [key, val] of this) {
result.push(callback(key, val, count++));
}
return result;
},
});
Object.defineProperty(Map.prototype, 'mapKeys', {
enumerable: false,
writable: false,
configurable: false,
value: function mapKeys<K, V, U>(callback: (key: K, value: V) => U): Map<U, V> {
const result = new Map<U, V>();
for (const [key, _val] of this) {
const val = this.get(key);
result.set(callback(key, val), val);
}
return result;
},
});
Object.defineProperty(Map.prototype, 'mapValues', {
enumerable: false,
writable: false,
configurable: false,
value: function mapValues<K, V, U>(callback: (key: K, value: V) => U): Map<K, U> {
const result = new Map<K, U>();
for (const [key, val] of this) {
result.set(key, callback(key, val));
}
return result;
},
});
Object.defineProperty(Map.prototype, 'reduce', {
enumerable: false,
writable: false,
configurable: false,
value: function reduce<K, V, U>(
callback: (accumulator: U, key: K, value: V, index: number) => U,
initialValue: U,
): U {
let accum = initialValue;
let count = 0;
for (const [key, val] of this) {
accum = callback(accum, key, val, count++);
}
return accum;
},
});
Object.defineProperty(Map.prototype, 'sortKeys', {
enumerable: false,
writable: false,
configurable: false,
value: function sortKeys<K, V>(compareFn: (a: K, b: K) => number): Map<K, V> {
const keys = Array.from(this.keys()) as K[];
const tmpMap = new Map<K, V>();
for (const sortedKey of keys.sort(compareFn)) {
tmpMap.set(sortedKey, this.get(sortedKey));
}
this.clear();
for (const [key, val] of tmpMap.entries()) {
this.set(key, val);
}
return this;
},
});
//------------------------------------------------------------------------------
Object.defineProperty(Number.prototype, 'fmtBr', {
enumerable: false,
writable: false,
configurable: false,
value: function fmtBr(casasDecimais = 0): string {
return this.toLocaleString('pt-BR', {
minimumFractionDigits: casasDecimais,
maximumFractionDigits: casasDecimais,
});
},
});
Object.defineProperty(Number.prototype, 'lpad', {
enumerable: false,
writable: false,
configurable: false,
value: function lpad(minLength: number): string {
return this.toString().padStart(minLength, '0');
},
});
Object.defineProperty(Number.prototype, 'round', {
enumerable: false,
writable: false,
configurable: false,
value: function round(decimalPlaces: number): number {
return Math.round((this + Number.EPSILON) * Math.pow(10, decimalPlaces))
/ Math.pow(10, decimalPlaces);
},
});
//------------------------------------------------------------------------------
Object.isEmpty = function isEmpty(o: object): boolean { // https://stackoverflow.com/a/28020863/6923555
return Object.keys(o).length === 0;
};
Object.isNotEmpty = function isNotEmpty(o: object): boolean {
return Object.keys(o).length > 0;
};
Object.mapIntoArray = function mapIntoArray<K extends PropertyKey, V, U>(
o: Record<K, V>,
callback: (key: K, value: V, index: number) => U,
): U[] {
const arr = [];
let count = 0;
for (const key in o) {
arr.push(callback(key, o[key], count++));
}
return arr;
};
Object.mapValues = function mapValues<K extends PropertyKey, V, U>(
o: Record<K, V>,
callback: (key: K, value: V, index: number) => U,
): Record<K, U> {
const accum = {} as Record<K, U>;
let count = 0;
for (const key in o) {
accum[key] = callback(key, o[key], count++);
}
return accum;
};
Object.reduce = function reduce<K extends PropertyKey, V, U>(
o: Record<K, V>,
callback: (accumulator: U, key: K, value: V, index: number) => U,
initialValue: U,
): U {
let accum = initialValue;
let count = 0;
for (const key in o) {
accum = callback(accum, key, o[key], count++);
}
return accum;
};
Object.removeIf = function<K extends PropertyKey, V>(
o: Record<K, V>,
callback: (key: K, value: V, index: number) => boolean,
): void {
let count = 0;
for (const key in o) {
if (callback(key, o[key], count++))
delete o[key];
}
};
//------------------------------------------------------------------------------
Object.defineProperty(String.prototype, 'capitalize', {
enumerable: false,
writable: false,
configurable: false,
value: function capitalize(): string {
return this.length === 0 ? ''
: this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
},
});
Object.defineProperty(String.prototype, 'contains', {
enumerable: false,
writable: false,
configurable: false,
value: function contains(substr: string): boolean {
return this.indexOf(substr) !== -1;
},
});
Object.defineProperty(String.prototype, 'isBlank', {
enumerable: false,
writable: false,
configurable: false,
value: function isBlank(): boolean {
return this.trim().length === 0;
},
});
Object.defineProperty(String.prototype, 'isEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isEmpty(): boolean {
return this.length === 0;
},
});
Object.defineProperty(String.prototype, 'isInteger', {
enumerable: false,
writable: false,
configurable: false,
value: function isInteger(): boolean {
if (this.length === 0) return false;
for (let i = 0; i < this.length; ++i) {
const code = this.charCodeAt(i);
const isNum = code >= 48 && code <= 57; // 0 to 9
if (i === 0) {
if (this[0] !== '-' && !isNum) return false; // first char can be a minus sign
} else {
if (!isNum) return false;
}
}
return true;
},
});
Object.defineProperty(String.prototype, 'isNotBlank', {
enumerable: false,
writable: false,
configurable: false,
value: function isNotBlank(): boolean {
return this.trim().length > 0;
},
});
Object.defineProperty(String.prototype, 'isNotEmpty', {
enumerable: false,
writable: false,
configurable: false,
value: function isNotEmpty(): boolean {
return this.length > 0;
},
});
Object.defineProperty(String.prototype, 'isPositiveInteger', {
enumerable: false,
writable: false,
configurable: false,
value: function isPositiveInteger(): boolean {
if (this.length === 0) return false;
for (let i = 0; i < this.length; ++i) {
const code = this.charCodeAt(i);
const isNum = code >= 48 && code <= 57; // 0 to 9
if (!isNum) return false;
}
return true;
},
});
Object.defineProperty(String.prototype, 'last', {
enumerable: false,
writable: false,
configurable: false,
value: function last(): string | undefined {
return this.length === 0 ? undefined : this[this.length - 1];
},
});
//------------------------------------------------------------------------------
Object.defineProperty(URLSearchParams.prototype, 'map', {
enumerable: false,
writable: false,
configurable: false,
value: function map<U>(callback: (key: string, value: string, index: number) => U): U[] {
const entries = Array.from(this.entries()) as [string, string][];
const result = [] as U[];
let count = 0;
for (const [key, val] of entries) {
result.push(callback(key, val, count++));
}
return result;
},
});
view raw implement.ts hosted with ❤ by GitHub

I tested the code above in a Vue project, but I believe it will work accordingly in a React project as well.

Wednesday, June 22, 2022

Checking if user passed a slot in Vue

In Vue 3 with the Composition API, there is no this.$slot entry to programmatically poke on the slots. You must summon the slots by calling useSlots() – yes, that’s a React hook right there.

The returned object has one entry for each slot. If your component has only one unnamed slot, it will be named 'default'. So, in order to check whether the user didn’t pass the slot, you simply check whether the entry exists:

<script setup lang="ts">
import {computed, useSlots} from 'vue';

const slots = useSlots();
const hasSlot = computed(() => slots['default'] !== undefined);
</script>

Tuesday, June 21, 2022

Global useState hooks with Jotai

I’ve been stressing out several React global state libraries in the past months. Last week it was Jotai’s time.

I liked the concept of “atoms” and how they feel like autonomous useState parts, and how they can be share state among components. I remember trying to write something like this in the past. Jotai seems to be what I tried to do back then:

import {atom} from 'jotai';

export const nameAtom = atom('hello');

Pretty much a global useState here, which is great:

import {useAtom} from 'jotai';
import {nameAtom} from './state';
	
function App() {
	const [name, setName] = useAtom(nameAtom);

	//...
}

In large, real-world applications you’d like to write mutation methods to implement specific logic, rather than having them scattered over the components. Thus we should not make setName public; instead we should provide more specific methods.

In Jotai, while reading the state is trivial, I found writing mutations to be rather cumbersome. The syntax of “writing atoms” is, to my tired eyes, very convoluted:

export const setSurnameAtom = atom(null, (get, set, surname) => {
	set(nameAtom, get(nameAtom) + ' ' + surname);
});

//...

function App() {
	const [, setSurname] = useAtom(setSurnameAtom);
}

After giving it some thought, it occurred me that since useAtom is a hook, I can compose a custom hook over it. And then it all clicked:

export function useName() {
	const [name, setName] = useAtom(nameAtom);
	return useMemo(() => ({
		value: name,
		setSurname(surname: string) {
			setName(name + ' ' + surname);
		},
	}), [name, setName]);
}

Usage of this custom hook is straightforward, crystal clear:

import {useName} from './state';
	
function App() {
	const name = useName();

	return <>
		<h1>{name.value}</h1>
		<button onClick={() => name.setSurname('foo')}>
			Set surname
		</button>
	</>;
}

This is a truly global custom useState hook. This works amazingly well with VSCode autocomplete. This is easy to read. This is beautiful.

In the custom hook above, note the use of useMemo. It cuts down a lot of the processing in inside the custom hook, and it was an insight I had after briefly talking to Daishi Kato himself, the author of Jotai, about this custom hook idea. The future useEvent hook will optimize the button call a little further.

Jotai, unfortunately, has a huge drawback of not allowing accessing atoms outside a React component.