Lädt...


🔧 Path To A Clean(er) React Architecture - API Layer & Data Transformations


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

The unopinionated nature of React is a two-edged sword:

  • On the one hand you get freedom of choice.
  • On the other hand many projects end up with a custom and often messy architecture.

This article is the third part of a series about software architecture and React apps where we take a code base with lots of bad practices and refactor it step by step.

Previously, we created the initial API layer and extracted fetch functions that can be used in the components. This way we already removed a lot of implementation details related to API requests from the UI code.

But we’re not done yet.

In this article, we identify data transformations in the component that can be moved to the API layer. Another step towards a cleaner architecture.

React Architecture Course Waitlist

Bad Code - Example 1: API responses inside UI code

Let’s have a look at a problematic code example. Here is a user profile component that fetches data from two different endpoints.

Can you identify the problem?

import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router";

import UserApi from "@/api/user";
import { LoadingSpinner } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { UserResponse, UserShoutsResponse } from "@/types";

import { UserInfo } from "./user-info";

export function UserProfile() {
  const { handle } = useParams<{ handle: string }>();

  const [user, setUser] = useState<UserResponse>();
  const [userShouts, setUserShouts] = useState<UserShoutsResponse>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    if (!handle) {
      return;
    }

    UserApi.getUser(handle)
      .then((response) => setUser(response))
      .catch(() => setHasError(true));

    UserApi.getUserShouts(handle)
      .then((response) => setUserShouts(response))
      .catch(() => setHasError(true));
  }, [handle]);

  if (!handle) {
    return <Navigate to="/" />;
  }

  if (hasError) {
    return <div>An error occurred</div>;
  }
  if (!user || !userShouts) {
    return <LoadingSpinner />;
  }

  return (
    <div className="max-w-2xl w-full mx-auto flex flex-col p-6 gap-6">
      <UserInfo user={user.data} />
      <ShoutList
        users={[user.data]}
        shouts={userShouts.data}
        images={userShouts.included}
      />
    </div>
  );
}

For completion, these are the fetch functions used in the component: UserAPI.getUser() and UserAPI.getUserShouts()

export interface UserResponse {
  data: User;
}

async function getUser(handle: string) {
  const response = await apiClient.get<UserResponse>(`/user/${handle}`);
  return response.data;
}

export interface UserShoutsResponse {
  data: Shout[];
  included: Image[];
}

async function getUserShouts(handle: string) {
  const response = await apiClient.get<UserShoutsResponse>(
    `/user/${handle}/shouts`
  );
  return response.data;
}

What is the problem?

We have the raw response data structures inside the component. But if you think about it, the UI code

  • shouldn’t care that the User object is returned as response.data
  • shouldn’t need to know that the shouts are in response.data and the images inside response.included.

Imagine we pass these response data structures to a dozen or more components in a larger project. Now imagine that the API changes the data structure. We’d have to adjust all of those components one by one.

Additionally, the code would be more readable if we wouldn’t have to deal with all those data and included fields anymore. Name it user, shout, or image instead.

To summarize: All this knowledge can be hidden inside the API layer as this is where we deal with API responses and the fetch functions should return the final User, Shout, or Image data structures.

Solution: Use data transformations in the API layer

The goal is to remove all code related to API response data structures from the component.

The solution is to use data transformations in the fetch functions:

export interface UserResponse {
  data: User;
}

async function getUser(handle: string) {
  const response = await apiClient.get<{ data: User }>(`/user/${handle}`);
  // first `.data` comes from axios
  const user = response.data.data;
  return user;
}

export interface UserShoutsResponse {
  data: Shout[];
  included: Image[];
}

async function getUserShouts(handle: string) {
  const response = await apiClient.get<UserShoutsResponse>(
    `/user/${handle}/shouts`
  );
  const shouts = response.data.data;
  const images = response.data.included;
  return { shouts, images };
}

Now the fetch functions return the actual data structures like User, Shout, or Image.

This allows us to remove the response data structures from the component:

