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
Feb 21, 2021 | 12 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.
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);45 useEffect(() => {6 const fetchProduct = async () => {7 const res = await fetch(`https://api.example.com/products/${productId}`);8 setProduct(res.json());9 };10 fetchProduct();11 }, []);1213 const onSubmit = () => {14 // ...15 };1617 if (!product) return null;1819 return (20 <Container>21 <Name>{product.name}</Name>22 <Price>{product.discountPrice || product.price}</Price>2324 {25 product.discountPrice26 && product.price - product.discountPrice > 10027 && <SuperSaleBadge />28 }2930 <Description>{product.description}</Description>31 <VariantSwitch32 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
1<Button />
1onSubmit
I’ve seen this kind of problem many times. It could also be called
1onClick
1onClickHandler
You could ask, why is it ok to be a name of the
1<Button />
See, the
1<Button />
1onClick
On the other hand, we have the context where we actually use the
1<Button />
1onSubmit
1foo
That
1onSubmit
1addToCart
Choosing names for handlers
Second, names of
1<VariantSwitch />
1value
1onClick
Compared to the
1<Button />
1<VariantSwitch />
Let’s start with the
1value
1<VariantSwitch />
1value
1selectedVariantId
Now, when it comes to the
1onClick
1<Button />
1<VariantSwitch />
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
1onScroll
Instead, that prop could be named e.g.
1onVariantSelection
1<VariantSwitch />
1const ProductPage = ({ productId }) => {2 const [product, setProduct] = useState(null);3 const [selectedVariantId, setSelectedVariantId] = useState(0);45 useEffect(() => {6 const fetchProduct = async () => {7 const res = await fetch(`https://api.example.com/products/${productId}`);8 setProduct(res.json());9 };10 fetchProduct();11 }, []);1213 const addToCart = () => {14 // ...15 };1617 if (!product) return null;1819 return (20 <Container>21 <Name>{product.name}</Name>22 <Price>{product.discountPrice || product.price}</Price>2324 {25 product.discountPrice26 && product.price - product.discountPrice > 10027 && <SuperSaleBadge />28 }2930 <Description>{product.description}</Description>31 <VariantSwitch32 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.
First architecture improvements
We should move it out of our
1<ProductPage />
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);34 const fetchProduct = async () => {5 const res = await fetch(`https://api.example.com/products/${productId}`);6 setProduct(res.json());7 };89 useEffect(() => {10 fetchProduct();11 }, [productId]);1213 return product;14};1516const ProductPage = ({ productId }) => {17 const [selectedVariantId, setSelectedVariantId] = useState(0);18 const product = useSingleProduct(productId);1920 const addToCart = () => {21 // ...22 };2324 if (!product) return null;2526 return (27 <Container>28 <Name>{product.name}</Name>29 <Price>{product.discountPrice || product.price}</Price>3031 {32 product.discountPrice33 && product.price - product.discountPrice > 10034 && <SuperSaleBadge />35 }3637 <Description>{product.description}</Description>38 <VariantSwitch39 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
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
1isOnSuperSale
1const useSingleProduct = (productId) => {2 const [product, setProduct] = useState(null);34 const fetchProduct = async () => {5 const res = await fetch(`https://api.example.com/products/${productId}`);6 setProduct(res.json());7 };89 useEffect(() => {10 fetchProduct();11 }, [productId]);1213 const currentPrice = useMemo(() => product?.discountPrice || product?.price, [product]);1415 const isOnSuperSale = useMemo(() => product?.discountPrice && product?.price - product.discountPrice > 100, [product]);1617 return {18 product,19 currentPrice,20 isOnSuperSale,21 };22};2324const ProductPage = ({ productId }) => {25 const [selectedVariantId, setSelectedVariantId] = useState(0);26 const {27 product,28 currentPrice,29 isOnSuperSale,30 } = useSingleProduct(productId);3132 const addToCart = () => {33 // ...34 };3536 if (!product) return null;3738 return (39 <Container>40 <Name>{product.name}</Name>41 <Price>{currentPrice}</Price>4243 {isOnSuperSale && <SuperSaleBadge />}4445 <Description>{product.description}</Description>46 <VariantSwitch47 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
1useCart
1const useSingleProduct = (productId) => {2 const [product, setProduct] = useState(null);34 const fetchProduct = async () => {5 const res = await fetch(`https://api.example.com/products/${productId}`);6 setProduct(res.json());7 };89 useEffect(() => {10 fetchProduct();11 }, [productId]);1213 const currentPrice = useMemo(() => product?.discountPrice || product?.price, [product]);1415 const isOnSuperSale = useMemo(() => product?.discountPrice && product?.price - product.discountPrice > 100, [product]);1617 return {18 product,19 currentPrice,20 isOnSuperSale,21 };22};2324const ProductPage = ({ productId }) => {25 const [selectedVariantId, setSelectedVariantId] = useState(0);26 const {27 product,28 currentPrice,29 isOnSuperSale,30 } = useSingleProduct(productId);3132 const { add: addToCart } = useCart();3334 if (!product) return null;3536 return (37 <Container>38 <Name>{product.name}</Name>39 <Price>{currentPrice}</Price>4041 {isOnSuperSale && <SuperSaleBadge />}4243 <Description>{product.description}</Description>44 <VariantSwitch45 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.
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
World-class React & Node.js experts
Related articles:
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?
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.
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!