Implementation Techniques for Complex Widgets using useReducer in Flutter Hooks
In Flutter, there is a Hook called useReducer in Flutter Hooks, which is a method for efficiently managing the state of Widgets and executing logic. However, its utilization has not been widely seen so far. This is probably because there is not enough understanding or knowledge about the behavior and utilization of useReducer. By mastering its usage, developers can manage a wider range of states, and it is also expected to make the code more efficient. Therefore, in this article, I would like to explain how to use useReducer with actual code, and propose more efficient implementation techniques.
What is useReducer?
useReducer is one of the hooks provided by the library called Flutter Hooks. This useReducer specializes in managing a series of actions performed on a specific state.
Generally, state management requires state variables and methods to update that state. However, as the application grows and the state becomes more complex, the methods to update it also tend to become complex. Here, `useReducer` comes into its own. `useReducer` manages the state using one method (the reducer function) for different actions, which makes the code more organized and readable.
From these characteristics, the following cases can be cited as specific use cases:
1. When you need to update multiple related values together: `useReducer` is useful when you need to update multiple related values together. Instead of using `useState` for each value, you can use `useReducer` to bundle all updates of related values at once.
2. When you want to manage complex state logic: By using `useReducer`, you can encapsulate complex state change logic within the reducer function. This makes the code easier to read and understand.
3. When you want to reuse the same state update logic in many components: Since the reducer function should be pure, it can be reused across multiple components without depending on the outside. This avoids code duplication.
4. When you want ease of testing: By implementing the reducer as a pure function that generates a consistent new state for a given state and action, unit testing becomes easier.
Example not using useReducer
Here, I will provide an example of managing complex states and logic with useState.
Specifically, it has the following features:
1. Select Dropdown 1 (the string is reset upon reselection)
2. String input (with string inspection function)
3. Select Dropdown 2
4. Display of results
5. Reset
Each Widget refers to multiple states and executes logic. If the number of states and logic increases further, you might want to consider introducing a ViewModel, or managing states with libraries like Riverpod.
Example of useReducer
We will rewrite the implementation with useState to useReducer.
First, let’s show the code for the Widget.
The state is consolidated in `store.state`, and executing logic has been reduced to just running `store.dispatch`.
Let’s take a look at the implementation of the `state`, `action`, and `reducer` functions.
When there are multiple states, instead of managing each one individually with useState, you can manage them collectively in the form of MyFormState. User actions are expressed using multiple classes that inherit from a sealed class called MyFormAction. As a result, the reducer function takes the current state and action and generates the next state.
In Flutter, when widgets become complex, it is common to introduce ViewModel or manage states with libraries like Riverpod. However, in projects already using Flutter Hooks, the use of useReducer is often beneficial. This reduces the need for lifetime management when using things like useTextEditingController.
However, caution is needed when using useReducer. In particular, handling the store can be a challenge. Complex widgets generally want to be divided into multiple descendant widgets, but in doing so, the store will be passed down to descendant widgets like a bucket relay. Moreover, every time the store is updated, all descendant widgets may be rebuilt. Therefore, simply using useReducer does not necessarily make it easy to create an efficient widget tree.
I will introduce techniques to solve these problems and achieve more efficient state management.
Reference to store by InheritedModel and InheritedWidget
To suppress rebuilding without passing the store via bucket relay, use InheritedModel and InheritedWidget to make store.state and store.dispatch referable from descendant widgets.
By creating such components, you will be able to directly reference state and dispatch.
The code incorporating these is shown below.
By creating a Store instance and passing it to the StoreInjector, it becomes possible to directly reference the state and dispatch within the _Form Widget, which is a descendant of all. Furthermore, by using the StateModel.selectOf method, you can control so that the corresponding Widget is rebuilt only when a specific part of the state is changed.
By combining Flutter’s InheritedModel or InheritedWidget with useReducer, it is possible to solve common problems such as the bucket relay issue and excessive rebuilds. These design patterns improve the performance and efficiency of the application.
Conclusion
Flutter Hooks’ useReducer is a powerful tool for effectively managing the state of Widgets. It allows you to consolidate complex state change logic in one place and call it with a simple interface. Its consistency and predictability also greatly enhance the readability of code and contribute to the efficiency of testing.
However, caution is needed when using useReducer, and the specific needs of the application must be considered when using it. In particular, careful planning is required regarding the data flow and the frequency of rebuilds of descendant Widgets.
For more advanced state management, it can be helpful to combine useReducer with Flutter’s InheritedModel or InheritedWidget. By utilizing these techniques, it is possible to develop more efficient, high-performance, and maintainable Flutter applications.