In my role as a Teaching Assistant, I often assist different groups of students with recurring problems. This allows me to consider these problems from various perspectives and understand the implications of different approaches and implementations, all without writing a single line of code.
Last week provided an example of this. The Angular batch I was assigned to was working on a week-long project to create a LinkedIn clone. I had previously assisted with this project several times using React, but this was my first experience with Angular.
One of the most common problems students present to me is something like, "When I update the profile picture, it gets updated on the profile page but not on the navbar."
This issue often results from having separate pieces of data and HTTP calls for something that should be retrieved only once.
This, in short, is the single source of truth (SSOT) concept. If your data comes from different sources, you can't be sure it's updated.
What is the single source of truth (SSOT) concept?
The SSOT concept, in software development, states that pieces of data should be stored in only one location within a system, ensuring there is one designated and authoritative storage for that information that is always up-to-date.
This ensures consistency and prevents data from being duplicated, reducing the risk of conflicts and discrepancies in the UI.
You can imagine that these results simplify development and maintenance significantly.
In the context of web development and, more specifically, frontend frameworks like React and Angular, SSOT is applied by having a central "state" that manages and distributes the data to the components that need it.
Let's find ourselves a "case study" to use for the rest of this article.
For simplicity, we will call the API endpoint for the logged account /me
.
Let's keep this layout in mind (please note that this does not reflect how Tumblr works at all!):
The logged account data, which here is 'thegreenunistudent,' would be needed in the top component to show the icon, in both the sidebars, and in the notification popover.
Take a moment to think about how you would set up your requests.
Let's first examine a structure that would NOT adhere to SSOT.
// Folder structure:
├── /pages
│ ├── HomeComponent <--- renders the main part of the page
├── /layout
│ ├── sidebars
│ │ ├── RightSidebarComponent <--- GET /me
│ │ ├── LeftSidebarComponent <--- GET /me
├── /components
│ ├── PopoverComponent <--- GET /me
│ ├── CreatePostComponent <--- GET /me
const GeneralOuterComponent = () => {
return <>
<LeftSidebarComponent/>
<main>
<CreatePostComponent/>
<HomeComponent/>
</main>
<RightSidebarComponent/>
</>
}
This would result in four different calls each time the page renders. Additionally, if we issued a PUT /me
request, we would need to make four more GET requests.
How would this impact the UX of Tumblr?
First of all, performance. Keep in mind that all the other components also have to make other fetches. Most likely, there will be a fetch for the posts, a fetch for all the blogs one might have, a fetch for recent activity (the graph you see on the right side of the page), and at least three open socket connections: one for notifications, one for messages, and one for new posts.
All of these, along with more that we don't know or haven't accounted for, could easily exceed 10 or even 15 if we consider the unnecessary /me
calls. On slower computers or connections, this could result in slower load times, which is not ideal for both the user experience and SEO purposes (if you're into that kind of stuff š).
Another problem with this approach is consistency. Let's assume that, when going to the profile edit page, the sidebars remain rendered the whole time because the routing only affects the element of the last snippet. So, the user goes to the profile edit, changes their blog name, and then returns to the homepage.
Now, the homepage makes another /me
call, but the sidebars don't. As a result, the CreatePostComponent is going to have the updated name, but the sidebars won't. Assuming you are a good developer who uses their eyes and not just their instincts, you would notice this right away and find a way to fix it.
To address this, you would need to implement a state lifting process after the PUT /me
request that goes to the parent and then to the various children, resulting in an additional 4 /me
calls. This approach is not only inconvenient but also error-prone and repetitive.
The SSOT concept in React
In React, we usually achieve this through one or more of these practices.
State Management Libraries (e.g., Redux):
State management libraries like Redux simplify the implementation of SSOT by creating a global state that holds all the data your application needs.
Using Redux has its pros and cons. Once it's set up, Redux is very convenient, but it's not a "plug and play" library. It involves several new concepts (reducers, slices, hooks) and a setup process, which may or may not be lengthy. Additional time may be needed if you're using RTK with TypeScript.
If your app is already in development, chances are you might want to refactor your entire app, and that takes time, resources, and possibly a learning process for some of the developers.
However, this is one of the best solutions for implementing SSOT in React. If done correctly, it eliminates code duplication and centralizes all states so that all parts of the UI are updated correctly.
Context API:
Similar to Redux, although not as powerful, React's built-in Context API allows you to create a global context that can be accessed by nested components. This is useful for sharing state across components without resorting to other, less clean practices like prop drilling and state lifting. It's often used for smaller things, like dark mode or language selection when setting up Redux may be overkill.
Using the Context API can be perfect for smaller features, but as the feature scales, it may start to pose problems. It's important to know that the Context API re-renders all components in which it's used every time the data changes. This means the state gets reset as well. So, if you're using the Context API to show dismissible error alerts, whenever an error gets added or removed from the list, all the components where you're using the Context (most of them, because you're going to need to be able to dispatch an error from pretty much everywhere) will cause your state to reset, potentially affecting form content, inputs, and selected options.
Component Hierarchy and Presentational Components:
The architecture you choose for your components also plays a major role in implementing SSOT. By thinking ahead, you can design your component hierarchy using 'container' or 'presentational' components. These components are responsible for fetching and distributing data among the components that need it.
In other words, a presentational (or container) component wraps all the sub-components needed for a
certain app view and takes care of the HTTP request and data distribution using props.
Here is an example of how our example structure could be improved using presentation components:
// Folder structure:
├── /pages
│ ├── HomeComponent <--- GET /me
├── /layout
│ ├── sidebars
│ │ ├── RightSidebarComponent <--- takes account data as a prop
│ │ ├── LeftSidebarComponent <--- takes account data as a prop
├── /components
│ ├── PopoverComponent <--- takes account data as a prop
│ ├── CreatePostComponent <--- takes account data as a prop
// HomeComponent
const HomeComponent = () => {
const [me, setMe] = useState(null)
// GET /me logic
return <>
<LeftSidebarComponent me={me}/>
<main>
<CreatePostComponent me={me}/>
</main>
<RightSidebarComponent me={me}/>
</>
}
Now, you can see that the number of calls needed to populate the /me
data has been reduced from 4 to 1.
As for many of the things we are discussing in this article, this approach has its own flaws. From a routing perspective, this means the sidebars will get re-rendered more times than they should. Just like anything in programming, the approach you choose depends on the situation.
State Lifting and Prop Drilling:
This is by far the most disliked thing by all React developers. State lifting can be challenging at all levels of coding. If you're a beginner, it might be hard to understand. As you grow as a developer, you might grasp state lifting, but it can still be a pain to debug. You have to follow the data around, open 12 files in your editor, console.log every step, and it can get really complicated.
However, it is a solution if none of the above options is a good fit. If Redux is not available or not worth it, and the Context API is causing more harm than good, you might consider state lifting your way to the top component.
Nevertheless, if you can avoid it, it's better to do so.
How to choose the correct solution for you
Choosing the right way to implement SSOT can be tricky. It depends on how far along you are in the development process, whether you have the time and resources to make significant changes, and if you have the decision-making authority.
In general, as a freelancer, I usually go the Redux route for anything that isn't related to dark mode, which I typically handle with Context.
However, if I already have Redux set up, I don't bother adding Context and manage dark mode through Redux as well.
In any case, you should always consider the SSOT concept while programming your web apps. It might not lead to a revolution in your codebase, but it will certainly improve your code, make it cleaner, and enhance your page's speed.
Happy coding!