export function UserProfile() {
  const { handle } = useParams<{ handle: string }>();

  const [user, setUser] = useState<User>();
  const [shouts, setShouts] = useState<Shout[]>();
  const [images, setImages] = useState<Image[]>([]);
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    if (!handle) {
      return;
    }

    UserApi.getUser(handle)
      .then((user) => setUser(user))
      .catch(() => setHasError(true));

    UserApi.getUserShouts(handle)
      .then(({ shouts, images }) => {
        setShouts(shouts);
        setImages(images);
      })
      .catch(() => setHasError(true));
  }, [handle]);

  ...

  return (
    <div className="max-w-2xl w-full mx-auto flex flex-col p-6 gap-6">
      <UserInfo user={user} />
      <ShoutList users={[user]} shouts={shouts} images={images} />
    </div>
  );
}

As you can see, all the code related to responses has vanished from the component. Not to forget that we got rid of the nasty data and included fields.

At the same time, the only changes so far are

  • we returned some nested fields instead of the complete response
  • we renamed some fields.

Clearly things can be more complex. So let’s look at another example.

React Architecture Course Waitlist

Bad Code - Example 2: Data transformations in UI code

Here’s another component of the example project: it’s a feed that shows the newest “Shouts” (aka Tweets) to a user.

import FeedApi from "@/api/feed";
import { LoadingView } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { FeedResponse, Image, User } from "@/types";

export function Feed() {
  const [feed, setFeed] = useState<FeedResponse>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    FeedApi.getFeed()
      .then((feed) => setFeed(feed))
      .catch(() => setHasError(true));
  }, []);
  if (hasError) {
    return <div>An error occurred</div>;
  }
  if (!feed) {
    return <LoadingView />;
  }

  const users = feed.included.filter((u): u is User => u.type === "user");
  const images = feed.included.filter((i): i is Image => i.type === "image");
  return (
    <div className="w-full max-w-2xl mx-auto flex flex-col justify-center p-6 gap-6">
      <ShoutList shouts={feed.data} users={users} images={images} />
    </div>
  );
}

We can again see that the response is stored in the component. And as before the fetch function directly returns the API response data structure.

async function getFeed() {
  const response = await apiClient.get<FeedResponse>("/feed");
  return response.data;
}

What is the problem?

Ok, this looks pretty much like the same problem. Apart from these two lines of code:

const users = feed.included.filter((u): u is User => u.type === "user");
const images = feed.included.filter((i): i is Image => i.type === "image");

Apparently, we again have to deal with an included field. But this time, that array not only contains images but also users. And the above two lines are data transformation logic that separate objects of type user and image.

Meaning we have data transformation logic inside the component.

But the component

- shouldn’t care that the `included` field in the feed response contains users and images
- shouldn’t need to know that it can distinguish the users and images by their `type` field.

Note: You think the response data structure with the data field and the included field with mixed data types looks weird? You might not have encountered the popular JSON:API standard yet.

Solution: Move data transformation logic to the fetch function

Again, the goal is to remove this logic from the component.

The solution is simple: we move the two lines to the fetch function.

import { FeedResponse, Image, User } from "@/types";

import { apiClient } from "./client";

async function getFeed() {
  const response = await apiClient.get<FeedResponse>("/feed");
  const shouts = response.data.data;
  const users = response.data.included.filter(
    (u): u is User => u.type === "user"
  );
  const images = response.data.included.filter(
    (i): i is Image => i.type === "image"
  );
  return { shouts, users, images };
}

export default { getFeed };

Now the fetch function returns the actual data structures User, Shout, and Image and doesn’t expose API responses to the UI code anymore.

The component becomes a lot simpler:

import { useEffect, useState } from "react";

import FeedApi from "@/api/feed";
import { LoadingView } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { Image, Shout, User } from "@/types";

export function Feed() {
  const [feed, setFeed] = useState<{
    shouts: Shout[];
    images: Image[];
    users: User[];
  }>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    FeedApi.getFeed()
      .then((feed) => setFeed(feed))
      .catch(() => setHasError(true));
  }, []);

  if (hasError) {
    return <div>An error occurred</div>;
  }

  if (!feed) {
    return <LoadingView />;
  }
  return (
    <div className="w-full max-w-2xl mx-auto flex flex-col justify-center p-6 gap-6">
      <ShoutList shouts={feed.shouts} users={feed.users} images={feed.images} />
    </div>
  );
}

