monio icon indicating copy to clipboard operation
monio copied to clipboard

Feature: `useState(..)`

Open getify opened this issue 3 years ago • 2 comments

Taking inspiration from Hooks (i.e., React), a state preserving mechanism like useState(..) is proposed.

You could use it in do-routines like this:

IO.do(component).run({});

function *component(viewContext) {
	var [ id, updateID ] = yield useState(() => Math.floor(Math.random() * 1000));
	
	console.log(`random ID: ${id}`);
	
	id = yield updateID(42);
	
	console.log(`fixed ID: ${id}`);
}

There are some unresolved issues with such a mechanism:

  • The nextSlotIdx needs some way to be reset between invocations of the component(..)
  • State is by default shared among all invocations in the same view-context... perhaps need a more explicit mechanism for forcing a sub-view-context for encapsulating the state
  • Since this uses numeric slot indexing (as React/etc does), there's no clear way to share/access state by some unique name. State sharing is an important requirement, so need to figure out to design that into this mechanism. Perhaps an optional ID that overrides using numeric slot indexing.

Here's a candidate first implementation:

function useState(initialVal) {
	return IO((viewContext = {}) => {
		var { state = {}, } = viewContext;
		viewContext.state = state;
		var { nextSlotIdx = 0, slots = [], } = viewContext.state;
		state.nextSlotIdx = nextSlotIdx;
		state.slots = slots;
		var curSlotIdx = state.nextSlotIdx++;

		if (!(curSlotIdx in slots)) {
			if (typeof initialVal == "function") {
				initialVal = initialVal();
			}
			slots[curSlotIdx] = initialVal;
		}
		return [ slots[curSlotIdx], function update(nextVal){
			return IO(({ state: { slots, }, }) => {
				if (typeof nextVal == "function") {
					nextVal = nextVal(slots[curSlotIdx]);
				}
				return (slots[curSlotIdx] = nextVal);
			});
		}];
	});
}

getify avatar Apr 09 '22 17:04 getify

Here's an alternate approach that I think addresses some of the above limitations:

doWithState(component).run({});

function *component({ useState, }) {
	var [ id, updateID ] = yield useState(() => Math.floor(Math.random() * 1000));
	
	console.log(`random ID: ${id}`);
	
	id = yield updateID(42);
	
	console.log(`fixed ID: ${id}`);
}

And the implementation of doWithState(..):

function doWithState(gen,...args) {
	var state = { nextSlotIdx: 0, slots: [], };
	
	return IO(env => {
		state.nextSlotIdx = 0;
		var context = { ...env, useState, };
		return IO.do(gen,...args).run(context);
	});

	// ********************************************

	function useState(initialVal) {
		return IO(env => {
			var slots = state.slots;
			var curSlotIdx = state.nextSlotIdx++;

			if (!(curSlotIdx in slots)) {
				if (typeof initialVal == "function") {
					initialVal = initialVal();
				}
				slots[curSlotIdx] = initialVal;
			}
			return [
				slots[curSlotIdx],
				
				function update(nextVal){
					return IO(() => {
						if (typeof nextVal == "function") {
							nextVal = nextVal(slots[curSlotIdx]);
						}
						return (slots[curSlotIdx] = nextVal);
					});
				},
			];
		});
	}
}

getify avatar Apr 11 '22 02:04 getify

A further refinement to the doWithState(..) approach, that seems to solve all the remaining issues as listed earlier... provides a useState(..) for accessing private state via numerically indexed slots, and useSharedState for accessing shared state via named slots, both of which are still fully self-contained in the provided IO reader-env object:

doWithState(component).run({});

function *component({ useState, useSharedState, }) {
	var [ id, updateID ] = yield useState(() => Math.floor(Math.random() * 1000));
	var [ greeting, updateGreeting ] = yield useSharedState("greeting","Hello World!");
	
	console.log(`random ID: ${id}`);
	
	id = yield updateID(42);
	
	console.log(`fixed ID: ${id}`);

	console.log(greeting);
}

And this implementation:

const privateStateKeys = new WeakMap();
const SHARED_STATE = Symbol("shared-state");

function doWithState(gen,...args) {
	return withState(IO.do(gen,...args),gen);
}

function doXWithState(gen,deps,...args) {
	return withState(IOx.do(gen,deps,args),gen);
}

function withState(io,gen) {
	if (!privateStateKeys.has(gen)) {
		privateStateKeys.set(gen,Symbol(`private-state:${gen.name || gen.toString()}`));
	}
	const PRIVATE_STATE = privateStateKeys.get(gen);
	
	return IO(env => {
		var {
			[PRIVATE_STATE]: privateState = { nextSlotIdx: 0, slots: [], },
			[SHARED_STATE]: sharedState = {},
		} = env;
		env[PRIVATE_STATE] = privateState;
		env[SHARED_STATE] = sharedState;
		
		privateState.nextSlotIdx = 0;
		return io.run({
			...env,
			useState,
			useSharedState,
		});
	});

	// ********************************************

	function useState(initialVal) {
		return IO(({ [PRIVATE_STATE]: privateState, }) => {
			return accessState(
				privateState.slots,
				privateState.nextSlotIdx++,
				initialVal,
				env => env[PRIVATE_STATE].slots
			);
		});
	}
}

function useSharedState(stateName,initialVal) {
	return IO(({ [SHARED_STATE]: sharedState, }) => {
		return accessState(
			sharedState,
			stateName,
			initialVal,
			env => env[SHARED_STATE]
		);
	});
}

function accessState(stateStore,prop,initialVal,getStateStore) {
	if (!(prop in stateStore)) {
		if (typeof initialVal == "function") {
			initialVal = initialVal();
		}
		stateStore[prop] = initialVal;
	}
	return [
		stateStore[prop],
		
		function update(nextVal){
			return IO(env => {
				var stateStore = getStateStore(env);
				if (typeof nextVal == "function") {
					nextVal = nextVal(stateStore[prop]);
				}
				return (stateStore[prop] = nextVal);
			});
		}
	];
}

getify avatar Apr 11 '22 15:04 getify