Minimize side effects in front end apps by limiting state updates to interactions with the outside world

Rob Bertram
4 min readAug 14, 2021

The topic of state management is supremely important in well designed front end applications. If you’re like me, you read blogs about how to properly structure something like a redux store and placed your stateful variables in the correct location in the component hierarchy to maximize performance readability then called it a day. You may very rarely give much thought to side effects and how to minimize them — you may not even think in terms of side effects — more about side effects in a moment.The main idea I’d like to explore is that, in general, external inputs are the only thing that should update state in a front end application. In other words, things that come directly from a user should trigger state updates with minimum transformation of received data.

The reason for minimizing updates to state is because updates to state are side effects. Side effects are operations that make a function impure in the sense that you do more than simply return an output based solely on its inputs. This is generally counterproductive when trying to maintain, test, and understand software. Selling you on the benefits of avoiding side effects isn’t the purpose of this blog, but you can read more about it here.

The issue of state is generally a major factor in what makes front end application difficult to design really well. Functions quickly become intertwined with updates to state that have implications for other parts of your app which become hard to manage as complexity increases.

So the question that this post tries to answer is: How can we limit setting state as much as possible so as to reduce the negative impacts of side effects in our code?

To answer this question, I’d like to start with a more fundamental question: Why do front end applications need state at all? The answer is because we’re dealing with users who can change our application over time based on things like keystrokes, mouse events, and the necessity to interact with servers. If we could some how completely remove factors such as these, we could have purely deterministic functions across the board with absolutely no side effects. Therefore, because we can’t realistically just do away with such factors, state should exist to facilitate/synchronize mutable variable changes based on these factors only.

To make this more clear, let’s start with an overly simple example. Suppose we’re collecting form data for a username and password. Our task is to

  1. collect the data
  2. validate it
  3. send the username, in all caps, to the back end along with the password when a login button is clicked

How might we correctly and incorrectly implement each of these steps? First, I’d say that we ought to mutate the data as little as is practically possible before setting it in state. In the case below, we use e.target.value with .toUpperCase() which is not as good as just setting the raw input then manipulating later when we need to. This example is kind of trivial and doesn’t have huge impacts, but a more real world example would be passing the data into some complicated function that at some point sets state based on potentially complicated conditions and logic.

BAD
function MyComponent() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
function updateUserName(e) {
// incorrectly capitalize the raw string setUsername(e.target.value.toUpperCase())
}
return (
<>
<input onChange={updateUserName}/>
<input onChange={e => setPassword(e.target.value)}/>
</>
)
}

What would be better is to have a function that computes what the formatted output should be given the existing state. In the example below, we now can more simply test the outputted capitalized username whereas before our function gave us no output; only a side effect. I’ve filled in some more of the functionality as well. Notice how handleLogin’s job is almost exclusively to deal with state. While it’s not exactly a simple function, the updates/reads from state are the minimum needed in reaction to a user clicking on the login button. Everything else that can be separated and removed from the component into a pure function is now above the component which we can more easily understand and test.

GOODconst getCapitalizedUsername = (username) => username.toUpperCase();function loginUser(username, password) {
return post('/login', {username, password});
}
function validateLogin(username, password) {
return !!username.length && !!password.length;
}
function MyComponent() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [userDetails, setUserDetails] = useState(null);
const [errorMsg, setErrMsg] = useState('');
async function handleLogin() {
const isValid = validateLogin(username, password);
if (!isValid) {
setErrorMessage('invalid login');
return;
}
const capitalizedUsername = getCapitalizedUsername(username); const userDetails = await loginUser(capitalizedUsername, password);
setUserDetails(userDetails);
}
return (
<>
<input onChange={e => setUsername(e.target.value)}/>
<input onChange={e => setPassword(e.target.value)}/>
<button onClick={handleLogin}>Login</button>
</>
)
}

I could be totally off in my thinking here, but it seems that the less we mutate state the better and that interaction with things external to your app really should be the only time you must update state. Everything else can be built with pure function. Following this rule could significantly improve the design of your applications. Let me know your thoughts :)

Unlisted

--

--

Rob Bertram

“A language that doesn’t affect the way you think about programming is not worth knowing.”