Tuesday, December 31, 2024

Zustand, devtools and immer wrapper, pt. 3

In another epiphany of the morning of this December 31, I enhanced my previous utility function to create Zustand stores, including devtools and immer middlewares:

import {create, StateCreator, StoreApi, UseBoundStore} from 'zustand';
import {devtools} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';

type ZustandActions<T extends object, U extends object> = StateCreator<
	T,
	[['zustand/devtools', never], ['zustand/immer', never]],
	[['zustand/immer', never], ['zustand/devtools', never]],
	U
>;

/**
 * Creates a Zustand store with state, computed and actions.
 */
export function createStore<
	T extends object,
	U extends object,
	C extends Record<string, (s: UseBoundStore<StoreApi<T>>) => void>,
> (
	{
		state,
		computed = {} as C,
		actions = {} as ZustandActions<T, U>,
	}: {
		state: T;
		computed?: C;
		actions?: ZustandActions<T, U>;
	},
) {
	const store = create(
		devtools(
			immer(() => state),
			{name: Math.random().toString(36).slice(2), serialize: true},
		),
	);

	if (import.meta.env.DEV) {
		for (const name in computed) {
			if (!name.startsWith('use'))
				throw new Error(name + ' is a hook, and its name must start with the word "use".');
		}
	}

	return {
		use: store,
		...computed,
		...actions(store.setState, store.getState, store),
	};
}

The key improvement here is that computed functions will be 100% reactive, because they’re simple hooks attached to the store object – they work like ordinary hook functions:

const bearStore = createStore({
	state: {
		bears: 0,
	},
	computed: {
		useHasBears: () => bearStore.use(s => s.bears > 0),
	},
	actions: (set, get) => ({
		increasePopulation(): void {
			set(state => {
				++state.bears;
			});
		},
	}),
});

function App() {
	const bears = bearStore.use(s => s.bears);
	const hasBears = bearStore.useHasBears();

	return <div>
		<span>{bears} - {hasBears}</span>
		<button onClick={() => bearStore.increasePopulation()}>
			Change
		</button>
	</div>;
}

I’m in awe looking at such a small function yielding such a great usability.

Multiple className values in React, pt. 2

When dealing with an important React project at work, I was manipulating multiple CSS Modules class names with a function I wrote 2 and a half years ago, accepting an object for conditionals.

The early hours of the morning of this last day of the year brought me an idea for a shorter, simpler syntax that works better for imported CSS Modules names:

/**
 * Generates the `className` attribute with the given class names.
 *
 * @example
 * cn( 'abc' );
 * cn( 'abc', 'def' );
 * cn( 'abc', ['def', true] );
 */
export function cn(...items: (string | null | undefined | [string, boolean])[]): string {
	let fin = '';
	for (let i = 0; i < items.length; ++i) {
		const item = items[i];
		if (item === null || item === undefined) {
			continue;
		} else if (typeof item === 'string') {
			fin += item + ' ';
		} else if (Array.isArray(item) && item.length === 2) {
			if (typeof item[0] === 'string') {
				if (item[1]) fin += item[0] + ' ';
			} else {
				throw new Error(`cn() item #${i} is an invalid tuple: ${JSON.stringify(item)}`);
			}
		} else {
			throw new Error(`cn() item #${i} is invalid: ${JSON.stringify(item)}`);
		}
	}
	return fin.length ? fin.substring(0, fin.length - 1) : '';
}

And this new function covers all my current needs.

Friday, December 6, 2024

Download YouTube videos

These days I was transferring one of by band’s channel, and I needed to download a 1h34min long video. No web downloader worked properly.

While searching for an actual software, I found this Reddit post referring to a Python command line tool named yt-dlp. Basic usage is pretty straightforward:

yt-dlp.exe URL

My first try downloaded two files: MP4 and WEBM. The MP4 had no sound, and the WEBM was unplayable. My recently downloaded Vegas 22 didn’t seem to recognize this files. A warning message appeared, though:

WARNING: You have requested merging of multiple formats but ffmpeg is not installed.
The formats won't be merged

The repository README had an specific section labelled FFmpeg Static Auto-Builds, with a download link to Windows x64 binaries. The zip contains, among others, a bin directory with three executables. I extracted them in the same folder of the original executable – yt-dlp.exe –, and this time the download completed smoothly, with a single file with video and audio properly merged.

Excellent tool.