Why You Should Not Be a Reactive Developer
In This Article
- What Is a Reactive Developer
- Case Scenario
- The Reactive Developer's Solution
- How To Solve It Properly
- The Impact of Reactive Programming
- An Extra Note About Writing Tests
- Summary
What Is a Reactive Developer?
Let's start with defining what I mean when I say "reactive developer". I don't refer to the programming paradigm reactive programming. Neither do I mean a developer who writes React code.
What I really am talking about when I use the term reactive developer, is a trait some developers have when they write code. The way they behave when writing code.
Reactive developers write code as a direct response to the problem they are facing for the moment. This means, if they find a bug, they will solve it hastily without considering alternative solutions. Phrased differently, they will implement the first solution that comes to their mind without thinking of the consequences it implies.
The opposite to a reactive developer, would be a proactive developer, a developer that plans ahead to find the best possible solution. Let's look at an example scenario to see the difference in how a reactive developer solves a problem compared to a proactive developer.
Case Scenario
Imagine you have an application where you can receive messages, and messages are marked as read when you read them. Then, when a user has read all its messages, a modal should display.
Imagine that functionality is already implemented with the code below. Unfortunately, it doesn't work, the modal doesn't open when the latest message is read. Maybe you can spot why?
// The passed in messages has a property named isRead which is
// true if the message has been read.
const MyReactComponent = ({ messages }) => {
const [modalOpen, setModalOpen] = useState(false)
const [unreadMessages, setUnreadMessages] = useState(messages)
useEffect(() => {
// If there are no unread messages, open the modal.
if (unreadMessages.length === 0) {
setModalOpen()
}
// Update unreadMessages to only contain unread messages.
setUnreadMessages(messages.filter(msg => !msg.isRead))
}, [messages])
return <Modal open={modalOpen} />
}
I don't know how experienced React developer you are, but the reason the modal isn't opened is because the useEffect has a missing dependency, unreadMessages. It's an honest mistake, developers do such things all the time.
With that said, the person who wrote this code is probably a rather unexperienced or hasty developer, a novice or reactive developer. Especially since this issue could have been caught if the developer used ESLint rules for React hooks, react-hooks/exhaustive-deps, and took notice of the warning it would have inferred about the missing dependency to the useEffect.
ESLint could have warned us with a "React Hook useEffect has a missing dependency" notice
The Reactive Developer's Solution
Time to solve the bug in the code above. As mentioned, the issue was that the unreadMessages was missing in the useEffect's dependency list. Our imagined reactive developer figures out that quickly. Just as quickly as he detects the problem, he also comes up with a solution. Quickly he adds the missing dependency and checks the browser result.
useEffect(() => {
if (unreadMessages.length === 0) {
setModalOpen()
}
setUnreadMessages(messages.filter(msg => !msg.isRead))
// The reactive developer added the unreadMessages dependency.
}, [messages, unreadMessages])
}
Hmm. Something isn't working... Is Babel failing? Why isn't the application responding? No, that's not it. The browser is stuck in an infinite loop!
When the developer added the missing unreadMessages dependency, the useEffect caused an infinte loop because unreadMessages updates within the effect, which in turn retriggers the effect to run once again. Luckily, this new bug couldn't go by undetected.
The reactive developer is inventive. He instantly figures out another solution. What if we only would invoke setUnreadMessages if the number of unread messages actually has changed? That way, we could bypass the infinte loop. In seconds he has updated the code to look as below.
const MyReactComponent = ({ messages }) => {
const [modalOpen, setModalOpen] = useState(false)
const [unreadMessages, setUnreadMessages] = useState(messages)
useEffect(() => {
if (unreadMessages.length === 0) {
setModalOpen()
}
const unreadMsgs = messages.filter(msg => !msg.isRead)
// If the number of unread messages is the same as last time
// the effect ran, do not update the state. This way we can
// avoid the infinite loop, because we only update
// unreadMessages if we have read some new messages.
if (unreadMessages?.length !== unreadMsgs.length) {
setUnreadMessages(unread)
}
}, [messages, unreadMessages])
return <Modal open={modalOpen} />
}
Does the code work well now? Honestly, I haven't even tested it. What I can tell already is that it isn't a good solution. Let's look at a better one.
How To Solve It Properly
The initial code was bad to begin with, and when we found out it caused an error, we shouldn't just have solved the bug, we should have reconsidered if there was a better way to implement the same functionality. The current code was obviously flaky already. Any other quick fix would either cause a new bug or contribute to more complex code.
Here's what could have been done.
const MyReactComponent = ({ messages }) => {
const unreadMessages = messages.filter(msg => !msg.isRead)
return <Modal open={unreadMessages.length === 0} />
}
Yeah. That's it. The code does exactly what we wanted it to do. Concise, readable, and no need for the three hooks and two if statements that all could cause some kind of bugs. We could even have turned the complete component into a one-liner, but that would reduce the readability of the code.
Replacing the original solution with this new solution is just as quick as patching the old solution. But instead of increasing complexity, readability and the risk for bugs, we instead made the code more readable and less error-prone.
The Impact of Reactive Programming
So, let's rethink what we just experienced and what kind of effects reactive programming results in.
- The reactive solution ended up with 3-4 times as much code. We definitely don't want our whole code base to grow with a factor 3 or 4. Apart from having to maintain the code base, redundant JavaScript code is also one of the most common causes for web applications to load slowly.
- The reactive solution gave birth to a new bug. In this case, it was easy to detect the new bug. We won't always be that lucky.
- The reactive developer's code takes a lot more time to read and understand.
- The reactive developer's React component will render more times. Doing this for one component isn't an issue, but in large-scale projects, or when dealing with big amount of data, it can make the application terribly slow.
- Since the reactive code can be refactored in a better way, all unnecessary code adds up to the project's technical debt.
- Developers copy a lot of code. There's a big chance (read risk) that someone copies the code the reactive developer wrote, leading to all the same mistakes in another part of the application.
- Unexperienced developers would spend a lot of time testing the longer reactive component. Not only because it's harder to test, but also because it looks harder. Many developers try to test internal logic, such as what value a useState has. Please note that you should never do that. Consider each React component as a unit (black box). You have some input and expect an output. What values a useState has is completely irrelevant.
I'm sure I missed a bunch of bullet points I could add. But I think you get the point, there's a lot of reasons not to be reactive and go for the fastest possible solution. Try to be proactive, to think of better solutions. It will save you a tremendous amount of time, not only long term, in most cases even short term.
An Extra Note About Writing Tests
When working at larger companies, testing is very essential, and it takes time, a lot of time. I would therefore like to add an additional note about testing the components written by the reactive and proactive developers.
Testing the proper proactive solution is a piece of case, one test to check that the modal is open when we have read all messages, and another one to check that it's closed when we have messages which are unread. That will test all scenarios that can occur and all branches of code (possible if-statement branching). We do not need to test that the code works when the unread count changes, because the code does not have an internal state or a useEffect.
// When the code doesn't include a state or an effect, there's
// no need to update the message prop when testing the component.
const MyReactComponent = ({ messages }) => {
const unreadMessages = messages.filter(msg => !msg.isRead)
return <Modal open={unreadMessages.length === 0} />
}
The longer solution written by the reactive developer is more troublesome to test. We could of course use the same two test cases, one for the open state and one for the closed state. Doing that would not test all scenarios that can happen when running the code though, and neither would it cover all code branches. It wouldn't even catch the bug on topic, where the modal doesn't open when the read count changes.
To test the reactive component fully, we would have to trigger a rerender and pass in a new messages prop to test it thoroughly. That is required because we have a useEffect that updates an internal state in the component.
// To test this component fully, we need to update messages prop
// within a test to see what happens when the useEffect triggers.
// If we don't do that, we cannot know what will happen when we
// get new messages passed in from a parent component.
const MyReactComponent = ({ messages }) => {
const [modalOpen, setModalOpen] = useState(false)
const [unreadMessages, setUnreadMessages] = useState(messages)
useEffect(() => {
if (unreadMessages.length === 0) {
setModalOpen()
}
const unreadMsgs = messages.filter(msg => !msg.isRead)
if (unreadMessages?.length !== unreadMsgs.length) {
setUnreadMessages(unread)
}
}, [messages, unreadMessages])
return <Modal open={modalOpen} />
}
Testing the above component is not very hard, but it can be. If we would test it using Enzyme and trying to do a shallow rendering, we would notice that the useEffect isn't even being triggered. More code always comes with more bugs, more test cases and more use cases that aren't supported by the frameworks we use. The best way to avoid writing a lot of tests is to write better code.
Summary
A reactive developer is a programmer which reacts to problems when they occur, and quickly finds a way to solve or mitigate the problem. In this article, we could see how such behavior could lead to writing more code which are both less readable and more prone to errors. We explained how being proactive and writing code with the future in mind could save time both when writing code and writing tests.