Tuesday, May 27, 2025

Pinia sucks

Today I completely wiped away Pinia from a large Vue codebase at work. It was an immense effort, but I’m so pissed I didn’t mind working overtime.

The main reason to use Pinia is that you can inspect the state in DevTools – an idea taken from React –, but guess what. The extension only exists for Chrome now, and the current old Firefox version is broken. It looks like a project driven by amateurs, just like the horrible VSCode extension. And I’m not alone on this.

For the global stores, I’m simple using a reactive object, with functions and computed values inside – that’s what Pinia does anyway. I found this by digging into its source code. I also found that a computed inside a reactive makes the .value unnecessary, which is a merit of Vue’s reactivity system, which I still believe to be the best out there.

const state = reactive({
	loading: false,
	name: '',

	foo: computed(() => state.name + '.'),

	doAction(): {
		console.info('Hello');
	},
});

I really regret choosing Vue back in the day for this project. I should’ve choosen good ol’ React, because despite its flaws, it’s dependable.

Thursday, April 3, 2025

Lean Pinia stores

Three years ago – time files – I wrote a hack to remove the boilerplate fields from a Pinia store. It worked well.

This morning, writing work stuff, I wrote a lightweight wrapper to Pinia’s defineStore, with the primary intent to remove the manual string ID which needs to be given. After the initial TypeScript brouhaha, it went so well that I was able to fully integrate the boilerplate strip with Omit, which is a solution even less intrusive than the one I wroten 3 years ago.

import {_ActionsTree, _GettersTree, defineStore, DefineStoreOptions, Pinia, StateTree, Store, StoreGeneric} from 'pinia';

let nStore = 0;

type LeanStoreDef<Id extends string = string, S extends StateTree = StateTree, G = _GettersTree<S>, A = _ActionsTree> = {
	(pinia?: Pinia | null | undefined, hot?: StoreGeneric): Omit<Store<Id, S, G, A>,
		'$id' | '$dispose' | '$onAction' | '$patch' | '$reset' | '$state' | '$subscribe' | '_customProperties'>;
	$id: Id;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export function newStore<Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A = {}>(
	options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>,
): LeanStoreDef<Id, S, G, A> {
	return defineStore(('siorg-store-' + ++nStore) as Id, options);
}

Usage is just exactly what I had in mind:

const useNames = newStore({
	state: () => ({
		names: [] as string[],
	}),
	getters: {
		count: state => state.names.length,
	},
	actions: {
		add(name: string): void {
			this.names.push(name);
		},
	},
});

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.

Monday, October 7, 2024

Goodbye my Universe

Today I finally parted ways with my long-time friend, my Ibanez Universe UV7BK green dots.

As far as I remember, I bought this guitar in 2008 amidst a craze of seven string guitars. I remember seeing an UV777BK on the TV and going nuts over that, even posting online at the old FCC forums. And by that time I joined a Dream Theater cover band, and I played solely this guitar. To this day, I still don’t know how I was able to play all that hard stuff on a seven string. Incredible young me.

The neck profile was truly great. I loved it. Pronounced shoulders, somewhat similar to the Suhr Modern Satin I had.

I remember trying to sell it 10 years ago or so, but I retreated.

After playing in a band where a seven string was demanded, and using a six string with a pitch shifter instead, I finally realized I don’t play sevens anymore. It must go. The original Ibanez wooden hardcase was wasted with mold, so I had to buy another one. Pretty bad fit, but anyway. The UV had grown mold in the neck too, but I cleaned. Maybe it will show up again in the future, who knows.

It was really hard to find someone to buy it. Professional sellers refused saying it’s hard to sell seven strings. In the end, I found someone to buy, and it posted it.

Goodbye, old friend.

Friday, July 26, 2024

Automating MSVC builds

While rewritting a few of my personal tools from Rust back to C++, I found myself willing to automate the build process. At first, I tried to invoke MSVC’s cl compiler directly, but it has proven to be absolute hell to do.

Eventually I stumbled upon the marvellous MSBuild tool, which is capable of understanding the .sln file and take all compiler and linker options from it:

msbuild foo.sln /p:Configuration=Release /p:Platform=x64

With that, I finally had the script to automate one build:

call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
set APP="vscode-font-patch"
msbuild %APP%.sln /p:Configuration=Release /p:Platform=x64
move /Y x64_Release\%APP%.exe "D:\Stuff\apps\_audio tools\"
rmdir /S /Q x64_Release
pause

Then it was time to automate the release build of all my listed projects. And I found out how awkward .bat syntax is. Loop syntax is particularly horrifying. In the end, I was able to cook a script to automate my C++ builds:

call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"

set APPS[0]="vscode-font-patch"

set "i=0"
:SymLoop
if not defined APPS[%i%] goto :EndLoop
call set APP=%%APPS[%i%]%%
cd %APP%
echo Building %APP%...
msbuild %APP%.sln /p:Configuration=Release /p:Platform=x64
move /Y x64_Release\%APP%.exe "D:\Stuff\apps\_audio tools\"
rmdir /S /Q x64_Release
cd ..

set /a "i+=1"
goto :SymLoop
:EndLoop
pause