riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Sync Providers managing the same entity

Open w0rsti opened this issue 4 months ago • 11 comments

Describe what scenario you think is uncovered by the existing examples/articles It is quite common to have a List<Item> provider as well as an Item provider, especially when dealing with web applications and (deep) links. It is not well documented on how to share state between provider that might contain the same Item. To be more precise on what I mean, lets take a look at an example:

Lets suppose we develop an Web-Application with the following paths:

https://my-awesome-todo.app/todos/
https://my-awesome-todo.app/todos/:id

and a simple model of the todo as the following:

@freezed
class Todo with _$Todo {
  const factory Todo({
    required String id,
    required String title,
    required String description,
    required bool isCompleted,
  }) = _Todo;

  factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}

As you can see, it is a pretty simple setup: /todos shows us an overview of the todos (e.g. only the title and the completion status) while /todos/:id/ will display an detailed version of that todo (e.g. title, status, description). Since this is an (web-) application, the user might navigate to the detailed screen without ever rendering the overview screen.

Lets start pretty simple - just have two separate providers that fetch the todos/todo:


Future<List<Todo>> fetchTodos() async {
  // Some async logic that fetches the todos
  // e.g. from a remote database.
}

Future<List<Todo>> fetchTodo(String id) async {
  // Some async logic that fetches the todo
  // e.g. from a remote database.
}

@riverpod
Future<List<Todo>> todos(TodosRef ref) {
  return fetchTodos();
}

@riverpod
Future<Todo> todo(TodoRef ref, String id) {
  return fetchTodo(id);
}

Okay - fair! But now the first problem raises: We might fetch the same entity twice - first when the user visits the overview and second when he visits the detailed view - pretty wasteful on ressources, no? You might argue now that this can be fixed fairly easy by awaiting the todos and returning the todo as a dependent provider.

@riverpod
Future<Todo> todo(TodoRef ref, String id) async {
  final  todos = await ref.watch(todosProvider.future);
  return todos.where((todo) => todo.id == id).first;
}

But now we still fetch the whole list just to get that one provider, so we can even optimize it further...

@riverpod
Future<Todo> todo(TodoRef ref, String id) async {
  // Check if the provider exists, so we don't initialize it
  if(ref.exists(todosProvider)) {
     // Just check the docs on ref.exists, it describes this.
     final todo = ref.watch(todosProvider).selectAsync(/* ... */);
     if(todo != null) return todo;
  }

  return fetchTodo(id);
}

Okay. Understandable to this point - TLDR; The detailed provider should check the list provider if it exists, and if yes, check its state so we don't fetch an todo twice - seems logical!

But now lets suppose we don't have a single todosProvider, this can be the due to multiple reasons:

  • Reason 1: The Todos Provider doesn't fetch all the todos, only those that are not completed (=> isCompleted: false). And we have another Todos Provider (uncompletedTodosProvider) that fetches the uncompleted todos. Now we need to modify our todo provider to check 2 possible lists... In this case is a little trivial but assume we have something else with a lot more properties and a more complex data model, we could end up with 3-4 places to check. While adding all of those providers to check first before fetching the actual item seem a little annoying, I am willing to accept it BUT:

  • Reason 2 (prob. more common): The Todos Provider is actually a FamilyProvider since we have several thousand of todos (overall we are pretty busy, no?) so that the fetching of all todos would take a long time and a lot of ressources. So we simply add a int page to the params of the todosProvider - Lets make a loop to check every familyProvider(page)... but wait - how far should we check? We don't know how many pages there might be...

Let's mentally reset and assume we found a solution OR just go with the simple overfetching - we fetch our todo it in the todosProvider aswell as in the todoProvider 🤷🏼‍♂️

But aren't Todos supposed to be updatable? I mean we somehow want to check of our todos, right? This means we have to make our todoProvider an class based (notifier) provider.. But updating the state of this provider doesn't update it within the overviewProvider and vice versa.. How can we keep them synchronized?

There just doesn't seem to be an sufficient example that is "complex" enough to showcase how to use riverpod with this common use-cases above.

I hope with this little example made my problem clear - if not, I am happy to discuss your solutions and will throw further constraints at you! :D

Describe why existing examples/articles do not cover this case The docs on the riverpod website cover just simple use-cases, either using read-only (family) provider or local/simple state notifier. I have been struggling with the problem mentioned for a long time now and can't find a general solution to tackle this problem. It seems like other people having problems with this problem (or a related one) too. E.g. #3285

w0rsti avatar Oct 16 '24 18:10 w0rsti