Building Cleaner UI with Separation of Concerns

Sangil Yun
7 min readMar 11, 2024

Photo by Workperch on Unsplash

Modern frontend libraries like React and Vue offered a smooth entry point for my UI development journey. I got comfortable with basics like componentizing UI, handling external APIs, managing UI states, etc. Well, at least I thought I did. Then, I started to work on bigger and more complex projects and things started to get out of hand quickly. A component that was easy to reason about in the beginning received updates as project grew and more code was added. Now this component that used to do one thing is doing multiple things and eventually became a giant mega component that does everything. Adding simple updates to a component like this started to take longer because updating something that looked completely unrelated broke something else. Whenever I had to touch the component, I was always worried about breaking something else and making sure that everything still worked took longer than writing the actual code. This fear naturally led me to look into better ways to write UI code and UI testing. I started to read/watch materials from subject matter experts like Dan Abramov, Kent C. Dodds, Lachlan Miller, Michael Thiessen to name a few.

There are many things I learned along the way but one of the biggest takeaways and recurring ideas that helped me a lot was this concept Separation of concerns. When a component is growing too big, identifying different concerns and decoupling them can be a good way to make the code more maintainable and testable.
The concept itself is fairly well known and each authors call it differently, but learning about it and seeing how it’s applied in the code helped me make better decisions.

Take this Vue.js password complexity checker code snippet for example:

<template>
<div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" />
</div>

<div>Password complexity: {{ passwordComplexity }}</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

const password = ref('')

const passwordComplexity = computed(() => {
if (password.value.length < 6) return 'Too short'
if (!/[0-9]/.test(password.value)) return 'Should contain a number'
if (!/[a-z]/.test(password.value)) return 'Should contain a lowercase letter'
if (!/[A-Z]/.test(password.value)) return 'Should contain an uppercase letter'
return 'Strong'
})

</script>

This is a simple password complexity checker where it takes password input and provides feedback on how strong the password is. The component checks if the password is long enough(at least 6 characters) and includes a number, lowercase letter, and uppercase letter.

Now, let’s add a new feature here — password score. This score tells you how many password criteria the password meets. Since passwordComplexity computed value is already checking the criteria, we can simply add a new ref and update it there.

<template>
<div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" />
</div>

<div>Password complexity: {{ passwordComplexity }}</div>
<div>Password score: {{ score }}</div> <!-- new -->
</div>
</template>
<script setup lang="ts">
// ...
const password = ref('')
const score = ref(0) // new

const passwordComplexity = computed(() => {
if (password.value.length < 6) {
score.value = 0 // new
return 'Too short'
}
if (!/[0-9]/.test(password.value)) {
score.value = 1 // new
return 'Should contain a number'
}
if (!/[a-z]/.test(password.value)) {
score.value = 2 // new
return 'Should contain a lowercase letter'
}
if (!/[A-Z]/.test(password.value)) {
score.value = 3 // new
return 'Should contain an uppercase letter'
}
score.value = 4 // new
return 'Strong'
})
</script>

With that, our component now can provide feedback on both password complexity and score! If the password is too short, Users will see the error message “Too short” and complexity score “0”.

While this component might work fine and get the job done, let’s pause for a moment and see what’s going on. With the new code added, now we have introduced a _side effect_ to this computed property. passwordComplexity not only calculates the password complexity, it also updates the score. In this small example, it might look insignificant but having side effects like this can be problematic for a few reasons.

When we look at a UI component and try to understand it, we look at how the state is being updated. However, since the state update is mixed in a computed property, it’s not very clear to see where the update is coming from. Also, having side effects like this can make refactoring difficult. If we want to refactor this component later and for whatever reason, we want to move a piece of logic to a different place, we have to take every place it’s being updated into consideration.

Let’s look at the passwordComplexity again and see if we can identify different concerns here to better organize the code.

<template>
<!--...-->
</template>

<script setup lang="ts">
// ...
const password = ref('')
const score = ref(0)

