rodydavis / signals.dart

Reactive programming made simple for Dart and Flutter
http://dartsignals.dev
Apache License 2.0
378 stars 44 forks source link

[feat] `.asyncMap` a Future to a Signal #271

Closed dickermoshe closed 2 weeks ago

dickermoshe commented 2 weeks ago

(I'm new to the whole idea of signals, forgive me) Imagine a scenario where we make a network call and then do a db query using that data. With signals is there a better way than this?

final idSignal = futureSignal(() async => 1); // First async operation
final idPlusOneSignal = computedFrom([idSignal], (i) {
  return i.first.map(
      data: (id) {
        return Future.value(AsyncData(id + 1)); // Second async operation
      },
      error: () async => i.first,
      loading: () async => i.first);
});
final Computed<int?>result = computed(() {
  final result = idPlusOneSignal.value.map(
    data: (d) => d.map(
      data: (d) => d,
      error: () => null,
      loading: () => null,
    ),
    error: () => null,
    loading: () => null,
  );
});

The biggest issue here is (besides how ugly it is) is that there will be 2 clicks of the clock before any data is returned from the result signal

SPiercer commented 2 weeks ago

It depends on your logic really, for me I do that in one futureSignal with no hassle.

and I prefer using futureSignals with GET requests and any submitted POST requests or basically a request that needs some parameters you can do it in a normal future function

dickermoshe commented 2 weeks ago

It depends on your logic really, for me I do that in one futureSignal with no hassle.

Yeah, the use case is for when more then one signal depends on the 1st one.

SPiercer commented 2 weeks ago

I'm not following really, however

i have a case where i have a signal containing a userModel (currentUser)

and a stream signal called isLoggedIn

streamSignal((){
   return Stream.value(currentUser.value != null);
},
  dependencies: [currentUser]
);

so is that what you're referring to?

or what?

dickermoshe commented 2 weeks ago

so is that what you're referring to?

Not at all.

Here is a better explanation. If we are comparing signals to streams, then there is no way to do a Stream's mapAsync or Future's then on signal.

Instead you get AsyncValue<AsyncValue<AsyncValue<...>>> as you add more signals

SPiercer commented 2 weeks ago

so basically what you're doing is creating an asyncSignal that returns another asyncSignal ?

dickermoshe commented 2 weeks ago

Yup If the issue was only nested .map calls I wouldn't complain. There real issue is that when the 1st stream emits a new value, you get this:

Loading // 1st future running
Loading // 2nd future running
Data // Finally

There is now way to .asyncMap a Future to a Signal

dickermoshe commented 2 weeks ago

Found a way

import 'package:signals/signals.dart';

extension StreamSignalAsyncMapExt<T> on StreamSignal<T> {
  Computed<AsyncState<R>> mapAsync<R>(Future<R> Function(T) map) {
    final f = futureSignal(() async => map(await future), dependencies: [this]);
    return computed(() {
      return f.value.maybeMap(
        loading: () => f.previousValue ?? const AsyncLoading(),
        orElse: () => f.value,
      );
    });
  }
}

void main() async {
  final stream = Stream<int>.periodic(const Duration(seconds: 1), (i) => i);
  final signal = streamSignal(() => stream);
  final signalTimesTen = signal.mapAsync((p0) async => p0 * 10);
  effect(() => print("Original: ${signal.value.value}"));
  effect(() => print("TimesTen: ${signalTimesTen.value.value}"));
  await Future.delayed(const Duration(seconds: 1000));
}

Result:

Original: null
TimesTen: null
Original: 0
TimesTen: 0
Original: 1
TimesTen: 10
Original: 2
TimesTen: 20
Original: 3
TimesTen: 30

@rodydavis , would you be interested in a PR that adds such functionality to the StreamSignal

dickermoshe commented 2 weeks ago

I'm fairly new to this, so it's very likely that there is an underlying issue here. Let me know if there is something evil going on under the hood