By Sebastian Markbage

Translator: UC International research and development Jothy


Welcome to the “UC International Technology” public account, we will provide you with the client, server, algorithm, testing, data, front-end and other related high-quality technical articles, not limited to original and translation.

Editor’s note: This article is from React Hooks Issue, written by React author Sebastian Markbage.


I’d like to sum up my impressions after reading all the relevant comments.

It has to be said that the reaction from React Hooks is very strong. It was popular and did well. It is widely accepted and applied to production. Its reputation and usage specifications seem to have spread well and have been adopted directly by other libraries. This is not to say that there are no other possible changes, but I would like to say that the current design is not a complete failure.

The main discussion around this mechanism is the dependency on the injection and persistent call order that hooks actually implement. Some want one, or both, to change. But the “purest” model is like Monad.


Injection pattern

Essentially, the argument is to replace the hooks implementation code. This is a bit like the usual dependency injection and inversion of control problem. React doesn’t have its own dependency injection system (unlike Angular), nor does it need it, since most entry points are (actively) pull rather than push. As for the rest of the code, the module system already provides good dependency injection boundaries. For testing, we recommend other techniques, such as mocking at the module system level (for example, using JEST).

Different apis include setState, replaceState, isMounted, findDOMNode, batchedUpdates, etc. In fact, React already uses dependency injection to insert the updater into the Component base class as the third argument to the constructor. Component doesn’t actually do anything. This is why React has multiple types of renderers in different versions of the same environment, such as React ART or React Test Renderer. Custom renderers do the same thing.

In theory, third party libraries like the React-Clones could use updater to inject their implementations. In practice, most people prefer to replace the react module with the SHIm module because there may be trade-offs or they may want to implement certain apis (for example, remove development mode content, or merge base classes and their implementation details).

In the world of Hooks, these two options remain. Hooks are not actually implemented in the React package. It simply calls the current dispatcher. As I mentioned above, you can temporarily reload it to any implementation you want at any time, and the React renderer does just that, enabling multiple renderers to share the same API. For example. You can create a Hooks test scheduler specifically for unit test hooks. The name “scheduler” is a little scary, but we can change it at any time. It’s not a design flaw. Now, you could move the scheduler into user space, but this would create additional distractions for users who have little or no connection to the component’s author, as most of the people in this issue don’t know the React updater.

In general, we will probably introduce more static function calls because they are better suited to tree shaker techniques for better optimization and inlining.

Another problem is that the main entry for hooks is React and not third-party packages. It is likely that react will remove many existing things in the future and hooks will be retained, so hooks bloat is not a problem. The only problem is that these hooks belong to React, not something more generic. Vue, for example, also considered implementing the hooks API. However, the key to the hooks is that their primitives are clear. At this point, Vue has a completely different primitive, and we’ve iterated over the primitive. Other libraries may come up with slightly different primitives. At this point, it makes no sense to make these too general too soon. We chose to implement the first iteration on React just to show that this was our vision for the primitive. If other libraries are similar, we’ll create a third-party package to integrate those libraries and redirect the React library to that package.



Rely on persistent calls to indexes

To be clear, we’re not talking about execution order dependence. Questions like use estate or useEffect first don’t matter.

React has many modes that depend on the order of execution, precisely because mutation is allowed in the render (which still keeps the render itself pure).

I can’t change the order of children and headers in the code.

Hooks don’t care what order you use them in, it only cares if the order is persistent, the same order every time. This is quite different from the implied dependencies between calls.

It’s best not to rely on persistence orders – all things are equal. However, you have to make trade-offs – like syntax interference or other confusing things.

Some people think it should be done, if only for the sake of purism. However, some people are also thinking more realistically.

There are concerns that it will become confusing, and this can happen on many levels. I don’t think it’s confusing because people are completely powerless or just give up. But in fact, the basics are pretty easy to pick up.

