Daxus v0.3.0 is released!
Tags: react, webdev, javascript, typescript
Published: 2023-06-18 15:15:05

## TL;DR

Daxus is a new server state management library for React. It allow developer to customize the data like using Redux, while also provide an easy-to-use api like React Query's `useQuery`. [Try it out!](https://github.com/jason89521/daxus)

---

Hi everyone, it feels like it's been a while since we last talked, even though it's only been a little over a week. Since my last article, I've been fully immersed in developing Daxus. Today, I'm excited to announce the release of Daxus version 0.3.0, along with the basic API documentation. I want to express my gratitude for all the encouragement I've received from you, whether it's the interactions on social media or the stars on the Daxus repository on GitHub. Your support has been a great motivation for me, thank you! (Especially the stars, they really get me excited.)

Now, let's dive into the specific features that Daxus provides.

## Fully Customizable Data

This is where Daxus differs the most from React Query. Daxus allows developers to define their desired data structures, similar to using Redux. Developers can tailor the data structures to their specific needs. However, this feature also comes with some trade-offs. Since the data is managed by the developers, Daxus is not aware of how to update the cache after fetching data. Therefore, when using Daxus, you need to inform it about how you want to update the cache. If fully customizable data is not your primary concern, using React Query to manage the server state may be more suitable.

### Adapter

When using Daxus, I recommend developers create an adapter for the models. This adapter provides the initial state values and various operations for the state, such as read and write. Creating an adapter can be cumbersome, so Daxus currently includes support for a pagination adapter, inspired by our company's approach. This adapter should suffice for most pagination scenarios.

Since my initial goal was to address pagination, Daxus currently only supports the pagination adapter. However, in the future, additional adapters will be added to meet other requirements. I also welcome any ideas you may have.

## Deduplication

In Redux, deduplication can also be achieved. In my company, we write code like this:

```typescript
export const listAchievementBadge = createAsyncThunk<
  { items: BasicAchievementBadge[] },
  ListAchievementBadgePayload
>(
  `${ACHIEVEMENT_BADGE}/list`,
  (ctx, { lang }) => {
    const res = await listAchievementBadgeApi({lang});
    return {items: res.items}
  },
  {
    modifiers: [throttle(10000, { keySelector: (_, { lang }) => lang })],
  }
);

```

`throttle(10000)` means that the same action won't be dispatched within 10 seconds, and `keySelector` is used to determine whether the actions are the same. Usually, most APIs require deduplication, so adding `throttle` to each new action can be quite cumbersome. Moreover, it's important not to forget to define the `keySelector`, otherwise, every dispatch of the action will be considered unique. I once forgot to add `keySelector` and ended up with excessive API calls after the product went live.

Daxus automatically handles deduplication, so you don't have to worry about repeated requests being continuously dispatched.

## Revalidate On Focus/Reconnect

Just like React Query's `refetchOnWindowFocus` and `refetchOnReconnect`, Daxus also provides these features to enhance the user experience.

> My company hasn't implemented this feature with Redux, but I think it would be quite troublesome.

## Polling

If you want to fetch data from the backend at regular intervals, Daxus also offers polling functionality. You just need to tell Daxus how often to fetch data, and it will handle it automatically for you.

## Error retry

When encountering an error while fetching an API, you can simply throw an error, and Daxus will automatically retry until the defined number of attempts is reached before throwing the error. Of course, you can also disable this feature so that a single API failure is considered a failure and shown to the user.

## Invalidate data

Currently, Daxus only supports invalidating a single piece of data and doesn't have the ability to invalidate multiple data entries at once, like React Query. For example, if you have query keys like the following:

```javascript
['posts', 'list', 'all']
['posts', 'get', 1]
```

Using `queryClient.invalidateQueries({queryKey: ['post']})` will mark all the mentioned data as stale. This is a great feature, and Daxus is expected to support similar functionality in the future.

## Written in TypeScript

Using TypeScript has become a standard practice for packages nowadays. Writing code with TypeScript provides a more pleasant development experience.

## Getting Started

After discussing all of that, I believe many of you may still be unsure about how to use Daxus (since there was no related code mentioned above). Now, let's dive into how to actually utilize Daxus!

### Create Model

Unlike Redux, where data is centralized in the store, Daxus requires developers to categorize data and create different models for different data types. For example, in our company, we have data types like posts, comments, and forums, so we need to create separate models for them. The reason behind this is that different data types may require different data structures. For example, posts may be suited for a pagination data structure, but user settings may not be.

```typescript
import { createPaginationAdapter, createModel } from 'react-server-model';

const postAdapter = createPaginationAdapter<Post>();
const postModel = createModel(postAdapter.initialState);
```

