Re:source

How to Understand 'Side Effects'

React introduced hooks in functional components, and one of the most important and frequently used hooks is useEffect. Many students use this hook as a 'data listener,' performing certain actions when dependencies change. Is this the correct usage? Why is this hook called 'effect' instead of 'listener'? What does 'effect' signify? This article will provide the answers.

Translation Notice

This content is automatically translated from Chinese by AI. While we strive for accuracy, there might be nuances that are lost in translation.

Side Effects

Real-world ‘Side Effects’

In daily life, we most often hear about ‘side effects’ in the context of taking medication. Typically, drugs have ‘side effects’ (also known as adverse reactions). For example, cephalosporins are used to treat bacterial infections but can cause side effects like diarrhea, nausea, and rashes.

So, we can understand that in order to achieve a certain goal, other unintended effects may occur, which are called ‘side effects.'

'Side Effects’ in Programming

The term ‘side effects’ commonly appears in functional programming. Before discussing ‘side effects,’ let’s briefly talk about ‘functional programming.’

In functional programming, there is a type of function called pure functions:

A pure function is a function that, given the same input, will always return the same output and has no observable side effects.

For example:

function add(a: number, b: number) {
  return a + b
}

This is a pure function. For any given parameters a and b, this function will always produce the same output, no matter when or where it is called. Another example is the two often-confused functions in JavaScript: slice and splice. Both can extract a portion of an array, but their implementations differ. We can consider slice a pure function, while splice is not, because splice modifies the original array, causing observable side effects:

var xs = [1,2,3,4,5];

// Pure
xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]

// Impure
xs.splice(0,3);
//=> [1,2,3]

xs.splice(0,3);
//=> [4,5]

xs.splice(0,3);
//=> []

If a function, during its execution, affects the external environment, it is said to have produced ‘side effects.’ Common scenarios that produce side effects include but are not limited to:

  • Modifying any external variables or object properties (e.g., global variables, static variables)
  • Modifying function parameters
  • Throwing exceptions
  • Printing or logging
  • Reading or writing files (IO operations)
  • Database operations
  • Making network requests

In summary, any interaction with the external environment of a function constitutes a side effect. Are side effects bad? Certainly not! In practical programming scenarios, the above situations are inevitable. The philosophy of functional programming assumes that side effects are the main cause of improper behavior, but it does not mean that all side effects should be banned. Instead, they should be controlled within manageable limits. For more on how to control them, interested readers can refer to the ‘Digression’ section.

React React useEffect

Let’s revisit React React ’s useEffect. First, this issue only exists in Function Components (since only Function Components have hooks), which are essentially functions. As functions, they can have side effects. How do we handle side effects? useEffect is the mechanism React React provides for handling side effects.

Let’s look at the useEffect API documentation:

useEffect is a React React Hook that lets you synchronize a component with an external system.

In other words, useEffect allows you to synchronize with external systems, i.e., handle side effects (so useEffect is definitely not meant to be used as a listener). The documentation also notes:

If you’re not trying to synchronize with some external system, you probably don’t need an Effect. If you’re not synchronizing with an external system, you probably don’t need an Effect.

It is recommended to carefully read the ‘Further Reading’ section on React React Effect, which will be shared later.

Digression: Handling Side Effects in Functional Programming

In functional programming, it is advocated to separate side effects, keeping pure logic free from side effects. This is where IO Monad comes into play.

First, let’s understand what a Monad is. A Monad is used to solve two problems: ‘function composition’ and ‘exception handling.’

Suppose we have two functions, f and g, both taking and returning a number (ignoring the specific implementation, the following are just function signatures):

f::Number -> Number
g::Number -> Number

Now, we want to combine them to create a new function h, which also takes and returns a number:

h::Number -> Number

This is simple. We just need to create a utility function to compose the functions:

let compose = (func1, func2) => x => func1(func2(x))
let h = compose(g, f)

The above solves the problem of function composition. Next, let’s look at exception handling. In functional programming, it is generally believed that exception handling should not interrupt the execution flow, so using try-catch to catch exceptions is not feasible. To handle exceptions without interrupting execution, we need a ‘container’ to wrap the function’s result. This container is called Maybe.

interface Maybe<T> {
  error?: boolean
  data?: T
}

Now, let’s modify the functions f and g to return Maybe, and we want the combined function mh to do the same:

mf::Number -> Maybe Number
mg::Number -> Maybe Number

mh::Number -> Maybe Number

However, our previous compose method no longer works because mf returns Maybe Number, while mg expects a Number. We need a new mcompose method. At this point, Maybe becomes a Monad.

It might be easier to understand in a chained call format:

let mf = x => Maybe.of(x).map(f)
let mg = x => Maybe.of(x).map(g)

let mh = x => Maybe.of(x).chain(mf).chain(mg)

Here, ‘of’ wraps a number into a Maybe structure.

‘chain’ processes the value it holds using the input function and outputs a new Maybe structure holding the processed result.

‘map’ is used for specific value transformations.

ES’s native Monad: Array

let f = x => x + 1
let g = x => x ** 2

let mf = x => Array.of(x).map(f)
let mg = x => Array.of(x).map(g)

let mh = x => Array.of(x).flatMap(mf).flatMap(mg)

Monads are also the foundation of streams. The core of various streaming frameworks is Monad, such as Observable in rx.

Now that we’ve covered Monads, let’s look at IO Monad. We mentioned that side effects involve interactions with external systems, which are essentially reads and writes. Therefore, IO in IO Monad stands for Input/Output, a Monad that wraps read and write operations to the external world. Its core idea is:

No matter what side effects you encounter, don’t be afraid; wrap them in a function and handle them later.

For example:

function Test() {
  const dataStr = localStorage.getItem('key')
  const data = JSON.parse(dataStr)
  const reviewDate = data.review
  const reviewOutput = reviewData.map((text, index) => `${index}: ${text}`)
  console.log(reviewOutput)
}

In this code, the second and sixth lines have side effects. What we need to do is wrap these two lines in functions:

const readFromStorage = () => localStorage.getItem('key')
const writeToConsole = console.log

Then, wrap readFromStorage in an IO Monad:

const storageIO = new IO(readFromStorage)

The value wrapped by storageIO is not a specific value retrieved from storage but the readFromStorage function itself. This function, as a value, is controllable and deterministic. Next, wrap the other logic into functions and organize them using map in a chained call:

const parseJSON = string => JSON.parse(string);

const getReviewProp = data => data.review;

const mapReview = reviewData => reviewData.map((text, index) => `${index}: ${text}`)

const task = storageIO
  .map(parseJSON)
  .map(getReviewProp)
  .map(mapReview)

Finally, we execute the task, and the IO Monad’s unique fork method subscribes to the writeToConsole function:

task.fork(writeToConsole)

Further Reading

Functional Programming Guide

Deep Dive into Functional Programming (Part 1) - Meituan Tech Team

Deep Dive into Functional Programming (Part 2) - Meituan Tech Team

React React useEffect

React React Effect Series

https://react.dev/learn/synchronizing-with-effects

https://react.dev/learn/you-might-not-need-an-effect

https://react.dev/learn/lifecycle-of-reactive-effects

https://react.dev/learn/separating-events-from-effects

https://react.dev/learn/removing-effect-dependencies

References

https://medium.com/nerd-for-tech/what-are-side-effects-in-programming-51f7ef340f98

https://medium.com/@remoteupskill/what-is-a-react-side-effect-a5525129d251