young woman with dark hair uses a marker to write on the whiteboard

Meet Paridokht: RTK Query and Leveraging New Libraries

“We’re trusted to use our time wisely to experiment and explore the new technologies out there – while also driving forward the business. There’s a learning culture I appreciate. Especially, because we know that tech is fast and growing every day.”

Nothing in life comes easy. But when a developer brings in new solutions to streamline your development processes, things come a lot easier.

This is where Paridokht Alavinia, a full-stack-developer-turned-frontend-developer with eight years’ of experience, takes center stage at adjoe. Before, she was testing C# and .NET technologies. Now, at adjoe, she writes unit tests to reduce defects in newly developed requirements and reduce bugs while changing the existing functionality.

Armed with both experience and an experimental mindset, she applies best practices for the RTK Query and Mock Service Worker libraries.

No Writing Data-Fetching and Caching Logic Yourself

What’s so great about RTK Query? Straight out the gate, RTK Query enables Paridokht to extract tasteful logic from a component, making it easy to test with RTK’s custom hooks. And besides that, with RTK Query, she doesn’t need to

  • write caching mechanisms from scratch
  • spend time and energy writing the code herself
  • risk introducing errors or bugs in the code – with RTK Query being community-supported, bugs would be edge cases
  • cause confusion in the developer community by writing improper code

Since implementing this advanced data-fetching and caching tool, Paridokht has so far been able to radically reduce time spent writing code by around 70 percent. That’s 70 percent less time writing or worrying about caching or fetching – just from implementing and running RTK Query on production for around three months.

A Case Study for Cleaner Code and Cache Mutations

Let’s say Paridokht wants to display a list of todos in adjoe’s application. She needs to fetch a list of todos from the API. She can use a React app using TypeScript and start a new project by running the command below in terminal.

npx create-react-app todos --template typescript

1. Once the demo project is ready, they need to install some dependencies.

npm install redux react-redux
npm install @reduxjs/toolkit

2. She defines the “Todo” type (if she’s using TypeScript in the application): src/types.d.ts.

type Todo = {
 userId: number;
 id: number;
 title: string;
 completed: boolean;
};

3. Now, she’s ready to create an API slice: src/api/apiSlice.ts.

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
 
export const apiSlice = createApi({
 reducerPath: "api",
 baseQuery: fetchBaseQuery({
   baseUrl: "https://jsonplaceholder.typicode.com",
 }),
 endpoints: (builder) => ({
   getTodos: builder.query<Todo[], void>({
     query: () => "/todos",
     transformResponse: (response: Todo[]) => {
       let filtered = response.filter((r) => r.id <= 10);
       return filtered.sort((a, b) => b.id - a.id);
     },
   }),
 }),
});
 
// custom hooks based on the methods that we provide
export const {
 useGetTodosQuery,
} = apiSlice;

4. After this, she needs to wrap the application component with “ApiProvider”: src/index.tsx.

import { ApiProvider } from "@reduxjs/toolkit/query/react";
import { apiSlice } from "./api/apiSlice";
root.render(
 <React.StrictMode>
   <ApiProvider api={apiSlice}>
     <App />
   </ApiProvider>
 </React.StrictMode>
);

5. Then, she can use the custom hook created by RTK Query in Todos.tsx component src/Todos.tsx.

import { useGetTodosQuery } from "../api/apiSlice";
 
export const Todos = () => {
 const { data: todos, isError, isLoading, error } = useGetTodosQuery();
 
 if (isLoading) return <p>Loading...</p>;
 if (isError) return <p>{JSON.stringify(error)}</p>;
 return (
   <>
     {todos?.map((todo) => (
       <p key={todo.id}>
         {todo.title}
       </p>
     ))}
   </>
 );
};

Utilize Custom Hooks for CRUD Operations