What’s more, if something goes wrong, it’s hard to figure out what went wrong. Even if you understand how it works, you can still make mistakes, in which case you have to easily identify the problem and fix it. We found quite a few of these problems. Normally it will be caught by the Lint rule, and the error message is enough to explain why. But we can do more. We can make compilation hard errors, keep track of hooks in development mode, and issue warnings when switching orders. In these cases, we can optimize the error message to show the changed stack instead of just showing something changed. There is a growing trend towards simulation effects in type systems, such as Koka. I bet JavaScript will use it, it’s just a matter of time.

Another question is whether these constraints make coding more difficult. This does not seem to be the case with ordinary conditional code. It is generally easy to refactor. The lack of early response is annoying, but not a big deal. Hooks have other problems that have nothing to do with order.

However, Hooks in refactoring loops can be very annoying. The solution is usually to decompose the body of the loop into separate functions. This is also inconvenient because you need to pass all the data through props, otherwise there will be closure issues. This is the harder problem in React, not limited to Hooks. Doing so is best practice and helps optimize. For example, keep rendering costs low when changing individual items in a list, ensuring that each subitem can stand alone. Using Suspense means that each child item can be fetched and rendered in parallel rather than sequentially, and error bounds have similar requirements. So even if you solve the Hooks problem separately, it is still best practice to split the body of the loop into separate components – this also solves the Hooks loop problem.

That is, the initial implementation of Hooks could create a keyed nested scope to be used as a compiler target. They do create a mechanism to support hooks in a nested way. But it’s not very ergonomic or easy to interpret, and would have run into the above problems anyway. We can add it in the future if needed. This should not be a common occurrence now.

Each of the alternatives we considered had many drawbacks.

  • Most schemes do not support loops. This is the biggest limitation with regard to the Hooks’ unconditionality, and many proposals seem to ignore it. There’s no value in just making it available on your own terms.

  • Most scenarios do not explain why custom hooks are common. We think this is an important goal for performance and syntax lightness.

  • Once you allow hooks to be used for conditional statements, something gets weird. For example, you might see useState in the condition, but what does that mean? Does this mean that its scope is only within the block, or does its life cycle change with it? if (c) useEffect(…) What does that mean? Does this mean that the effect fires when the condition is true, or that it fires every time the effect is true? Uninstall or continue the component life cycle when the condition is no?

  • What do multiple calls to hooks mean for a proposal like declaring hooks outside of body? Just because it’s technically possible doesn’t make it any less messy.

  • Most schemes use a lot of indirect and virtual scheduling that makes it difficult to do static analysis, making dead code elimination, inlining, and other types of optimizations more difficult. The current hooks proposal is very effective because it only has index attributes, can be easily minify, and has predictable O(1) lookup costs. Keep in mind that file size matters and this is a real highlight of the current design.

As a side note: someone mentioned that everyone is concerned about the global state of concurrency. In the future, if JS supports threads, or if we compile something that supports threads, we want to be able to support parallel execution of multiple components. However, this is not a problem in practice, because the small state we store to track currently executing components and current hook indexes can easily go into thread-local storage — which is always required in some form of solution anyway, Both mutable field and algebraic effects.


debugging

There was widespread concern about what debugging would look like. Let’s look at it from the following perspectives.

The first is the error message. We worked hard to get better error messages. When a hook violation is detected in DEV, we can at least handle the error well.

Breakpoint debugging becomes very simple because it just uses the normal execution stack, unlike React. Some alternatives use more indirection, making breakpoint debugging more difficult.

Another problem is the reflection of trees. React DevTools allows you to check the current state of anything in the tree. In production packages, we often minify class names and so on. It’s likely that after we added more production optimizations, such as inlining and removing unnecessary props objects, more of these things would not happen automatically without the Source Map. We have no faith in adding metadata to our API designs for production debugging. However, we can do things like auxiliary metadata (such as source Map) while developing the pattern.

That said, we have demonstrated that we can use the Debug Tools API to extract large amounts of reflection metadata. We plan to add more so that the library has good extension points to provide richer reflection data for debugging, or to parse source lines to add names to individual useState calls.


