Reality is, all of these tools are pretty good and have their place. Which one you should use depends on both your use case and preference.
When building a new app you need to ask yourself some questions.
- How much global state do you plan to have?
- How often is the global state going to change?
- How much application logic/non-fetching side effects do you plan to have?
- How complex is this application going to be, do you want to have separation of view/data/applogic layers?
- How many people are going to work on this and for how long?
- How does the current ecosystem look like?
- Do you hate boilerplate or do you like bit of it of it to introduce structure and consistency into your project?
- How important is Typescript and proper typing to you?
If you plan to have only little or no global state or it is not going to change much:
I'd suggest to stick to React context & react-query for fetching. It will likely cover all your needs without any issues and there will be very little boilerplate.
If you plan to have lots of global state/its going to change often.
Use redux, when it comes to global state management it is the most mature solution with strong ecosystem and lots of tools and examples available. It is also still actively maintained and developed. See redux-toolkit for the modern way to work with redux. Redux-toolkit also includes redux-thunk and reselect by default. Thunks will cover your needs for the occasional extra side effect in addition to fetching.
If most of your side effects are fetching data
Use redux-query, you get the perfect combo of getting rid of most of boilerplate & fast early development while retaining the ability to manage well your global state and cache computed data.
If you plan to have lots of application logic, managing different asynchronous processes that run for longer time etc.
Use redux-saga, its very stable library that works great with redux for global state management. You can use it well even together with redux-query if you want to have the fetching part covered. One issue with sagas is that Typescript currently doesn't have great support for generator async runners. So you will need to explicitly type results of effects.
Mixing async middleware
It is possible to mix thunks & sagas together in one project, and it works well. Some people advocate to mix them based on your current use case others to stick to just one for consistency. Personally, If I end up adding sagas to a project I prefer to stick to them for everything just so I don't have to keep rewriting code between the two of them.
Future
The redux team is also working on a new experimental action-listener-middleware api, that will allow you to listen to actions similar to what sagas can do. I haven't tried it yet, but it is probably worth keeping your eyes on.
Do you plan to work on this project just yourself?
If so, pick whichever tool you find most fun. Keeping yourself motivated & learning new things is often the most important thing. You can have the best architecture there is, but if you are going to drop the project three days in, it won't help you. If on the other hand this is a company project with lots of people working on it and it will need to be maintained for a long time, try to pick the best tools for the job and to introduce some structure & consistency. Bit of boilerplate in large projects is usually a good thing as it helps in navigating the project and keeping it consistent. Having well maintained libraries with strong ecosystem behind them is a benefit too as it will make it easier to figure out how to bend them in the direction you will need.