Published on by Leandro Ubilla

React Best Practices - Senior-Level Code with Single Level of Abstraction

Want to write clean React code that even the coding gods admire? In this post, we're breaking down Single Level of Abstraction (SLA)—a simple pattern for code you can be proud of.

Latest YouTube thumbnail.

What does this principle state?

SLA is a principle where each method or function handles only one level of abstraction. In simple terms, each piece of code should do one thing, and do it well.

When writing React components or functions, the idea is to keep logic clear and focused. This makes your code easier to read, test, and maintain.

Why SLA Matters

If you mix too many concerns in one function, it gets hard to understand what that function is doing. Here's how SLA helps:

  • Better readability: Smaller functions with one responsibility are easier to read.
  • Easier testing: Focusing on a single task makes writing tests simpler.
  • Faster debugging: If something goes wrong, it's easier to pinpoint the issue.

High-Level vs Low-Level Abstraction

High-Level Abstraction: This focuses on what you're trying to achieve—what the component or feature is meant to do.

Low-Level Abstraction: This focuses on how you accomplish that goal—getting into the details of how something works.

Ask Yourself: "What" and "How"

When you're designing your components, ask yourself two questions:

What am I trying to do?

This defines the high-level abstraction—what is the goal of this function or component?

How am I going to do it?

This is where you break it down into the low-level details—how will you accomplish that goal?

If you focus on the what at the top level and leave the how for lower levels, your code will stay clean, focused, and easier to maintain.

Let's jump into the code—this example will make the concept crystal clear.

Home page

Home page.

First, I want to show you the folder structure.

React Folder Structure.

I've added an Engagement Score component to app.jsx which calculates the engagement score of a fetched user.

engagement-score.tsx
import { useState, useEffect } from 'react';
 
type UserData = {
  timeSpent: number;
  clicks: number;
  interactions: Array<{ type: string }>;
}
 
const EngagementScore = () => {
  const [score, setScore] = useState<number>(0);
  const [userData, setUserData] = useState<UserData | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
 
  // Simulate an API call
  const fetchUserData = () => {
    setTimeout(() => {
      const data = {
        timeSpent: 450,
        clicks: 30,
        interactions: [
          { type: 'like' },
          { type: 'comment' },
          { type: 'share' },
          { type: 'view' }
        ]
      };
      setUserData(data);
      setLoading(false);
    }, 2000); // Simulating a 2-second delay for the API call
  };
 
  useEffect(() => {
    fetchUserData();
  }, []);
 
  // Function to calculate the engagement score 
  const calculateEngagementScore = (
    timeSpent: number, 
    clicks: number, 
    interactions: Array<{ type: string }>
  ) => {
    let baseScore = 0;
 
    baseScore += timeSpent > 300 ? timeSpent * 0.1 : timeSpent * 0.05;
    baseScore += clicks > 20 ? clicks * 1.5 : clicks * 0.5;
 
    interactions.forEach(interaction => {
      if (interaction.type === 'like') {
        baseScore += 5;
      } else if (interaction.type === 'comment') {
        baseScore += 10;
      } else if (interaction.type === 'share') {
        baseScore += 20;
      } else {
        baseScore += 1;
      }
    });
 
    const adjustedScore = baseScore > 100 ? baseScore * 1.1 : baseScore * 0.9;
    return adjustedScore.toFixed(2);
  };
 
  // Callback function to calculate the engagement score
  const handleCalculate = () => {
    if (userData) {
      const { timeSpent, clicks, interactions } = userData;
      const engagementScore = calculateEngagementScore(timeSpent, clicks, interactions);
      setScore(Number(engagementScore));
    }
  };
 
  if (loading) {
    return <div>Loading user data...</div>;
  }
 
  return (
    <div>
      <h1>User Engagement Score</h1>
      <button onClick={handleCalculate}>Calculate Score</button>
      <p>Score: {score}</p>
    </div>
  );
};
 
export default EngagementScore;
 

If you've ever looked at your code and thought, 'Wow, this is getting out of hand,' then you might be dealing with an SLA (Service Level Agreement) problem.

You know how some games have clean, intuitive menus… and others bury options under five layers of nonsense? That's what we're talking about—code that stays on one level vs. code that makes you dig through layers of logic.

Let's see if we can identify what is low-level and what is high-level abstraction.

I'm gonna start with this quick example

High-Level Abstraction:

// Display the user profile
function UserProfile() {
  return <Profile />;
}

Low-Level Abstraction:

// Fetch user data from the API and render it
function UserProfile() {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);
 
  return user ? <Profile user={user} /> : <Loading />;
}

1. The Component - High-Level (WHAT it does)

import { useState } from 'react';
 
const EngagementScore = () => {
  const [score, setScore] = useState<number>(0);
 
  return (
    <div>
      <h1>User Engagement Score</h1>
      <button onClick={handleCalculate}>Calculate Score</button>
      <p>Score: {score}</p>
    </div>
  );
};

Here's our EngagementScoreComponent.

This is our top-level function.

Its job? Render UI and trigger calculations.

The state holds the user's engagement score. No logic, no calculations—just data storage. Simple. Clean. High-level.

Data fetching simulation code (Low level - How we get the data for the component)