test

Everyone knows that testing is important, so we need to document it clearly before a wider release. There is no doubt about that.

As for the technical details, I think the dependency injection points mentioned above show you how you can do it.

I think there is a sense in API design that there are “testable” apis. When I hear people say that, I think they think of something like pure functions, with only a few input variables that can be tested separately. The React API is so simple that you might only want to call the Render function directly or call a single hook directly.

Unfortunately, the richness of the API also brings some nuances. You don’t always rely on it, so you can always use it in special cases, or in one step in simple tests. However, you’ll run into more and more of these situations as your code base grows, and you don’t want to re-implement all the nuances of the React runtime in every code base. So you want a testing framework.

For example, we built the shallow renderer class for this use case. It allows you to “use” or “follow” (such verbs) the correct semantics to invoke all lifecycles. It is also worth testing all the nuances of the Hooks primitive.

In practice, however, we find that shallow renderers are rarely used. Using deep rendering is more common because the units of work you are testing usually rely on lower levels that have passed the test.

That is, we will also include a component-isolated method to test directly for custom hooks. All we have to do is add something that mimics the scheduler, while keeping the semantics of the primitive consistent.




API design

useReducer

Will this replace Redux? Does it add to the burden of having to learn all about Flux? Reducer has a much narrower scope of use than a generic Flux framework. It’s simple. However, if you look at the direction of frameworks/languages like Vue, Reason, Elm. This pattern of scheduling and gathering logic to transition between higher-level states seems to have been a great success. It also solves many of the odd problems with React callbacks, bringing more intuitive solutions to complex state transitions. Especially in a parallel world.

In terms of bloat, it doesn’t add any code to React that it doesn’t need. On the conceptual side, I think this is a concept worth learning because the same pattern keeps cropping up in various forms and it’s good to have a central API to manage it.

So I think of useReducer more as the central API than useState. UseState is still great because it is very concise and easy to explain for simple use cases, but one should explore useReducer or similar patterns early on.

That said, useReducer doesn’t do many of the things that Redux and other Flux frameworks do. I generally think you wouldn’t need it, so it might not be as prevalent as it is now, but it’s still there.

Context Provider

Someone said that when you only want to expose one way to consume the Context, ideally you should not expose the Context Provider from the module. It looks like useContext encourages you to expose the Context object. I think the way to do this is to expose a custom hooks for consumption. Such as useMyContext = () => useContext(Private), which is usually better because you are free to add custom logic, change it to global logic, or add deprecation warnings again. It doesn’t seem like something that requires further abstraction from the framework to implement.

We could consider having createContext return a hooks directly, and we encourage the use of this common pattern. [MyContextProvider, useMyContext] = createContext()

Another quirk of the Context Provider is that you can’t use hooks to provide new Context, you still need a wrapper component. A similar problem is that you cannot attach event listeners to the current component via Hooks or something like findDOMNode.

The reason for this is that Hooks are either designed independently or simply observations. This means that using a custom Hook does not affect anything in the component that is not explicitly passed to the Hook, and it never burrows into any level of abstraction. This also means that order dependence doesn’t matter. The only exception is when dealing with globally mutable state, like traversing the DOM. It’s an escape hatch, but not something you can abuse in the React world. This also means that using Hooks does not depend on order. Like useA (); useB(); Or useB (); useA(); You can call it that way. Unless you explicitly create dependencies by sharing data. let a = useA(); useB(a);

useEffect

By far the most bizarre Hook is the useEffect. To be clear, this is expected to be by far the most difficult Hook to use because it uses imperative code that is difficult to manage, which is why we try to keep it declarative. However, moving from declarative to imperative is difficult because declarative can handle more different types of states and transitions per line of code. When you implement an effect, you should ideally deal with all the cases that follow. Part of the goal is to encourage more cases so that quirks can be resolved.

