React Refactoring (w/Storybook): Separate UI and Business Logic

lico

SeongKuk Han

Posted on September 24, 2022

React Refactoring (w/Storybook): Separate UI and Business Logic

React Refactoring (w/Storybook): Separate UI and Business Logic

When I joined my previous company, no one knew about the project code, the developer who made the project left.
There were many components, I didn't know what components had what kind of business logic. There were components that I wanted to reuse but I couldn't because they had a complicated business logic that could affect other components' behaviors. This makes it hard to recycle the component and reuse the UI, and sometimes I even made a component that already existed as similar looking.

One day, I decided to refactor them. For that, I separated UI and Business logic, and I used storybook for listing components. After this work, I was able to reuse components' UIs easily and see how they look.


I'll show you an example of this.
There is a component TodayVisitorCard that shows a number of visitors.



import { useEffect, useState } from "react";
import styled from "@emotion/styled";

const CardWrapper = styled.div`
  border: 1px solid black;
  border-radius: 8px;
  box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
  padding: 24px;
  text-align: center;
  width: fit-content;
`;

interface VisitorResponse {
  total: number;
  today: number;
}

const TodayVisitorCard = () => {
  const [data, setData] = useState<VisitorResponse | null>();
  const [hasError, setHasError] = useState(false);

  const fetchVisitor = async () => {
    try {
      const res = await fetch("http://localhost:7777/visitor");

      if (res.status !== 200) {
        throw new Error(`Response status is ${res.status}.`);
      }

      const data = await res.json();
      setData(data);
    } catch (e) {
      console.error(e);
      setHasError(true);
      setData(null);
    }
  };

  useEffect(() => {
    fetchVisitor();
  }, []);

  if (data === undefined) return <div>loading...</div>;

  return (
    <CardWrapper>
      {hasError && <div>Something went wrong!</div>}
      {data && (
        <>
          <h2>Total: {data.total}</h2>
          <span>Today: {data.today}</span>
        </>
      )}
    </CardWrapper>
  );
};

export default TodayVisitorCard;


Enter fullscreen mode Exit fullscreen mode

Today Visitor Card

Let's separate the logic.



import { useEffect, useState } from "react";
import styled from "@emotion/styled";

const CardWrapper = styled.div`
  border: 1px solid black;
  border-radius: 8px;
  box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
  padding: 24px;
  text-align: center;
  width: fit-content;
`;

interface VisitorResponse {
  total: number;
  today: number;
}

const TodayVisitorCard = () => {
  const [data, setData] = useState<VisitorResponse | null>();
  const [hasError, setHasError] = useState(false);

  const fetchVisitor = async () => {
    try {
      const res = await fetch("http://localhost:7777/visitor");

      if (res.status !== 200) {
        throw new Error(`Response status is ${res.status}.`);
      }

      const data = await res.json();
      setData(data);
    } catch (e) {
      console.error(e);
      setHasError(true);
      setData(null);
    }
  };

  useEffect(() => {
    fetchVisitor();
  }, []);

  if (data === undefined) return <div>loading...</div>;

  return <TodayVisitorCardUI {...data} hasError={hasError} />;
};

const TodayVisitorCardUI = ({
  hasError,
  total,
  today,
}: {
  hasError?: boolean;
  total?: number;
  today?: number;
}) => {
  return (
    <CardWrapper>
      {hasError && <div>Something went wrong!</div>}
      {total && <h2>Total: {total}</h2>}
      {today && <span>Today: {today}</span>}
    </CardWrapper>
  );
};

export { TodayVisitorCard };
export default TodayVisitorCardUI;


Enter fullscreen mode Exit fullscreen mode

Now, you can use other business logic easily with the UI, and
you can list up components UIs with storybook like below.

file structure



import { ComponentStory, ComponentMeta } from "@storybook/react";

import TodayVisitorCard from ".";

export default {
  title: "\"components/TodayVisitorCard\","
  component: TodayVisitorCard,
  args: {
    hasError: false,
  },
  argTypes: {
    today: {
      type: "number",
    },
    total: {
      type: "number",
    },
  },
} as ComponentMeta<typeof TodayVisitorCard>;

const Template: ComponentStory<typeof TodayVisitorCard> = (args) => (
  <TodayVisitorCard {...args} />
);

export const Example = Template.bind({});

Example.args = {
  today: 6522,
  total: 139,
};

export const Error = Template.bind({});

Error.args = {
  hasError: true,
};


Enter fullscreen mode Exit fullscreen mode

first storybook example

second storybook example


Conclusion

In the real world, It would be more complicated. There might be lots of things you should consider about. Structures and Naming Conventions might be one of them. You should make your own strategy that fits your project.

I hope it would be helpful for someone.
Happy Coding!

💖 💪 🙅 🚩
lico
SeongKuk Han

Posted on September 24, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related