Paridokht can also add API calls for add, delete, and update operations and utilize custom hooks available from RTK Query. She can add code below to the API slice endpoints object src/api/apiSlice.ts.

  addTodo: builder.mutation({
     query: (todo: Todo) => ({
       url: "/todos",
       method: "POST",
       body: todo,
     }),
   }),
   updateTodo: builder.mutation({
     query: (todo: Todo) => ({
       url: `todos/${todo.id}`,
       method: "PUT",
       body: todo,
     }),
   }),
   deleteTodo: builder.mutation({
     query: (id: number) => ({
       url: `/todos/${id}`,
       method: "DELETE",
     }),
   }),
export const {
 useAddTodoMutation,
 useDeleteTodoMutation,
 useUpdateTodoMutation
} = apiSlice;

src/Todo.tsx

 const [addTodo] = useAddTodoMutation();
 const [deleteTodo] = useDeleteTodoMutation();
   const [updateTodo] = useUpdateTodoMutation();
 deleteTodo(todo.id);
 addTodo({id: 201, userId: 10, title: 'new todo is added', completed: false});
   updateTodo({ id: 193, userId: 10, title: "my first todo item", completed: true });

In this process, results get cached, and Paridokht doesn’t invalidate the previous cache. This means the component doesn’t display the new changes upon adding, deleting, or updating a todo item. To resolve this issue, she can assign a tag to the cache to let it know which mutation invalidates the cache to ensure it automatically refetches that data.

src/api/apiSlice.ts

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
 
export const apiSlice = createApi({
 reducerPath: "api",
 baseQuery: fetchBaseQuery({
   baseUrl: "https://jsonplaceholder.typicode.com",,
 }),
 tagTypes: ["Todos"], //defining tags' type
 endpoints: (builder) => ({
   getTodos: builder.query<Todo[], void>({
     query: () => "/todos",
     transformResponse: (response: Todo[]) => {
       let copy = response.filter((r) => r.id <= 10);
       return copy.sort((a, b) => b.id - a.id);
     },
     providesTags: ["Todos"], // defining a tag for this call
   }),
   addTodo: builder.mutation({
     query: (todo: Todo) => ({
       url: "/todos",
       method: "POST",
       body: todo,
     }),
     invalidatesTags: ["Todos"], // invalidate this tag for this mutation so that data can automatically re-fetched
   }),
   updateTodo: builder.mutation({
     query: (todo: Todo) => ({
       url: `todos/${todo.id}`,
       method: "PUT",
       body: todo,
     }),
     invalidatesTags: ["Todos"], // invalidate this tag for this mutation so that data can automatically re-fetched
   }),
   deleteTodo: builder.mutation({
     query: (id: number) => ({
       url: `/todos/${id}`,
       method: "DELETE",
     }),
     invalidatesTags: ["Todos"], // invalidate this tag for this mutation so that data can automatically re-fetched
   }),
 }),
});
 
// custom hooks based on the methods that we provide
export const {
 useGetTodosQuery,
 useAddTodoMutation,
 useDeleteTodoMutation,
 useUpdateTodoMutation,
} = apiSlice;

Besides eliminating the need to hand-write code yourself, the tool has more tricks up its sleeve to further optimize development processes.

Paridokht has her own ideas on how to further leverage RTK Query by use case. When the frequency of data change is high (for example, one second or less than 30 seconds), and the application should show data instantly; it should not use the cached data. However, when the data frequency is less and the response object is big (for example, more than 1MB), the team can enable caching. This enables a better user experience and also reduces the burden on adjoe’s servers.

mswjs: API Mocking for Testing, Development, and Debugging

In an ecosystem where there is little documentation on testing RTK Query hooks, adjoe’s frontend team found the mswjs library while researching what other React-active developers are utilizing out there.

Paridokht and her colleagues decided to use mswjs for two reasons. 1) To use mock data and develop new features in the frontend when the endpoint is not ready on the server side and 2) to write unit tests for components using RTK to fetch data. If you are unfamiliar with mswjs, you wouldn’t be the only one. But you should know the following:

  • The library is designed to intercept requests on the network level, and mock data helps write the test for RTK Query to test the API and components.
  • This way, Paridokht and her team don’t need to call the API on the server, and mocking is completely seamless.

