Sebastian Markbage, a member of the React core team (inventor of the React Hooks), once said: “What we did with React is we implemented Algebraic Effects.
So, what are the algebraic effects? What does he have to do with React?
What are algebraic effects
Algebraic effects are a concept in functional programming used to separate side effects from function calls.
Let’s use fictional grammar to explain.
Suppose we have a function called getTotalPicNum that passes in two user names, looks for the number of images saved by that user, and returns the total number of images.
function getTotalPicNum(user1, user2) {
const num1 = getPicNum(user1);
const num2 = getPicNum(user2);
return picNum1 + picNum2;
}
Copy the code
In getTotalPicNum, we don’t care about the implementation of getPicNum, just the process of “getting two numbers and adding them together returns”.
Let’s implement getPicNum.
“The number of images saved by the user on the platform” is saved on the server. So, to get this value, we need to make an asynchronous request.
To keep getTotalPicNum as unchanged as possible, we first thought of using async await:
async function getTotalPicNum(user1, user2) {
const num1 = await getPicNum(user1);
const num2 = await getPicNum(user2);
return picNum1 + picNum2;
}
Copy the code
However, async await is contagious — when a function becomes async, it means that the function calling it also needs to be async, which breaks the synchronous nature of getTotalPicNum.
Is there any way to keep getTotalPicNum the same as the existing call to asynchronous requests?
No. But we can make one up.
Let’s make up a try like… Catch syntax — try… Handle and the two operators perform and resume.
function getPicNum(name) {
const picNum = perform name;
return picNum;
}
try {
getTotalPicNum('kaSong'.'xiaoMing');
} handle (who) {
switch (who) {
case 'kaSong':
resume with 230;
case 'xiaoMing':
resume with 122;
default:
resume with 0;
}
}
Copy the code
Perform Name is executed when the getPicNum method inside getTotalPicNum is executed.
At this point, the stack of function calls will jump out of the getPicNum method and be replaced by the latest try… Handle capture. Similar to throw Error after the latest try… Catch caught.
For example, Error will be used as catch after perform name, and Name will be used as handle after Perform name.
With the try… The biggest difference with a catch is that when an Error is caught by a catch, the previous call stack is destroyed. Handle resumes and returns to the call stack for Perform.
For case ‘kaSong’, resume with 230; The call stack returns to getPicNum, where picNum === 230
Again, try… The syntax of Handle is made up, just to illustrate the idea of algebraic effects.
To summarize: Algebraic effects separate side effects (in this case, the number of images requested) from the function logic, keeping the function focus pure.
Perform resume does not need to differentiate between synchronous asynchronous and asynchronous.
Application of algebraic effects in React
So what do algebraic effects have to do with React? The most obvious example is Hooks.
For hooks like useState, useReducer, and useRef, we don’t need to worry about how FunctionComponent state is stored in the Hook. React handles it for us.
We just have to assume that useState returns the state we want and write the business logic.
function App() {
const [num, updateNum] = useState(0);
return (
<button onClick={()= > updateNum(num => num + 1)}>{num}</button>
)
}
Copy the code
If this example isn’t obvious enough, take a look at the official Suspense Demo
In the Demo, ProfileDetails is used to display user names. The user name is requested asynchronously.
But in Demo it’s all synchronous.
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
Copy the code
Algebraic effects and generators
From React15 to React16, a major goal of Reconciler refactoring is to transform the old synchronous update architecture into asynchronous and interruptible update.
Asynchronous interruptible updates can be understood as updates that may be interrupted during execution (browser time fragmentation running out or a higher-priority task jumping the queue) and then resume the intermediate state that was previously executed when execution can continue.
This is the algebraic effect of try… The role of handle.
In fact, browsers natively support a similar implementation, which is called a Generator.
However, some flaws with Generator caused the React team to drop it:
-
Like Async, Generator is infectious, and other functions that use the context of Generator need to change as well. This is a heavy mental burden.
-
The intermediate state of the Generator execution is context-dependent.
Consider the following example:
function* doWork(A, B, C) {
var x = doExpensiveWorkA(A);
yield;
var y = x + doExpensiveWorkB(B);
yield;
var z = y + doExpensiveWorkC(C);
return z;
}
Copy the code
One of the doExpensiveWork is executed each time the browser has free time, interrupted when time runs out, and resumed from where it left off when it resumes again.
The Generator works well for asynchronous interruptible updates when only interrupts and continues of single-priority tasks are considered.
But when we consider the case of “high priority task jump the queue”, if at this time has completed doExpensiveWorkA and doExpensiveWorkB calculate x and Y.
At this point, component B receives a high-priority update. As the intermediate state performed by the Generator is context-dependent, x previously calculated cannot be reused when calculating Y and needs to be recalculated.
A new level of complexity is introduced if the previously executed intermediate state is saved through global variables.
Refer to this issue for a more detailed explanation
For these reasons, React does not use a Generator for the coordinator.
Algebraic effects and Fiber
Fiber is not a new term in computer terminology. Its Chinese translation is called Fiber, which is a program execution Process, along with Process, Thread and Coroutine.
In many articles, fibers are understood as an implementation of coroutines. In JS, the implementation of a coroutine is a Generator.
Therefore, we can understand Fiber and coroutine as the embodiment of algebraic effect in JS.
React Fiber can be understood as:
React implements a set of status updates. Supports different priorities of tasks, interrupts and recovers, and can reuse the previous intermediate state after recovery.
Each task update unit is the Fiber node corresponding to the React Element.
Learn the React source code and read the React Technology