What's New in StateAdapt 2.0.0
Pierre Bouillon
Posted on November 5, 2023
Three weeks ago, StateAdapt released a new major version.
The main changes here is a rework of the adapt
API that might have been confusing to some, especially newcomers.
Today we will have a brief overview of those breaking changes.
If you are more interested in watching than reading, take a look at Mike Pearson's video on the new version.
Creating an adapter in 1.x
Initially, there was 4 overloads of adapt
, each offering various possibilities:
adapt(path, initialState)
adapt([path, initialState], adapter)
adapt([path, initialState], sources
adapt([path, initialState, adapter], sources)
While the array syntax is consise and helps to reduce lines of code, having four overloads comes with a little bit of trouble:
- If you are doing something wrong, TypeScript might not be of a great help because of that, outputing confusing error messages
- Creating a new adapter when joining in a project and not having prior experience with StateAdapt could also be a bit frustrating until you get used to the syntax
Let's see how we called an adapter previously in the first version of StateAdapt:
- Single value
const name = adapt('name', 'John Doe');
- With an adapter
const name = adapt(['name', 'John Doe'], {
uppercase: name => name.toUpperCase(),
selector: {
firstLetter: name => name.at(0),
},
});
- From a source
const name = adapt(
['name', 'John Doe'],
http.get('/name').pipe(toSource('[Name] Get from HTTP')),
);
- With an adapter and a source
const nameAdapter = createAdapter<string>()({
uppercase: name => name.toUpperCase(),
selector: {
firstLetter: name => name.at(0),
},
});
const name = adapt(['name', 'John Doe', nameAdapter], {
set: http.get('/name').pipe(toSource('[Name] Get from HTTP')),
});
What v2 is about
Well aware of the issues induced by the plurality of the adapt
API, Mike Pearson opened a issue about this, with the goal of discussing how to unify the four overloads into a single one:
Explore removing overloads for StateAdapt.adapt #45
The API for StateAdapt.adapt
has been mostly the same for 2 years. But I've received feedback from a few people that the overloads are confusing. I've seen that the TypeScript errors can be very confusing as well. A couple of people have also said they want path
to be optional, and the current syntax would make that very difficult.
So, I believe StateAdapt.adapt should move to only 1 overload, with 3 possibilities for 2nd argument: undefined, adapter or options. Here is each existing overload and the new syntax:
1. adapt(path, initialState)
// old
const count1 = adapt('count1', 4);
// new
const count1 = adapt(4);
2. adapt([path, initialState], adapter)
// old
const count2_2 = adapt(['count2_2', 4], {
increment: count => count + 1,
selectors: {
isEven: count => count % 2 === 0,
},
});
// new
const count2_2 = adapt(4, {
increment: count => count + 1,
selectors: {
isEven: count => count % 2 === 0,
},
});
3. adapt([path, initialState], sources)
// old
const count3 = adapt(
['count3', 4],
http.get('/count/').pipe(toSource('http data')),
);
// new
const count3 = adapt(4, {
sources: http.get('/count/').pipe(toSource('http data')),
});
4. adapt([path, initialState, adapter], sources)
// old
const adapter4 = createAdapter<number>()({
increment: count => count + 1,
selectors: {
isEven: count => count % 2 === 0,
},
});
const count4 = adapt(['count4', 4, adapter4], watched => {
return {
set: watched.state$.pipe(delay(1000), toSource('tick$')),
};
});
// new
const count4 = adapt(4, {
path: 'count4',
adapter: {
increment: count => count + 1,
selectors: {
isEven: count => count % 2 === 0,
},
},
sources: watched => {
return {
set: watched.state$.pipe(delay(1000), toSource('tick$')),
};
},
});
Implementation
I had to use a trick to get inference to work. Here's the type implementation:
adapt<State, S extends Selectors<State>, R extends ReactionsWithSelectors<State, S>>(
initialState: State,
second?: (R & { selectors?: S, adapter?: never, sources?: never, path?: never }) | {
path?: string;
adapter?: R & { selectors?: S };
sources?: SourceArg<State, S, R>;
}
): SmartStore<State, S & WithGetState<State>> & SyntheticSources<R> {
Discussion
What I like about this change:
- The new
sources
syntax taking a function for recursive sources. See #44 - The optional
path
, enabled by 1. For now it will show up in DevTools as0
,1
, etc. Automatically chosen path. And maybe it can get smarter over time. But if necessary, can specify path. - Only 1 overload creates much better TS warnings.
- Simpler for newcomers.
- Leaves room for more options. 2 come to mind already:
sinks
andresetOnRefCount0
with the option to keep state in cache for a certain time after all unsubscribes, similar to the same option inshare()
- It enables even more incremental syntax than before. It's a smaller gap from
useState(0)
orsignal(0)
toadapt(0, { increment: n => n + 1 })
.
What I hate about it: It's a breaking change. I will try to find a way to make migrating easier. I myself would benefit from migration tools because I have so many projects I will want to update.
Plans
Before this, I will add the new source function syntax inspired by #44, add the new injectable function from that discussion, and release that as 1.2.0. Then I'll fix #38 and release that in 1.2.1.
Since this issue is a breaking change in a main feature, I will make this a major version bump to StateAdapt 2.0.
All of this can be done before adding signal features for Angular, which will require Angular 16+, so I will release that in version 2.1.
As a result, adapt
now only requires an initial value and a second, optional, configuration object takes care of specifying any additional behaviour.
This also means that this version is a breaking change, hence the bump in the major digit
Usage and migration
Let's see how this migration will impact the current code:
- Single value
// v1
const name = adapt('name', 'John Doe');
// v2
const name = adapt('John Doe', {
path: 'name',
});
Optionally, since the configuration object is optional, defining a path is no longer required:
const name = adapt('John Doe');
- With an adapter
// v1
const name = adapt(['name', 'John Doe'], {
uppercase: name => name.toUpperCase(),
selector: {
firstLetter: name => name.at(0),
},
});
// v2
const name = adapt('John Doe', {
path: 'name',
uppercase: name => name.toUpperCase(),
selector: {
firstLetter: name => name.at(0),
},
});
- From a source
// v1
const name = adapt(
['name', 'John Doe'],
http.get('/name').pipe(toSource('[Name] Get from HTTP')),
);
// v2
const name = adapt('John Doe', {
path: 'name',
sources: http.get('/name').pipe(toSource('[Name] Get from HTTP')),
});
- With an adapter and a source
// v1
const nameAdapter = createAdapter<string>()({
uppercase: name => name.toUpperCase(),
selector: {
firstLetter: name => name.at(0),
},
});
const name = adapt(['name', 'John Doe', nameAdapter], {
set: http.get('/name').pipe(toSource('[Name] Get from HTTP')),
});
// v2
const name = adapt('John Doe', {
path: 'name',
// 👇 If needed, the adapter can be created locally
adapter: {
uppercase: name => name.toUpperCase(),
selector: {
firstLetter: name => name.at(0),
},
},
sources: http.get('/name').pipe(toSource('[Name] Get from HTTP')),
});
We can still define the adapter elsewhere and use it only when calling
adapt
:const nameAdapter = createAdapter<string>()({ uppercase: name => name.toUpperCase(), selector: { firstLetter: name => name.at(0), }, }); const name = adapt('John Doe', { path: 'name', // 👇 Using an existing adapter adapter: nameAdapter, sources: http.get('/name').pipe(toSource('[Name] Get from HTTP')), });
Personal thoughts
I discovered StateAdapt a couple of months ago through a video of Joshua Morony and it gained my interest.
However, after diving into it, I was quickly lost between the overloads of the library, and it was making the shift from other state management libraries such as NgRx harder.
With this update, I find the unified synthax way more accessible, and easier to get started with.
Mike Pearson is doing an awesome work with this and I hope StateAdapt will continue to grow!
If you would like to give it a try, he has a Youtube channel full of resources, and a great overview of the new version:
If you wish to migrate, he also recorded a step by step migration example using all the overloads.
I hope you learned something useful there!
Cover image from StateAdapt's website
Posted on November 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.