Back

Separation of concerns in modern React apps - key principles and examples

Building React apps, ever wondered where to put that "thing", and how to name it?
Rafał ŚwiętekFeb 22, 2021 | 7 min read

A few years ago, when React started becoming popular, one of the things that made developers love it was its flexibility in implementation. Everyone pointed out that it’s not a framework, but a library and anyone can implement it as one likes.

Well, that hasn’t changed yet, but React mutated so much that if you take a look at the app’s code written using their technology in 2016 and compare it to what we write today, you would probably think that these are 2 completely different things.

In 2019, hooks were introduced and that significantly changed the way we create and structure our components and logic. Hooks began to gradually push class components out and it feels like we finally have a clean and dedicated solution for separating logic from the visual layer.

Structuring components and logic before hooks

Let’s not spend too much time talking about the past. Shortly, the most popular solution for separating the view from logic, that I’ve seen in React apps before hooks, was using class components called Containers that contained the whole logic and wrapping other components that would contain just the view.

That solution had at least two very important disadvantages. First, it was sometimes difficult to reuse those containers, and second, we usually had to use some external state management libraries anyway to share the state between different parts of the application.

Back-end experience much appreciated

There’s a general opinion suggesting that people who want to learn some web development should start from the front-end because it’s “easier” (whatever that means). I tend to disagree with that. Modern technologies meant that more and more logic can actually appear on the front-end. There are arguments for and against but it’s already happening, whether you like it or not, and front-end developers need to get to know how to deal with handling that logic.

A general idea behind a good separation of concerns

Imagine you want to buy a Snickers from the vending machine. You put some coins inside, you tap on the “SNICKERS” button, and voila! Your favorite chocolate bar is now waiting for you to take the first bite.

Now, I know that might sound like we’re moving a little bit off the main topic but we’re getting somewhere, I promise.

You just bought something from the vending machine, you just used the vending machine, but you don’t really care about how the vending machine works, do you?

The whole process that the machine had to go through to give you the item is completely out of context when you just wanted to get that delicious snack.

You don’t want to know that the machine had to count the money you put inside, push a product from shelf #12, then move the product to the distribution pocket, and, as a side effect, print out a receipt.

The vending machine probably also doesn’t care about the fact that you’d like to eat a Snickers. It doesn’t need to know that the button you pushed had a “SNICKERS” logo on it. For that machine, it’s just a product laying on shelf #12.

vending machine

In this example, you’re just like a React component trying to use a custom hook (the vending machine). I think you already know what I’m trying to say. From here, the more interesting part starts.

Practical examples

Let’s say we’re developing an e-commerce app and we’re currently working on the product page. First, we want to display some details about the product. Here’s how we could do that without any custom hook.

1const ProductPage = ({ productId }) => {
2 const [product, setProduct] = useState(null);
3 const [selectedVariantId, setSelectedVariantId] = useState(0);
4
5 useEffect(() => {
6 const fetchProduct = async () => {
7 const res = await fetch(`https://api.example.com/products/${productId}`);
8 setProduct(res.json());
9 };
10 fetchProduct();
11 }, []);
12
13 const onSubmit = () => {
14 // ...
15 };
16
17 if (!product) return null;
18
19 return (
20 <Container>
21 <Name>{product.name}</Name>
22 <Price>{product.discountPrice || product.price}</Price>
23
24 {
25 product.discountPrice
26 && product.price - product.discountPrice > 100
27 && <SuperSaleBadge />
28 }
29
30 <Description>{product.description}</Description>
31 <VariantSwitch
32 variants={product.variants}
33 value={selectedVariantId}
34 onClick={setSelectedVariantId}
35 />
36 <Button onClick={onSubmit}>Add to cart</Button>
37 </Container>
38 );
39};

Besides the fact that the logic is mixed with the view layer, there are already a few naming issues that we’re gonna fix right now.

Choosing components' props names

First, the name of a function that we pass to the onClick prop of the <Button /> component - onSubmit.

I’ve seen this kind of problem many times. It could also be called onClick (just like the prop name) or sometimes onClickHandler. These are all wrong names. Just as it’s good for the name of a component’s prop, as bad for a name of a function.

You could ask, why is it ok to be a name of the <Button /> prop, and not of a function that we pass over that prop? Well, mainly because the name of the prop was made in a completely different context - the Button’s context.

See, the <Button /> is a general and reusable component. Obviously, it shouldn’t know what kind of operation we want to execute when someone clicks it. From the inside of the component, it’s clear that the onClick prop is a handler that we want to run after someone clicks on the button. From the outside of the component, it’s also pretty clear. A button is something that users can click. It makes perfect sense that whatever we pass to that prop is going to be executed on the click event.

On the other hand, we have the context where we actually use the <Button /> component. Reading the code, we would like to know what is going to happen when someone clicks it. That onSubmit function name tells us nothing. We could name it foo instead and that would have the same effect.

That onSubmit function should be named addToCart to tell what it actually does.

Choosing names for handlers

Second, names of <VariantSwitch /> component’s props - value and onClick.

