Open Bloomca opened 5 months ago
I might be reinventing the wheel here.
First, it is not really necessary to include that in the State
type at all. Consider this:
import { createState, type State } from "veles";
function selectState<F, T>(
state: State<F>,
selector: (state: F) => T
): State<T> {
const initialValue = selector(state.getValue());
const newState = createState(initialValue);
state.trackValueSelector(
selector,
(selectedState) => {
newState.setValue(selectedState);
},
{ skipFirstCall: true }
);
return newState;
}
This implementation is more or less what I wanted, and essentially is a map
operation on observables. If you think of State
as a stream of values, we can subscribe to it, modify it as we want, and then continue to use it. Also we can insert custom comparator at every opportunity.
After thinking some more on the nature of createState
and potential composability, I feel like it is better to refactor the approach and to extract useAttribute
, useValue
and so on into their own functions.
So:
<div>
{useValue(nameState)}
</div>
The idea is that this way we can accept other observables. I am only somewhat familiar with Rxjs, I will need to review what's available, what is their API and if I can offer selector functionality (imo selectors are pretty important to avoid unnecessary re-renders, and cascading selectors should be really good for that).
For now, the API will stay the same, but since I basically re-created observables, I don't see the point to expand basic tools for them. Maybe I can implement from
for rxjs
to transform, and that would be enough (Solidjs does that).
This is just an idea, but I think there is value in allowing to create substates.
Basically, we'll be able to do
const newState = state.select(selector)
. What is the point? Well, we can effectively denormalize data without touching the "main" store.For example, we can have:
Meanwhile a denormalized state can contain task arrays which are grouped by due date, project ID, section ID, etc. This can have several benefits compared to typical selector functions from reselect.
First, we can use
useValueSelector
and if we write a custom comparator, we can easily solve a problem of invalidating previous results. E.g. if we have 12 projects, it is pretty tricky to write a selector which will correctly memoize tasks for each project, we have to compromise to either call it on the first render (create a custom selector per component and memoize it), or just hope that the re-rendering component will have the same project ID, and hence the selector won't be invalidated.This problem won't exist here, because we can ensure that a subset of a group will be the same by reference (if no tasks have changed there, of course).
Second, we'll still have access to
combineStates
, which by default doesn't do anything without subscriptions, so with smart comparators we can potentially avoid pretty much any updates where the data didn't change.Last, by creating a tree of subscriptions, we can calculate pretty much everything in a single iteration. This is a pretty niche use-case, because technically both a single traversal and multiple ones are O(1), but in case we have thousands of items, and we are not too careful, there definitely can be a difference between traversing over 10000 items once, and traversing between 10000 items 30 times (imagine calculating number of tasks per each project in the sidebar separately).
The only downside is that we can calculate some things prematurely, especially if we utilize comparators, so maybe some lazy mechanics can be introduced.