No doubt, the second parameter is odd. It’s the second argument instead of the first because with all these methods, you can code them first and add them later. The nice thing about this property is that you can use Linter or a code refactoring tool in the IDE, or have the compiler add it automatically based on your callbacks. This is a lesson learned from C#, where syntactic order is designed to support things like auto-complete.

Maybe it should have a comparison function. I haven’t seen a situation where we can’t rewrite it as input, but whether we add a comparison function or not, we can do it later. That also requires an input array to let us know what to store and pass to the comparison function.

The use of asynchronous functions for effect is now disallowed, which means that you have to make an effort to do asynchronous cleanup. It’s hard to get asynchronous effects right because anything is possible between these steps. You cannot clean up a new effect before initializing it, or else the effect properties may be affected. It’s possible to relax this later, but I suspect it’s a bad model, and maybe we shouldn’t encourage it in the first release.

UseEffect is strangest when using closures. This can be confusing when we want to keep track of certain values. Because the values of closures aren’t actually reactive, they capture the current state, which is actually a nice advantage. Because of batch and concurrent patterns, most of the time things are intertwined in unexpected ways. Captured values do cause errors due to counterintuitive closures, but can greatly reduce their race condition problems once fixed.

Another is memory usage. I’d say the memory usage of React is generally unpredictable, because we basically remember everything in the tree. However, because closures share the execution environment, it can lead to additional counter-intuitive extensions. This can be fixed by making it a custom hook, but it’s not always obvious, so sometimes you have to do this. An optimized compiler that understands this pattern can also easily solve this problem.

One solution to this problem is to introduce useEffect as the same function, pass all the input parameters as parameters of the function, and encourage suspending them. This is problematic, however, because closures have the advantage of conveniently referencing computed values. Closures are also encouraged for other patterns. So this seems to undermine the idea of “everything goes into the method body.” This in turn addresses other issues such as default props, computed values, etc. I’m not sure it’s worth doing for the few remaining cases, but not doing it leaves more.




The lack of the API

Several people pointed out that we were missing some apis.

The second argument to setState does not work well in this model. Again, we don’t have anything like UpdateWithSideEffects in ReasonReact yet. We have thought about how to use it and will add to it later. For example, call emitEffect in reducer.

There is no way to change individual components due to state transitions. If there is a method, then we might in turn need to bypass it to make changes as forceUpdate does.

We don’t have alternatives to getDerivedStateFromError and componentDidCatch yet, but we do have HOC to provide this functionality. Catch ((props, error) => {if (error) setState(…) ; useEffect(() => { if (error) … }); }). We’ll add that later.

There have always been questions about whether there could be lower-level apis to implement the specific semantics of other languages, such as ClojureScript or Reason. This is something we definitely want to support, but I’m not sure it should be done using something like a public API. For example, the React optimized compiler requires a different entry point to locate the format. The mechanism should be usable for a variety of lower-level operations without needing to be optimized for ease of use. Therefore, we might add it separately from the public API.




type

I believe that most of the JavaScript type issues have been resolved because Flow and TypeScript are now defined.

One interesting question that has not been addressed is whether it is possible to code correctly in another language, such as Rust or Reason, even if the order of calls is out of order. This has not been proven – at least not at run time.




Compiler optimization

Some people worry that these impure functions affect compiler optimizations. I take issue with this: we also want to, and have done a lot of runtime or statically executed optimizations.

We actually tried very hard to get Hooks out because they are very good for tuning. It carefully encourages many static resolution patterns to emerge.

There are two optimizations about merging components. For components rendered unconditionally by the parent component, this is plain hooks that you can call directly. You can perform this optimization in user space. Even for loops and conditions, we know how to add the same scope to them. Even with dynamically rendered components, we can skip the creation of additional Fibers, such as when a set of parent components renders a child component that is also a function component. We just need to keep track of the order in which the switch occurs in the case of a function type change.