### Accessor with `useAccessor` hook

Once the model is created, you can start defining accessors. Accessors play a crucial role in Daxus. They fetch data from the server and synchronize it with your model. After your model is updated, it notifies the components that use the corresponding model to check if a rerender is needed.

```typescript
const getPostById = postModel.defineAccessor<number, Post>('normal', {
    fetchData: async (id) => {
        const data = await getPostApi(id);
        return data;
    },
    syncState: (draft, payload) => {
        postAdapter.upsertOne(draft, payload.data);
    }
})
```

The first argument of the `defineAccessor` method accepts only `'normal'` or `'infinite'`. Usually, you only need to use `'infinite'` when implementing infinite loading. In most cases, using `'normal'` is sufficient.

The second argument is the accessor's **action**. `fetchData` tells the accessor how to fetch data from the server, and `syncState` specifies how to sync the received data with the model's state.

The `defineAccessor` method returns an accessor creator function. If you pass the same arguments, it will return the same accessor. Now, let's use the accessor created by `defineAccessor` with the `useAccessor` hook.

```typescript
function usePost(id: number) {
    const accessor = getPostById(id);
    const { data, error, isFetching } = useAccessor(accessor, state => postAdapter.tryReadOne(state, id));
    
    return { post: data, error, isFetching, revalidate: () => accessor.revalidate() };
}
```

The second argument of `useAccessor` determines the shape of `data`. You can think of it as a selector function in Redux. In Daxus, we call this parameter `getSnapshot` because it provides a snapshot of the model's state. If you only want to retrieve the title of a specific post, you can write it like this:

```typescript
function usePostTitle(id: number) {
    const accessor = getPostById(id);
    const { data } = useAccessor(accessor, state => postAdapter.tryReadOne(state, id)?.title);
    
    return data;
}
```

Although both hooks subscribe to the same accessor, they will rerender at different times due to the difference in the second argument. `usePost` will rerender when the corresponding post data for the given `id` changes, while `usePostTitle` will only rerender when the title of the corresponding post with the given `id` changes.

It's important to note that `getSnapshot` is tied to the accessor. You must ensure that the props and state used in `getSnapshot` are the necessary parameters for the accessor creator. Otherwise, you may encounter unexpected behavior. In the above example, only `id` affects the accessor, so only `id` can be included in `getSnapshot`. You can think of this limitation as similar to the dependencies array in `useEffect`.

In addition to being used with `useAccessor`, accessors themselves have some methods that can be used, such as `accessor.revalidate` used in `usePost`. If there is no ongoing revalidation process for the accessor, calling this method will fetch data and synchronize it with the model.

### Benefits of Customized Data

Now, let's explore how Daxus addresses the issues mentioned in my previous article.

> Readers who are not familiar can refer to [this article][motivation].

First, let's define `getPostList`.

```typescript
const getPostList = postModel.defineAccessor<string, Post>('infinite', {
    fetchData: async (filter, { previousData }) => {
        if (previousData.length === 0) return null;
        const data = await getPostListApi(filter);
        return data;
    },
    syncState: (draft, payload) => {
        postAdapter.appendPagination(draft, payload.filter, payload.data);
    }
})
```

`getPostList` fetches different post lists based on different `filter` values and stores these lists in the model's state.

Suppose we individually fetched paginations with `filter` values of `'latest'` and `'popular'`, and both lists contain a post with the ID of 100. Now, if a user leaves a comment on this post and we execute the following code:

```typescript
postModel.mutate(draft => {
    postAdapter.readOne(draft, 100).totalCommentCount += 1;
})
```

When the user switches to the popular list, they will see that the total comment count of the post with ID 100 has been updated. If they then switch to the latest list, they will also see the same result. All of this is possible thanks to the customized data structure in Daxus. In the pagination adapter, all entities are managed centrally, and pagination claims data based on IDs. Therefore, whenever an entity is updated, any pagination that includes that entity will be updated as well. This perfectly solves the problem mentioned earlier.

## Conclusion

Although Daxus is not yet a mature package, I will continue to improve it. In addition, I have managed to persuade senior members in our company to apply it to our products. I hope it can help us completely eliminate Redux.

Finally, I want to express my gratitude once again to the community. Without your encouragement and practical support, the senior members of our company might not have agreed to let me do this. It's thanks to your recognition of Daxus that I was able to seize this opportunity. Thank you very much! I also hope that Daxus can help readers who are looking for alternative solutions. Have a wonderful day!

[motivation]:https://dev.to/jason89521/im-developing-a-new-server-state-management-library-3nhk