const [userData, setUserData] = useState<UserData | null>(null);
const [loading, setLoading] = useState<boolean>(true);
 
// Simulate an API call
const fetchUserData = () => {
  setTimeout(() => {
    const data = {
      timeSpent: 450,
      clicks: 30,
      interactions: [
        { type: 'like' },
        { type: 'comment' },
        { type: 'share' },
        { type: 'view' }
      ]
    };
    setUserData(data);
    setLoading(false);
  }, 2000); // Simulating a 2-second delay for the API call
};
 
useEffect(() => {
  fetchUserData();
}, []);

We have two states here, a fetchUser function, and a useEffect that simulates an API call to fetch user data. We're also using setTimeout to simulate an API call with a 2-second delay.

But, take a look at this. We have a lot of logic happening right in this component. This is what we call low-level abstraction. Ideally, we should be moving this logic into a custom hook.

Now, you could use something like TanStack React Query to manage data fetching more efficiently, but for this video, we're keeping it simple.

We can move this to /src/hooks/fetch-user-data.ts

The Calculation Function - Low-Level (HOW the score is made)

const calculateEngagementScore = (timeSpent, clicks, interactions) => { ... }

We can move this function to /src/utils

Handling User Input - High-Level (WHAT happens when you click)

const handleCalculate = () => { ... }

This just calls calculateEngagementScore, passes some fake data, and updates state. High-level again. It doesn't care how the score is made—just fires the function.

THE PROBLEM? SLA IS BROKEN

Right now, we're mixing high and low levels inside one component.

Imagine your tech lead is having a rough week. You definitely don't want them to see this kind of code. I've seen tech leads lose it over messy code, and trust me, that is not a place you want to be. You'll hear the sound of rage, and you'll never forget it.

Angry tech lead

Or what if you get an email from your killer web app saying there's a payment issue, and you're forced to dig through your own code—only to find yourself staring at lines you can barely understand.

THE FIX? SEPARATE THE LEVELS

Move low-level logic into its own utility function:

Let's start moving all the low level stuff outside of the component And import it in our component. Now, the component only deals with what happens, not how.

And this is the updated component

import { useState, useEffect } from 'react';
import { fetchUserData, calculateEngagementScore } from '../utils';
 
const EngagementScore = () => {
  const [score, setScore] = useState(0);
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetchUserData(setUserData, setLoading);
  }, []);
 
  const handleCalculate = () => {
    if (userData) {
      setScore(calculateEngagementScore(userData));
    }
  };
 
  if (loading) {
    return <div>Loading user data...</div>;
  }
 
  return (
    <div>
      <h1>User Engagement Score</h1>
      <button onClick={handleCalculate}>Calculate Score</button>
      <p>Score: {score}</p>
    </div>
  );
};
 
export default EngagementScore;
 

SRP & SLA: THE PERFECT COMBO FOR CLEAN CODE

engagement-score.tsx
import { useState, useEffect } from "react";
 
function Modal({ isOpen, onClose }) {
  const [fadeIn, setFadeIn] = useState(false);
 
  useEffect(() => {
    if (isOpen) {
      setFadeIn(true);
    } else {
      setTimeout(() => setFadeIn(false), 300); // Simulate fade-out delay
    }
  }, [isOpen]);
 
  useEffect(() => {
    const handleKeyDown = (event) => {
      if (event.key === "Escape") onClose();
    };
 
    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [onClose]);
 
  if (!isOpen && !fadeIn) return null;
 
  return (
    <div className={`modal ${fadeIn ? "fade-in" : "fade-out"}`}>
      <div className="modal-content">
        <p>Modal Content</p>
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

You've probably heard of SRP—it's all about making sure a function or component does just one thing. But both SRP and SLA are key to writing clean, maintainable code.

While SRP focuses on responsibility, SLA keeps things consistent at the same level of abstraction. Let's dive into how they work together.

SLA (Single Level of Abstraction) is a more specific application of SRP in the context of abstraction levels.

Here's the difference:

SRP is about ensuring that a class or function does only one thing, and therefore can be modified for one reason only. It's about keeping responsibilities clear and concise.

SLA takes that idea further by ensuring that the code at any level of abstraction stays focused on a single level. For example, in a component, you wouldn't mix the UI rendering (the what) with the data-fetching logic (the how).

In practical terms:

SRP tells you, “Keep your functions and components focused on one task.”

SLA tells you, “Don't mix levels of abstraction in the same function or component. Keep the high-level stuff (like rendering the UI) separate from the low-level stuff (like handling calculations).”

WHEN SLA CAN MAKE YOUR CODE WORSE

This is a modal component with a fade effect and keyboard support.

  • When isOpen is true, the modal appears and triggers a fade-in effect.
  • When isOpen is false, it waits 300ms before fully disappearing—mimicking a fade-out.
  • It also listens for the Escape key, so users can close it without clicking.
  • If the modal is closed and fully faded out, it doesn't even render—saving performance

Why This Doesn't Need SLA:

  • ✅ The logic is UI-specific. There's no real business logic to extract.
  • ✅ State and effects are directly tied to rendering. Extracting them would fragment the component.
  • ✅ Keeping logic inside keeps it readable. Extracting the fade state or keyboard listener into hooks would add extra files without much benefit.