What’s New in StateAdapt 2.0.0: adapt rework

what’s-new-in-stateadapt-20.0:-adapt-rework

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
    Error example
  • 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 induces 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


mfp22 avatar


mfp22
posted on

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:

  1. The new sources syntax taking a function for recursive sources. See #44
  2. The optional path, enabled by 1. For now it will show up in DevTools as 0, 1, etc. Automatically chosen path. And maybe it can get smarter over time. But if necessary, can specify path.
  3. Only 1 overload creates much better TS warnings.
  4. Simpler for newcomers.
  5. Leaves room for more options. 2 come to mind already: sinks and resetOnRefCount0 with the option to keep state in cache for a certain time after all unsubscribes, similar to the same option in share()
  6. It enables even more incremental syntax than before. It’s a smaller gap from useState(0) or signal(0) to adapt(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 library 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

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
i-built-a-snake-game-in-react

I built a Snake Game in React

Next Post
python-secure-password-management:-hashing-and-encryption-

Python Secure Password Management: Hashing and Encryption #️⃣🔐✨

Related Posts