rrousselGit / flutter_hooks

React hooks for Flutter. Hooks are a new kind of object that manages a Widget life-cycles. They are used to increase code sharing between widgets and as a complete replacement for StatefulWidget.
MIT License
3.11k stars 176 forks source link

How to reuse other hooks in class hook? #215

Open jeiea opened 3 years ago

jeiea commented 3 years ago

Describe what scenario you think is uncovered by the existing examples/articles I want to use other hook in class hook.

Describe why existing examples/articles do not cover this case I thought I can use useState in HookState.build, but it throws the following.

Bad state: Type mismatch between hooks:
- previous hook: _Hook<T>
- new hook: _StateHook<S>

I couldn't find the way to achieve the above.

rrousselGit commented 3 years ago

Could you share a full example?

jeiea commented 3 years ago

@rrousselGit https://github.com/jeiea/flutter_example/tree/class-hook

jeiea commented 3 years ago

Any information is needed?

rrousselGit commented 3 years ago

Not really

I just need to find some time to work on it. It's a regression

Feel free to make a PR if it's urgent for you

harkairt commented 3 years ago

I imagine you are extremely busy, but I'd also like to request a fix for this. <3

ben-xD commented 1 year ago

For anyone facing this issue and want a workaround:

Instead of re-using other hooks (since that causes this exception), I just re-implemented the other hooks I needed. For example, in my useIsConnected hook to check for internet connectivity using DNS, I wanted to use useEffect. Because of this issue, I just implemented it using ValueNotifier()..addListener(), like useEffect does.

AAverin commented 1 year ago

I was hoping to make my custom class hook-aware by wrapping it into a hook, but stumbled into this issue too. Any other way to have a viewmodel-like class with useReducer inside it?

AAverin commented 1 year ago

Here is a quick test for this case. I have tried looking into the code here https://github.com/rrousselGit/flutter_hooks/commit/ecbffc75c6280e9ec1992f5b9360c9b89b018869#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed but honestly this is beyond something I can understand within an hour to make a PR.

import 'package:flutter/cupertino.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_test/flutter_test.dart';

_TestHookState useTestHook() {
  return use(_TestHook());
}

class _TestHook extends Hook<_TestHookState> {
  @override
  HookState<_TestHookState, Hook<_TestHookState>> createState() =>
      _TestHookState();
}

class _TestHookState extends HookState<_TestHookState, _TestHook> {
  @override
  _TestHookState build(BuildContext context) {
    useState(5);
    return this;
  }
}

void main() {
  testWidgets('hooks can be used inside other hooks', (tester) async {
    await tester.pumpWidget(
      HookBuilder(builder: (_) {
        useTestHook();
        return Container();
      }),
    );
  });
}
rrousselGit commented 1 year ago

The workaround is to use your hooks within the custom useMyHook:

_TestHookState useTestHook() {
  final state = useState(5);
  return use(_TestHook(state));
}
AAverin commented 1 year ago

@rrousselGit Thanks, I will see if this can be applied in practice. Test case is, of course, very simplified.

Here is the more complicated code I have that fails. The idea is to abstract away all the logic into ViewModel, exposing only ValueNotifier with state to the Widget. And I would like to use hooks inside my ViewModel, at least in constructor, to make some async calls replacing the typical way data is fetched for the screen.

I know there are other ways to do that, like Riverpod, but current project I work on has only hooks and redux.

abstract class ViewModel<S> {
  late ValueNotifier<S?> _stateNotifier;

  ValueNotifier<S?> get state {
    return _stateNotifier;
  }

  ViewModel(this._stateNotifier);

  void dispose() {}
}

abstract class ViewModelFactory<S, V> {
  V create(ValueNotifier<S?> stateNotifier);
}

ValueNotifier<S?> useViewModel<S, T extends ViewModel>(ViewModelFactory<S, T> factory) {
  return use(_UseViewModelHook<S, T>(factory));
}

class _UseViewModelHook<S, T extends ViewModel> extends Hook<ValueNotifier<S?>> {
  final ViewModelFactory<S, T> factory;

  _UseViewModelHook(this.factory);

  @override
  HookState<ValueNotifier<S?>, Hook<ValueNotifier<S?>>> createState() => _UseViewModelState<S, T>();
}

class _UseViewModelState<S, T extends ViewModel>
    extends HookState<ValueNotifier<S?>, _UseViewModelHook<S, T>> {
  T? _viewModel = null;
  final ValueNotifier<S?> valueNotifier = useValueNotifier<S?>(null);

  @override
  ValueNotifier<S?> build(BuildContext context) {
    _viewModel = hook.factory.create(valueNotifier);
    return valueNotifier;
  }

  @override
  void dispose() {
    _viewModel?.dispose();
  }
}

===== Usage example

class UserProfileViewModelFactory extends ViewModelFactory<UserProfileState, UserProfileViewModel> {
  final int? userId;

  UserProfileViewModelFactory({required this.userId});

  @override
  UserProfileViewModel create(ValueNotifier<UserProfileState?> stateNotifier) =>
      UserProfileViewModel(stateNotifier, userId: userId);
}

class UserProfileViewModel extends ViewModel<UserProfileState> {
  final int? userId;

  UserProfileViewModel(ValueNotifier<UserProfileState?> stateNotifier, {required this.userId})
      : super(stateNotifier) {
    state.value = UserProfileState(userId: userId);
    useEffect(() {
      Future.microtask(() async {
        await _asyncInit();
      });
    });
  }

  _asyncInit() async {
    state.value = state.value!.copyWith(test: "Test");
  }
}

// Somewhere in the HookWidget
final state = useViewModel<UserProfileState, UserProfileViewModel>(
        UserProfileViewModelFactory(userId: userId));
ksyro98 commented 5 months ago

hey!

i like the workaround with useState

is there anything similar with useEffect? or is using useEffect there generally discouraged?