This article is written by Sebastian Markbage, core developer of React, to explain his original intention of designing React and the basic theoretical concepts related to React. By reading this article, you can think about React’s past, present and future at a higher level. 译 文 : React – Basic Theoretical Concepts.
In this post, I want to formally elaborate on my mental model of React. The purpose is to explain why we designed React the way we did, and you can also deduce React from these arguments.
Admittedly, some of the arguments or premises in this article are controversial, and some of the examples may have bugs or omissions in their design. And this is just the beginning, so feel free to submit a pull Request if you have a better idea for how to improve it. This article will not cover the details of the code base, but will help you see React design process from simple to complex.
The real implementation of React. Js is full of problem-specific solutions, incremental solutions, algorithm optimizations, legacy code, debugging tools, and other things that make it truly highly available. This code can be unstable, as future browser changes and feature weights are subject to change. So the specific code is difficult to explain thoroughly.
I prefer a more concise mental model for my introduction.
Transformation
The core premise for designing React is that the UI simply transforms data into another form of data through mapping. The same input must have the same output. This happens to be a pure function.
function NameBox(name) {
return { fontWeight: 'bold'.labelContent: name };
}
'Sebastian Markbåge'- > {fontWeight: 'bold'.labelContent: 'Sebastian Markbåge' };
Copy the code
Abstraction
You can’t implement a complex UI with just one function. Importantly, you need to abstract the UI into multiple reusable functions that hide internal details. To implement a complex UI by calling one function within another is called abstraction.
function FancyUserBox(user) {
return {
borderStyle: '1px solid blue'.childContent: [
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]
};
}
{ firstName: 'Sebastian'.lastName: 'Markbåge'} - > {borderStyle: '1px solid blue'.childContent: [
'Name: ',
{ fontWeight: 'bold'.labelContent: 'Sebastian Markbåge'}};Copy the code
Composition
To achieve true reuse, it’s not enough to just reuse leaves and then create a new container for them each time. It must also be possible to recompose containers containing other abstractions. I understand “composition” as combining two or more different abstractions into a new “abstraction”.
function FancyBox(children) {
return {
borderStyle: '1px solid blue'.children: children
};
}
function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}
Copy the code
State
The UI is not just a replication of server-side response data or business logic state. There are actually many states for a specific render target. For example, typing in a text field doesn’t have to be copied to another page or to your mobile device. Another example is the scroll position state, which you will almost never copy to multiple render targets.
We tend to use immutable data models when updating state. And a function that changes state is like an atomic operation, and we need to connect them from the top down.
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
// Implementation details
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
/ / initialization
FancyNameBox(
{ firstName: 'Sebastian'.lastName: 'Markbåge' },
likes,
addOneMoreLike
);
Copy the code
Note: Updating the status in this example has side effects (in the addOneMoreLike function). My practical idea is that we return the status of the next version when an “update” comes in, but that would be complicated. This example needs to be updated
Memoization
For pure functions, using the same arguments over and over is a waste of resources. We can create a Memorized version of a function that keeps track of the last argument and result. This way, if we keep using the same value, we don’t need to execute it over and over again.
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}
var MemoizedNameBox = memoize(NameBox);
function NameAndAgeBox(user, currentTime) {
return FancyBox([
'Name: ',
MemoizedNameBox(user.firstName + ' ' + user.lastName),
'Age in milliseconds: ',
currentTime - user.dateOfBirth
]);
}
Copy the code
Lists
Most UIs display list structures of different items in list data. This creates a natural parent-child hierarchy.
To manage the state of each item in the list, we can create a Map that holds the state of a specific item.
function UserList(users, likesPerUser, updateUserLikes) {
return users.map(user= > FancyNameBox(
user,
likesPerUser.get(user.id),
() => updateUserLikes(user.id, likesPerUser.get(user.id) + 1))); }var likesPerUser = new Map(a);function updateUserLikes(id, likeCount) {
likesPerUser.set(id, likeCount);
rerender();
}
UserList(data.users, likesPerUser, updateUserLikes);
Copy the code
Note: Now we pass multiple different parameters to FancyNameBox. This breaks our memoization because we can only store one value at a time. More on this below.
Continuations
Unfortunately, since there are too many lists in the UI, we need a lot of repetitive boilerplate code to manage them visually.
We can move some templates out of the business logic by delaying the execution of some functions. For example, use “Currize” (bind in JavaScript). We can then pass state from outside the core function so there is no boilerplate code.
This doesn’t reduce boilerplate code, but at least it takes it out of the critical business logic.
function FancyUserList(users) {
return FancyBox(
UserList.bind(null, users)
);
}
const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
constresolvedBox = { ... box,children: resolvedChildren
};
Copy the code
State Map
We mentioned “composition” earlier, and you can use composition to avoid reusing the same boilerplate pattern. We can move the execution and passing of state logic to lower-level functions that are used a lot.
function FancyBoxWithState(children, stateMap, updateState) {
return FancyBox(
children.map(child= > child.continuation(
stateMap.get(child.key),
updateState
))
);
}
function UserList(users) {
return users.map(user= > {
continuation: FancyNameBox.bind(null, user),
key: user.id
});
}
function FancyUserList(users) {
return FancyBoxWithState.bind(null,
UserList(users)
);
}
const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);
Copy the code
Memoization Map
Once we want to memoize multiple items in a list cache, it becomes difficult. Because you need to develop complex caching algorithms to balance call frequency and memory usage.
Fortunately, the UI is relatively stable in the same location. The same location will generally accept the same parameters each time. As such, using a collection to do Memoization is a very useful strategy.
We can pass an memoization cache in our combined functions in the same way we did with state.
function memoize(fn) {
return function(arg, memoizationCache) {
if (memoizationCache.arg === arg) {
return memoizationCache.result;
}
const result = fn(arg);
memoizationCache.arg = arg;
memoizationCache.result = result;
return result;
};
}
function FancyBoxWithState(children, stateMap, updateState, memoizationCache) {
return FancyBox(
children.map(child= > child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
))
);
}
const MemoizedFancyNameBox = memoize(FancyNameBox);
Copy the code
Algebraic Effects
When multiple layers of abstraction need to share trivial data, passing data through one layer is cumbersome. It would be nice if there were a way to quickly transfer data across multiple levels of abstraction without having to involve the middle level. React we call it context.
Sometimes data dependencies are not strictly top-down from the abstract tree. For example, in a layout algorithm, you need to know the size of child nodes before implementing their positions.
Now, this example is a little bit super general. I will use algebraic effects, a new ECMAScript feature proposal that I initiated. If you’re familiar with functional programming, they avoid the ritual-like coding imposed by Monad.
function ThemeBorderColorRequest() {}function FancyBox(children) {
const color = raise new ThemeBorderColorRequest();
return {
borderWidth: '1px'.borderColor: color,
children: children
};
}
function BlueTheme(children) {
return try {
children();
} catch effect ThemeBorderColorRequest -> [, continuation] {
continuation('blue'); }}function App(data) {
return BlueTheme(
FancyUserList.bind(null, data.users)
);
}
Copy the code