DEV Community

Xuan
Xuan

Posted on • Updated on

Daxus v0.3.0 is released!

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!


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:

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 })],
  }
);

Enter fullscreen mode Exit fullscreen mode

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:

['posts', 'list', 'all']
['posts', 'get', 1]
Enter fullscreen mode Exit fullscreen mode

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.

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

const postAdapter = createPaginationAdapter<Post>();
const postModel = createModel(postAdapter.initialState);
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
})
Enter fullscreen mode Exit fullscreen mode

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.

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() };
}
Enter fullscreen mode Exit fullscreen mode

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:

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

    return data;
}
Enter fullscreen mode Exit fullscreen mode

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.

First, let's define getPostList.

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);
    }
})
Enter fullscreen mode Exit fullscreen mode

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:

postModel.mutate(draft => {
    postAdapter.readOne(draft, 100).totalCommentCount += 1;
})
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)