State: Single Source of Truth

2 mins |

September 17, 2021

It is always advised to have a single source of truth for your application. Before we start, let’s see what exactly the state of the application is.

State is part of the application that holds a source of data. It could be your database like Table, Document or React states like useState or useReducer.

Single Source of Truth

Any time you query for data, it should be fulfilled by a unique set of State. For eg. If you ask for List of all users who have commented on a post, you should get a list of users from set tables such that this information is not replicated in any other data.

1select *
2from users u
3inner join post_comments pc on pc.user_id = u.user_id
4where pc.post_id = 1

Here, we are querying from two tables, users and post_comments. This is fine, as none of the information is repeated;

Examples where you might not have a single source of truth

There might be a lot of cases where we might not be having a single source of truth. It could be an aggregate table, a cache, a data copied from source props, or requesting data from an AJAX request. In all of these instances, we are copying data from one source to another. There are many other instances where you might be doing the same.

But if you had created an aggregate table, users_comments, which has all the comments of a given user, then we will have two sources of data for the same query.

1/* Query 1 */
2select *
3from users u
4inner join post_comments pc on pc.user_id = u.user_id
5where pc.post_id = 1
6
7/* Query 2 */
8select *
9from users_comments
10where post_id = 1

Essentially both Query 1 and Query 2 are returning the same data.

Another example

1function NumberList() {
2 const [numbers, setNumbers] = useState()
3 return <Child state={state} />
4}
5
6function EvenNumbers({ numbers }) {
7 const [evenNumbers, setEvenNumbers] = useState()
8 useEffect(() => {
9 setEvenNumbers(numbers.filter(filterEvenNumbers))
10 }, [numbers])
11}

Let’s say we have a list of numbers, and we want to filter out the numbers that are even in the EvenNumbers component.

Here, evenNumbers copied from numbers, But there is a possibility that these sources can diverge in the future. Maybe a bug in code, you come infrastructure outage caused write in one of the tables failed.

There is nothing that ensures numbers and evenNumbers hold the same source of truth. You might be thinking useEffect ensures that. But it is not. It is creating a copy of data, there is no check to make sure that useEffect is not removed in the future.

For eg.

When numbers was null our code broke, so add a check to make sure that if it is null then copy doesn’t happen.

1function NumberList() {
2 const [numbers, setNumbers] = useState()
3 return <Child state={state} />
4}
5
6function EvenNumbers({ numbers }) {
7 const [evenNumbers, setEvenNumbers] = useState()
8 useEffect(() => {
9+ if(!state) return
10 setEvenNumbers(numbers.filter(filterEvenNumbers))
11 }, [numbers])
12}

But this introduces a new problem. In this example, lets say numbers is [2] and we sync it to evenNumbers so it will be [2]. Now, let us update the numbers to null. Now because of the check we introduced in useEffect, copyState will be [2] and state will be null. Our state is not in sync.

Syncing state

Whenever you have an observer listening to data and it updating another source of data. Then it is called syncing data. For eg. in our above react example, we are syncing using useEffect. In caching it could be our cache invalidation logic, in the case of aggregate table, it could the triggers.

By definition, we have a separate function to sync data. This implies that there is a possibility that our sync logic might not run. It could be because some developers decided to add some logic to sync data.

If possible we should be deriving data from the source instead of copying data. For eg.

1function NumberList() {
2 const [numbers, setNumbers] = useState()
3 return <Child state={state} />
4}
5
6function EvenNumbers({ numbers }) {
7- const [evenNumbers, setEvenNumbers] = useState()
8+ const evenNumbers = numbers.filter(filterEvenNumbers);
9- useEffect(() => {
10- if(!state) return
11- setEvenNumbers(numbers.filter(filterEvenNumbers))
12- }, [numbers])
13}

Avoid copying state

  • Avoid copying state, instead derive state from the source. Copying introduce a latent bug.
  • If you have to copy state, make sure that you have additional checks to make sure sync is happening.

Got a Question? Bala might have the answer. Get them answered on #AskBala

Edit on Github

Subscribe Now! Letters from Bala

Subscribe to my newsletter to receive letters about some interesting patterns and views in programming, frontend, Javascript, React, testing and many more. Be the first one to know when I publish a blog.

No spam, just some good stuff! Unsubscribe at any time

Written by Balavishnu V J. Follow him on Twitter to know what he is working on. Also, his opinions, thoughts and solutions in Web Dev.