Now the component directly gets the users, shouts, and images. It has no knowledge of the API responses anymore.

Great, our code becomes more streamlined. But so far we only focused on response data. What about input data like request bodies?

Bad Code - Example 3: Transformation of input data

Here’s yet another component: A dialog that allows the user to reply to another user’s shout (aka tweet).

import MediaApi from "@/api/media";
import ShoutApi from "@/api/shout";
import UserApi from "@/api/user";

...

export function ReplyDialog({ children, shoutId }: ReplyDialogProps) {
  const [open, setOpen] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [hasError, setHasError] = useState(false);

  ...

  async function handleSubmit(event: React.FormEvent<ReplyForm>) {
    event.preventDefault();
    setIsLoading(true);
    try {
      const message = event.currentTarget.elements.message.value;
      const files = event.currentTarget.elements.image.files;
      let imageId = undefined;
      if (files?.length) {
        const formData = new FormData();
        formData.append("image", files[0]);
        const image = await MediaApi.uploadImage(formData);
        imageId = image.data.id;
      }
      const newShout = await ShoutApi.createShout({
        message,
        imageId,
      });
      await ShoutApi.createReply({
        shoutId,
        replyId: newShout.data.id,
      });
      setOpen(false);
    } catch (error) {
      console.error(error);
    } finally {
      setIsLoading(false);
    }
  }

  // a form using the handleSubmit function is rendered here
  return (
    ...
  );
}

The chained requests are a problem for the future. Here we focus on the preparation of the input data. Particularly, the image upload.

What is the problem?

It appears that the MediaApi expects the image to be added to a FormData object.

  • But is this something the component should be concerned with?
  • Why should it matter to the UI code whether the image is uploaded using form data or any other mechanism?

Let’s imagine we wanted to switch to another cloud storage like AWS S3? We’d most likely have to adjust the component code as well.

And since other places like the user profile are likely to upload images as well we’d have to adjust multiple parts of the codebase just because the underlying image storage changed.

We can do better.

Solution: Move transformation of input data to API layer

Similar to the previous examples, let’s move the data transformation to the API layer. Here’s the adjusted MediaApi.uploadImage() function:

import { Image } from "@/types";

import { apiClient } from "./client";

async function uploadImage(file: File) {
  const formData = new FormData();
  formData.append("image", file);

  const response = await apiClient.post<{ data: Image }>("/image", formData);
  const image = response.data.data;
  return image;
}

export default { uploadImage };

The fetch function in the API layer now expects a File as input. The exact upload mechanism is hidden from the component.

This simplifies the component code quite a bit:

export function ReplyDialog({ children, shoutId }: ReplyDialogProps) {
  const [open, setOpen] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [hasError, setHasError] = useState(false);

  ...

  async function handleSubmit(event: React.FormEvent<ReplyForm>) {
    event.preventDefault();
    setIsLoading(true);
    try {
      const message = event.currentTarget.elements.message.value;
      const files = event.currentTarget.elements.image.files;
      let image;
      if (files?.length) {
        image = await MediaApi.uploadImage(files[0]);
      }
      const newShout = await ShoutApi.createShout({
        message,
        imageId,
      });
      await ShoutApi.createReply({
        shoutId,
        replyId: newShout.data.id,
      });
      setOpen(false);
    } catch (error) {
      console.error(error);
    } finally {
      setIsLoading(false);
    }
  }

  // a form using the handleSubmit function is rendered here
  return (
    ...
  );
}

The component is still responsible getting the files from the event target and passing only the first one files[0] to the media API. The rest of the heavy lifting is handled by the API.

Next refactoring steps

We took another step closer towards a cleaner React architecture. Data transformations are now handled by the API layer.

But there’s still room for improvement. For example, the fetch functions still pass the data structures User, Shout, or Image as they are returned from the API.

And look at this example. Ain’t pretty, right?

