How to Handle Async Actions for Global State With React Hooks and Context

With React Tracked


I have been developing React Tracked, which is a library for global state with React Hooks and Context.

This is a small library and focuses on only one thing. It optimizes re-renders using state usage tracking. More technically, it uses Proxies to detect the usage in render, and only triggers re-renders if necessary.

Because of that, the usage of React Tracked is very straightforward. It is just like the normal useContext. Here's an example.

For a concrete example, please check out "Getting Started" in the doc.

Now, because React Tracked is a wrapper around React Hooks and Context, it doesn't support async actions natively. This post shows some examples how to handle async actions. It's written for React Tracked, but it can be used without React Tracked.

The example we use is a simple data fetching from a server. The first pattern is without any libraries, and uses custom hooks. The rest is using three libraries, one of which is my own.

Custom hooks without libraries#

Let's look at a native solution. We define a store at first.

This is one of the patterns to create a store (container) in React Tracked. Please check out the recipes for other patterns.

Next, we create a custom hook.

This is a new hook based on useTracked and it returns state and actions. You can invoke action.fetch(1) to start fetching.

Note: Consider wrapping with useCallback if you need a stable async function.

React Tracked actually accepts a custom hook, so this custom hook can be embedded in the container.

Try the working example.


react-hooks-thunk-reducer provides a custom hook useThunkReducer. This hook returns dispatch which accepts a thunk function.

The same example can be implemented like this.

Invoking an async action would be like this.

It should be familiar to redux-thunk users.

Try the working example.


use-saga-reducer provides a custom hook useSagaReducer. Because this library uses External API, you can use redux-saga without Redux.

Let's implement the same example again with Sagas.

Invoking it is simple.

Notice the similarity and the difference. If you are not familiar with generator functions, it may seem weird.

Anyway, try the working example.

(Unfortunately, this sandbox doesn't work online as of writing. Please "Export to ZIP" and run locally.)


use-reducer-async provides a custom hook useReducerAsync. This is the library I developed, inspired by useSagaReducer. It's not capable of what generator functions can do, but it works with any async functions.

The following is the same example with this hook.

You can invoke it in the same way.

The pattern is similar to useSagaReducer, but the syntax is similar to useThunkReducer or the native solution.

Try the working example.


Although it can be biased, here's what I suggest. If you would like a solution without libraries, use the native one. If you are saga users, use useSagaReducer with no doubt. If you like redux-thunk, useThunkReducer would be good. Otherwise, consider useReducerAsync or the native solution.

For TypeScript users, my recommendations are useSagaReducer and useReducerAsync. The native solution should also work. Please check out the fully typed examples in React Tracked.

Closing notes#

To be honest, I think the native solution works fine for small apps. So, I wasn't so motivated to create a library. However, during writing a tutorial for React Tracked, I noticed that having a pattern restricted by a library is easier to explain. use-reducer-async is a tiny library and it's nothing fancy. But, it shows a pattern.

The other note about async actions is Suspense for Data Fetching. It's currently in the experimental channel. The new recommended way of data fetching is Render-as-You-Fetch pattern. That's totally different from the patterns described in this post. We will see how it goes. Most likely, that new pattern requires a library that would ease developers to follow the pattern. If you are interested, please check out my experimental project.