Compared to the <Button /> component, <VariantSwitch /> is not that general. By its name, we can tell that it’s some custom component allowing it to switch between product variants. It’s been designed in a way that we could imagine using the same component for something else. It’s strictly tied to the variant’s context.

Let’s start with the value prop name. Ok, it’s not that bad, but I think it could be even better. Once we start using a component named <VariantSwitch /> we could probably figure out that its value prop should receive some information about the currently selected variant. But don’t you agree that selectedVariantId would be much clearer?

Now, when it comes to the onClick prop. We just said that it’s an ok prop name for the <Button /> component. Why isn’t it ok for the <VariantSwitch />, right? We just agreed that this component is bound to some context, and that context isn’t about clicking or buttons. It’s about switching between variants.

This component is here to add a layer of abstraction over its actual implementation. We shouldn’t know or care about how this switch actually handles switching between variants. In the future, someone may change the way that variant switch works, and instead of clicking on some buttons, now users need to swipe or scroll. Should we now change its onClick prop name to onScroll and refactor every component where we use that switch?

Instead, that prop could be named e.g. onVariantSelection. This way we leave all the implementation details to the <VariantSwitch /> component itself. All we care about is that it will trigger a function from that prop whenever someone selects a variant. Sounds reasonable, right?

1const ProductPage = ({ productId }) => {
2 const [product, setProduct] = useState(null);
3 const [selectedVariantId, setSelectedVariantId] = useState(0);
4
5 useEffect(() => {
6 const fetchProduct = async () => {
7 const res = await fetch(`https://api.example.com/products/${productId}`);
8 setProduct(res.json());
9 };
10 fetchProduct();
11 }, []);
12
13 const addToCart = () => {
14 // ...
15 };
16
17 if (!product) return null;
18
19 return (
20 <Container>
21 <Name>{product.name}</Name>
22 <Price>{product.discountPrice || product.price}</Price>
23
24 {
25 product.discountPrice
26 && product.price - product.discountPrice > 100
27 && <SuperSaleBadge />
28 }
29
30 <Description>{product.description}</Description>
31 <VariantSwitch
32 variants={product.variants}
33 selectedVariantId={selectedVariantId}
34 onVariantSelection={setSelectedVariantId}
35 />
36 <Button onClick={addToCart}>Add to cart</Button>
37 </Container>
38 );
39};

That’s better! Now we can take care of that ugly API request fetching product details.

Build your modern Web App with top React & Node.js engineers

First architecture improvements

We should move it out of our <ProductPage /> component to start separating logic that is connected with using a single product. It will help us with further development and app maintenance because we won’t need to untangle logic from the view, plus we will be able to find everything connected with the same context in a single place. Also, it will help us stay DRY when we’ll need to refer to a single product in a different place in the code.

The way I prefer is to create a custom hook that would become a layer of abstraction over the logic I want to separate. What name should the hook have? The key is always the context. We’d like to create a hook that handles everything connected within the context of a single product. It makes sense to just name it useSingleProduct:

1const useSingleProduct = (productId) => {
2 const [product, setProduct] = useState(null);
3
4 const fetchProduct = async () => {
5 const res = await fetch(`https://api.example.com/products/${productId}`);
6 setProduct(res.json());
7 };
8
9 useEffect(() => {
10 fetchProduct();
11 }, [productId]);
12
13 return product;
14};
15
16const ProductPage = ({ productId }) => {
17 const [selectedVariantId, setSelectedVariantId] = useState(0);
18 const product = useSingleProduct(productId);
19
20 const addToCart = () => {
21 // ...
22 };
23
24 if (!product) return null;
25
26 return (
27 <Container>
28 <Name>{product.name}</Name>
29 <Price>{product.discountPrice || product.price}</Price>
30
31 {
32 product.discountPrice
33 && product.price - product.discountPrice > 100
34 && <SuperSaleBadge />
35 }
36
37 <Description>{product.description}</Description>
38 <VariantSwitch
39 variants={product.variants}
40 selectedVariantId={selectedVariantId}
41 onVariantSelection={setSelectedVariantId}
42 />
43 <Button onClick={addToCart}>Add to cart</Button>
44 </Container>
45 );
46};

Perfect! Finally, we have a separate place for some single product related logic. Let’s see if there is anything else we could move to that hook.

Move the business logic out of your components

Take a look at the JSX code from the example above. Especially two segments: displaying the price and the “super sale” badge.

If you notice some comparisons or more complicated conditions mixed with the JSX code, that’s usually a sign it could be abstracted away. Why should your view template know which price of the product is the active one? Why should your view template know what conditions the product needs to meet to be considered as one on “super sale”?

Both price, and “super sale” badge will probably show up on the product list too. Does it mean we should have this kind of logic in two separate places? Instead, we could move this code into the place where it belongs - the useSingleProduct hook.

Whenever the conditions for displaying the “super sale” badge changes, we can refactor just the hook and be sure that we covered all cases. The view is responsible just for displaying (or not) the data. Only the hook knows what data is correct.