const passwordComplexity = computed(() => {
if (password.value.length < 6) { // business logic
score.value = 0 // UI update
return 'Too short' // UI update
}
if (!/[0-9]/.test(password.value)) { // business logic
score.value = 1 // UI update
return 'Should contain a number' // UI update
}
if (!/[a-z]/.test(password.value)) { // business logic
score.value = 2 // UI update
return 'Should contain a lowercase letter' // UI update
}
if (!/[A-Z]/.test(password.value)) { // business logic
score.value = 3 // UI update
return 'Should contain an uppercase letter' // UI update
}
score.value = 4 // UI update
return 'Strong' // UI update
})
</script>

As you can see in the code above, we’ve identified which lines are related to business logic and which lines are related to UI updates. The business logic is the password complexity checks, and the UI updates are the score updates and error messages.

Having identified them, let’s see if we can decouple them. The business logic of password validation shouldn’t be directly tied to UI updates. Ideally, it should receive a password as input and return a simple true/false value indicating whether the password meets the criteria. This separation allows us to structure the code more effectively, as we’ll see in the following code snippet.

const complexityChecker = (password) => {
const hasNumber = /[0-9]/.test(password)
const hasLowerCase = /[a-z]/.test(password)
const hasUpperCase = /[A-Z]/.test(password)
const isLongEnough = password.length >= 6
return {
hasNumber,
hasLowerCase,
hasUpperCase,
isLongEnough,
}
}

We have extracted the business logic into a pure function. Now let’s use this function in our component.

<template>
<!--...-->
</template>
<script>
// ...
const passwordComplexity = computed(() => {
const {
hasLowerCase,
hasNumber,
hasUpperCase,
isLongEnough,
} = complexityChecker(password.value)

if (!isLongEnough) {
return 'Too short'
}
if (!hasNumber) {
return 'Should contain a number'
}
if (!hasLowerCase) {
return 'Should contain a lowercase letter'
}
if (!hasUpperCase) {
return 'Should contain an uppercase letter'
}
return 'Strong'
})
//...
</script>

How about the score? We have already extracted the business logic, so we can use the same function to calculate the score.

<template>
<div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" />
</div>

<div>Password complexity: {{ passwordComplexity }}</div>
<div>Password score: {{ getPasswordScore(password) }}</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

const password = ref('')

const getPasswordScore = (password: string) => {
const {
hasNumber,
hasLowerCase,
hasUpperCase,
isLongEnough,
} = complexityChecker(password)

return [hasNumber, hasLowerCase, hasUpperCase, isLongEnough].filter(Boolean).length
}

const complexityChecker = (password: string) => {
const hasNumber = /[0-9]/.test(password)
const hasLowerCase = /[a-z]/.test(password)
const hasUpperCase = /[A-Z]/.test(password)
const isLongEnough = password.length >= 6
return {
hasNumber,
hasLowerCase,
hasUpperCase,
isLongEnough,
}
}
const passwordComplexity = computed(() => {
const {
hasLowerCase,
hasNumber,
hasUpperCase,
isLongEnough,
} = complexityChecker(password.value)

if (!isLongEnough) {
return 'Too short'
}
if (!hasNumber) {
return 'Should contain a number'
}
if (!hasLowerCase) {
return 'Should contain a lowercase letter'
}
if (!hasUpperCase) {
return 'Should contain an uppercase letter'
}
return 'Strong'
})
</script>

<style scoped>
</style>

There are more improvements we could’ve made but for the demonstration purpose, I think this is good enough. What are the advantages of structuring the code this way? It offers several advantages:
With the separation, you can clearly see where the business logic ends and the UI update begins. This improves code readability and maintainability. In addition, It’s easy to test the business logic in isolation now that they are just a pure function. Also, if you need to update the UI in the future, you can do so without worrying about breaking the business logic.

Conclusion

As our projects grow, so too does the complexity of our code. While initial approaches might seem sufficient, they can quickly become difficult to maintain. Separation of concerns offers insight for structuring code in a way that promotes readability, maintainability, and testability. By identifying distinct concerns within a component and decoupling them, we can ensure that changes to one area don’t unintentionally break another. This not only saves us time and frustration but also allows us to write cleaner and more robust code in the long run.

We have explored the concept of separation of concerns through the lens of a simple password complexity checker. However, this concept is equally applicable to a wide range of UI components. By embracing separation of concerns, we can improve ourselves to build more maintainable applications.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response