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ętek

Rafał Świętek

Feb 21, 2021 | 12 min read

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

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

1onClick
prop of the
1<Button />
component -
1onSubmit
.

I’ve seen this kind of problem many times. It could also be called

1onClick
(just like the prop name) or sometimes
1onClickHandler
. 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

1<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

1<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
1onClick
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

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

That

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

Choosing names for handlers

Second, names of

1<VariantSwitch />
component’s props -
1value
and
1onClick
.

Compared to the

1<Button />
component,
1<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

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

Now, when it comes to the

1onClick
prop. We just said that it’s an ok prop name for the
1<Button />
component. Why isn’t it ok for the
1<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

1onClick
prop name to
1onScroll
and refactor every component where we use that switch?

Instead, that prop could be named e.g.

1onVariantSelection
. This way we leave all the implementation details to the
1<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.

Stay updated

Get informed about the most interesting MasterBorn news.

Check our Policy to know how we process your personal data.

First architecture improvements

We should move it out of our

1<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

1useSingleProduct
:

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

1useSingleProduct
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.

1shouldDisplaySuperSaleBadge
, 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
1isOnSuperSale
. 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

1addToCart
function - it definitely should go to the
1useCart
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.

Related articles:
/blog/React-and-Redux-examples-of-web-app-development/

React and Redux - 10 examples of successful Web App Development

Almost half of React apps use Redux today. The question is - why is Redux gaining so much steam? And, is React becoming more popular thanks to Redux?

/blog/Jak_nie_budowac_mvp-6-porad/

Jak NIE budować MVP - 6 porad dla CEO i Founderów

W MasterBorn ulepszanie procesu tworzenia oprogramowania stało się naszą firmową “obsesją”. W przypadku większości firm i zespołów proces ten zaczyna się od utworzenia i zdefiniowania MVP. W tym artykule chciałbym się podzielić spostrzeżeniami i najlepszymi praktykami, których nauczyliśmy się tworząc MVP dla naszych amerykańskich Klientów.

/blog/Why_use_nodejs_9_examples_of_node_apps/

Why use Node.js? 9 examples of successful Node.js apps'

Why use Node.js? Let the examples of Node.js apps speak for themselves: discover Uber, Figma, SpaceX, Slack, Netflix and more!

We build valuable, JavaScript products for U.S.-based companies

  • Nashville, U.S.

    2713 Wortham Ave.
    Nashville, TN 37215, USA

  • Wrocław, PL

    ul. Krupnicza 13, 50-075 Wrocław

  • Szczecin, PL

    ul. Wielka Odrzańska 26, 70-535 Szczecin

  • Kielce, PL

    ul. Gabrieli Zapolskiej 45B, 25-435 Kielce


Copyright © MasterBorn® 2016-2024

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