Test Your Component in mswjs 

When Paridokht wants to test a component that fetches data from an adjoe API with the mswjs library as well as mock the API response, she needs to install the necessary dependencies.

npm install msw --save-dev

2. Then she can define adjoe’s mocks: src/test/mocks/todoMocks.ts.

export const todoMocks: Todo[] = [
 {
   id: 1,
   userId: 10,
   title:
     "temporibus atque distinctio omnis eius impedit tempore molestias pariatur",
   completed: true,
 },
 {
   id: 2,
   userId: 10,
   title: "ut quas possimus exercitationem sint voluptates",
   completed: false,
 },
 {
   id: 3,
   userId: 10,
   title: "rerum debitis voluptatem qui eveniet tempora distinctio a",
   completed: false,
 },
 {
   id: 4,
   userId: 10,
   title: "sed ut vero sit molestiae",
   completed: true,
 },
];

src/test/mocks/handlers.ts

import { rest } from "msw";
import { todoMocks } from "./todoMocks";
 
export const handlers = [
 rest.get("https://jsonplaceholder.typicode.com/todos", (_, res, ctx) =>
   res(ctx.status(200), ctx.json<Todo[]>(todoMocks))
 ),
];

3. Afterward, she sets up the server src/test/server.ts.

import { setupServer } from "msw/node";
import { handlers } from "./handlers";
 
export const server = setupServer(...handlers);

4. Finally, she can implement the unit test: src/Todos.spec.tsx.

import { screen, waitFor, render } from "@testing-library/react";
import { ApiProvider } from "@reduxjs/toolkit/query/react";
import { server } from "../test/server";
import { Todos } from "./Todos";
import { apiSlice } from "../api/apiSlice";
 
describe("Todos", () => {
 beforeAll(() => {
   server.listen();
 });
 
 afterEach(() => {
   server.resetHandlers();
 });
 
 afterAll(() => {
   server.close();
 });
 
 it("should display todos", async () => {
   render(
     <ApiProvider api={apiSlice}>
       <Todos />
     </ApiProvider>
   );
 
   await waitFor(() => {
     expect(screen.getByText("sed ut vero sit molestiae")).toBeInTheDocument();
   });
 });
});

Paridokht and other frontend developers at adjoe will be able to develop mswjs incrementally in order to improve the unit tests of components that make API calls by defining mocks at the network level. Better still, they can seamlessly reuse the same mock definition for testing, development, and debugging.

The Core of Quality Code and Faster Frontend Processes

It’s not just tools such as RTK Query and mswjs that form the foundation of seamless debugging, development, and testing processes at adjoe. It’s the frontend team behind them who develops and optimizes them in agile environments. Without these tools, the implementation process would require adjoe’s developers to hand-write code themselves. With them, adjoe can enjoy impressive scalability and efficiency at its fingertips.

Looking beyond her frontend tasks, Paridokht is equipped with backend experience and her ideas of type-safe language, naming, and design. She prioritizes the bigger picture when it comes to code quality, company processes, and user experiences. And when she’s not exploring how adjoe can leverage languages and tools to advance teams toward their goals, the senior frontend developer carefully considers how technologies and her way of working can enhance other areas of the Tech teams at adjoe.

Playtime Supply

Frontend Developer (f/m/d)

  • Full-time,
  • Hamburg

Go Backend Developer (f/m/d)

  • Full-time,
  • Hamburg

QA Engineer (f/m/d)

  • Full-time,
  • Hamburg

UI/UX Designer (f/m/d)

  • Full-time,
  • Hamburg

Playtime Supply

Frontend Developer (f/m/d)

  • Full-time,
  • Hamburg

Go Backend Developer (f/m/d)

  • Full-time,
  • Hamburg

QA Engineer (f/m/d)

  • Full-time,
  • Hamburg

UI/UX Designer (f/m/d)

  • Full-time,
  • Hamburg