In the case of the “super sale” badge, for some of us, it might be tempting to export from the hook a parameter named e.g. shouldDisplaySuperSaleBadge, or something similar. Remember that we want to achieve a complete separation between the hook and the component. They should not know anything about themselves and they should not know how they are going to be used. A better name for that parameter would be isOnSuperSale. This way the hook stays more universal. See how the refactoring might look like through the example below:

1const useSingleProduct = (productId) => {
2 const [product, setProduct] = useState(null);
3
4 const fetchProduct = async () => {
5 const res = await fetch(`https://api.example.com/products/${productId}`);
6 setProduct(res.json());
7 };
8
9 useEffect(() => {
10 fetchProduct();
11 }, [productId]);
12
13 const currentPrice = useMemo(() => product?.discountPrice || product?.price, [product]);
14
15 const isOnSuperSale = useMemo(() => product?.discountPrice && product?.price - product.discountPrice > 100, [product]);
16
17 return {
18 product,
19 currentPrice,
20 isOnSuperSale,
21 };
22};
23
24const ProductPage = ({ productId }) => {
25 const [selectedVariantId, setSelectedVariantId] = useState(0);
26 const {
27 product,
28 currentPrice,
29 isOnSuperSale,
30 } = useSingleProduct(productId);
31
32 const addToCart = () => {
33 // ...
34 };
35
36 if (!product) return null;
37
38 return (
39 <Container>
40 <Name>{product.name}</Name>
41 <Price>{currentPrice}</Price>
42
43 {isOnSuperSale && <SuperSaleBadge />}
44
45 <Description>{product.description}</Description>
46 <VariantSwitch
47 variants={product.variants}
48 selectedVariantId={selectedVariantId}
49 onVariantSelection={setSelectedVariantId}
50 />
51 <Button onClick={addToCart}>Add to cart</Button>
52 </Container>
53 );
54};

Great! There’s one more thing we could do with this example to make it even better. The addToCart function - it definitely should go to the useCart hook, right?

1const useSingleProduct = (productId) => {
2 const [product, setProduct] = useState(null);
3
4 const fetchProduct = async () => {
5 const res = await fetch(`https://api.example.com/products/${productId}`);
6 setProduct(res.json());
7 };
8
9 useEffect(() => {
10 fetchProduct();
11 }, [productId]);
12
13 const currentPrice = useMemo(() => product?.discountPrice || product?.price, [product]);
14
15 const isOnSuperSale = useMemo(() => product?.discountPrice && product?.price - product.discountPrice > 100, [product]);
16
17 return {
18 product,
19 currentPrice,
20 isOnSuperSale,
21 };
22};
23
24const ProductPage = ({ productId }) => {
25 const [selectedVariantId, setSelectedVariantId] = useState(0);
26 const {
27 product,
28 currentPrice,
29 isOnSuperSale,
30 } = useSingleProduct(productId);
31
32 const { add: addToCart } = useCart();
33
34 if (!product) return null;
35
36 return (
37 <Container>
38 <Name>{product.name}</Name>
39 <Price>{currentPrice}</Price>
40
41 {isOnSuperSale && <SuperSaleBadge />}
42
43 <Description>{product.description}</Description>
44 <VariantSwitch
45 variants={product.variants}
46 selectedVariantId={selectedVariantId}
47 onVariantSelection={setSelectedVariantId}
48 />
49 <Button onClick={addToCart}>Add to cart</Button>
50 </Container>
51 );
52};

Last thoughts

I think you should now get the whole concept. Those rules seem very simple and obvious once you actually notice the problem. I hope my examples helped you to understand it.

code

Every developer can build a feature. Not every developer can build a feature in a way so it’s easy to understand and reuse by someone else.

Like I wrote in the beginning: React is very flexible and you can structure your code however you want. Yet, I can assure you that if you follow the practices introduced in this article, you’ll thank yourself later, when your project (and the team) grows.

I hope you find this article is helpful - in case you have any questions - feel free to drop me a line on LinkedIn.

Table of Content

Share

We build success with U.S. based startups
Related articles:

14 disruptive U.S.-based startups to watch in 2021

When it comes to startups and innovative tech, the United States is way more than just Silicon Valley today. The country is full of tech…

Read more

6 Tips on How NOT to Build a Minimum Viable Product – for CEOs and Founders

1. Ego-driven scope: building a product that’s for you more than from you I know for a fact that software projects tend to be very personal…

Read more

CTO myths debunked: Interview with Masterborn’s CTO Przemysław Królik

When talking about companies like Uber, Facebook or Amazon, people tend to excessively focus on their founding fathers – the famed and rich…

Read more

We build valuable and successful products for U.S. based startups

Our offices are waiting for you in:
Wrocław, PL (HQ)

ul. Krupnicza 13,
50-075 Wrocław

Kielce, PL

ul. Gabrieli Zapolskiej 45B
25-435 Kielce

Austin, U.S.

Austin, TX
United States

Clutch.co
01234

4,9/5

Google Reviews
01234

5.0

Masterborn logo
    HomeAbout UsCareerBlog

Copyright © MasterBorn 2016-2021

The Administrator of your data is MasterBorn, with its registered office in Wroclaw, Krupnicza 13, Wroclaw. If you want to withdraw, get an insight or update information about you, then contact us: contact@masterborn.com