{
    "id": "shout-1",
    "type": "shout",
    "createdAt": 1713455132695,
    "attributes": {
        "authorId": "user-1",
        "text": "The world sucks!!!!",
        "likes": 5,
        "reshouts": 0
    },
    "relationships": {
        "replies": [
            "shout-3"
        ]
    }
}

In the next article, we will introduce a domain layer that will help us get rid of this complexity.

React Architecture Course Waitlist

...

🔧 Path To A Clean(er) React Architecture (Part 8) - How Does React-Query Fit Into The Picture?


📈 42.54 Punkte
🔧 Programmierung

🔧 Comparing All-in-One Architecture, Layered Architecture, and Clean Architecture


📈 42.34 Punkte
🔧 Programmierung

🔧 Path To A Clean(er) React Architecture - Domain Entities & DTOs


📈 37.79 Punkte
🔧 Programmierung

🔧 Path To A Clean(er) React Architecture (Part 7) - Domain Logic


📈 35.43 Punkte
🔧 Programmierung

🔧 Path To A Clean(er) React Architecture (Part 6) - Business Logic Separation


📈 35.43 Punkte
🔧 Programmierung

🔧 What is Clean Architecture: Part 14-last step in Application Core Layer


📈 33.23 Punkte
🔧 Programmierung

🔧 Implementing Clean Architecture and Adding a Caching Layer


📈 33.23 Punkte
🔧 Programmierung

🔧 The difference between clean code and clean architecture?


📈 32.97 Punkte
🔧 Programmierung

📰 dns: a foundational security architecture for digital transformations


📈 31.77 Punkte
📰 IT Security Nachrichten

🔧 This Week In React #185: React Conf, React Query, refs, Next.js after, mini-react...


📈 28.46 Punkte
🔧 Programmierung

🔧 This Week In React #185: React Conf, React Query, refs, Next.js after, mini-react...


📈 28.46 Punkte
🔧 Programmierung

🔧 Excel Tutorial – How to Clean Data with the TRIM() and CLEAN() Functions


📈 25.91 Punkte
🔧 Programmierung

🔧 Figma to React: Convert designs to clean React code


📈 25.55 Punkte
🔧 Programmierung

🔧 How data are reshaping society: “Datafication” and socioeconomic transformations


📈 24.71 Punkte
🔧 Programmierung

🔧 The Future of Data Analytics with AI: Transformations and Opportunities


📈 24.71 Punkte
🔧 Programmierung

🎥 Cloud and Data Center transformations


📈 24.71 Punkte
🎥 Video | Youtube

📰 Authoring custom transformations in Amazon SageMaker Data Wrangler using NLTK and SciPy


📈 24.71 Punkte
🔧 AI Nachrichten

📰 CX Goals Are Driving Digital Transformations—but Better Data Needed


📈 24.71 Punkte
📰 IT Nachrichten

🕵️ CVE-2022-41722 | path-filepath on Windows filepath.Clean path traversal


📈 24.63 Punkte
🕵️ Sicherheitslücken

🔧 React Native/React Architecture


📈 24.57 Punkte
🔧 Programmierung

🔧 React Native/React Architecture


📈 24.57 Punkte
🔧 Programmierung

🔧 Что такое React Fiber - React Fiber Architecture


📈 24.57 Punkte
🔧 Programmierung

🔧 Event-Driven Architecture: Do you need other service’s data in microservice architecture


📈 23.95 Punkte
🔧 Programmierung

🔧 The Art of Writing Clean Functions: Clean Code Practices


📈 22.63 Punkte
🔧 Programmierung

🔧 The Clean Code book and the clean code paradigms


📈 22.63 Punkte
🔧 Programmierung

📰 How to Clean Up the Clean Energy Transition: Preventing Violence Over New ‘Conflict Minerals’


📈 22.63 Punkte
📰 IT Security Nachrichten

🔧 🧹 It's Time to Spring Clean Your Codebase: Celebrate National Clean Out Your Computer Day! 🖥️ ✨


📈 22.63 Punkte
🔧 Programmierung

matomo