This also works for memoization optimizations. In a language with algebraic effect, the memorization function just needs to remember effect at the same time. Same thing here. The memory only needs to trace the hooks that were issued during the invocation.

Many alternatives to using functions such as objects or transients need to be developed in a way that makes actual optimizations more difficult because of their indirection, with Generator being the most difficult.




security

Someone mentioned the overloaded API in setState, which takes a function or its return value as an argument. It was a tough design decision, and we made a lot of trade-offs. Indeed, overloaded apis can sometimes lead to unexpected security issues. We had an example of a factor component that accepted both strings and elements. But there are many overloaded apis that are very useful abstractions and don’t cause security problems. I would look into this a bit more to make sure there was a reasonable assessment of risk.

There is another point which has not yet been raised but which I would like to address. If you don’t think a third-party Hook can be trusted because it can conditionally add/remove its state hooks and then read from the end of their hooks. You allow it to read state from external components. If you can execute code, you usually lose, but in an environment like Caja/SES this can be relevant. This is an unfortunate situation.




motivation

Why are all special hooks core? Since all the mechanics have to be there, most of them don’t really add volume, but more conceptual overhead. Most of these are primitives that cannot be implemented in user space or provide significant value through descriptive intent.

For example, you can now use useMemo in user space, but we want state to be preserved in the future if memory is low or memory values are dropped in window components.

UseCallback is just a simple wrapper around useMemo, but we’ve already thought about how to further optimize them in the future, either statically or using runtime technology.

UseImperativeMethods provides an API that can be built in user space, but since we interact with Refs in several different ways, it is better to maintain them as a single specification. We have changed refs twice.

One argument I’ve been hearing is that there’s not enough motivation for this change, because “classes are fine.” The group of users who tried to learn allegedly crashed. I think this argument is too one-sided. Some people also emphasize that class is hard for beginners to beat — so that’s beside the point.

The main motivation is that patterns like closures naturally create copies of values, which makes it easier to write concurrent code because you can store N states at any given point, rather than just one in the case of mutable classes. This avoids many class pits, because the classes look intuitive but the actual results are unpredictable.

A class seems like an ideal way to maintain state, because that’s what it was born for. However, React is more like a declarative function that is executed repeatedly to simulate the reaction state. There is an impedance mismatch between the two, and it keeps leaking when we think of these as classes.

Another is the problem of classes in JS combining methods and values in the same namespace. This makes optimization difficult because sometimes methods behave like static methods and sometimes like containing the value of a function. The Hooks pattern encourages more statically resolvable calls to helper functions.

Within a class, each method has its own scope. It leads to problems like ours having to reinvent the default props so that we can create a separate shared resolution object. We also encourage you to use mutable fields in your class to share data between these methods, because the only thing shared is this. This is also problematic for concurrency.

Another problem is that React’s conceptual mental model is simply a function that recursively calls other functions. There’s a lot of value in expressing it in these terms, in building the right mental model.

One problem I have great sympathy with is that this will only increase the mental cost of learning React – in the short term. That’s because you’ll probably be learning Hooks and classes for the foreseeable future. Either you use both, you have a previous class in your code base or a class written by someone else, a class used by an example or tutorial you read on StackOverflow, or a library you’re debugging uses it.

It will take years, but MY bet is that one of these approaches will prevail. Either we have to roll back Hooks or gradually reduce the use of classes until they are completely gone.

I think the criticism is fair. We really don’t have a definitive answer, or an expected roadmap for how class can reasonably be moved out of the core and externalized into the Compat layer. I don’t think we’ll have a definitive answer for a while until we actually see Hooks get better. At that point, we’ll see if we give a timeline for reducing the importance of classes.


英文原文 :

https://github.com/reactjs/rfcs/pull/68#issuecomment-439314884



Good article recommendation:

React 16.x roadmap published, including Suspense components for server rendering, Hooks, etc

React supports lazy, Memo, and contextType in V16.6.0



UC International Technology is committed to sharing high quality technical articles with you

Please follow our official account and share this article with your friends