This is a wonderful example of a well engineered solution to the wrong problem. React is a wonderful view library, and if limited to that use it is remarkably fast and easy to use.
But the usual way of building things is to treat React as a total web app framework. The terminology is very confused: they still call it "rendering", even though it now usually includes data fetching, complex manipulations, event processing, etc. You just `React.render` once when your page first loads and then child components take care of the rest. It should really be called `React.start`.
> The reason for the stutter is simple: once rendering begins, it can’t be interrupted.
Rendering should never need to be interrupted. If your app is architected to have any rendering operation take long enough that it's perceptible to the user, STOP DOING THAT THING DURING RENDERING.
Want a performant web app? Apply separation of concerns properly, and limit your use of React to the view layer.
The React model makes sense because of how state management is part of it, not strictly just the view. I think seeing it just as a view library necessarily leads to the rest of your other conclusions. I recommend reading https://overreacted.io/react-as-a-ui-runtime/ by Abramov which describes React as a UI runtime.
Embracing React as a UI runtime you eventually find problems that concurrent mode solves. Some are related to performance and concurrency, but some are related to managing state and data dependencies. It also provides idiomatic ways that encourage good UX (like managing error/loading conditions once, at the right level).
> If your app is architected to have any rendering operation take long enough that it's perceptible to the user, STOP DOING THAT THING DURING RENDERING.
At my workplace we frequently encounter cases where the rendering itself - just the evaluation of the pure React functions - takes hundreds of milliseconds, if not several seconds. This is after memoizing every part of it that can be memoized, after profiling it for places where work can be shaved off, etc. Sometimes there really is just that much rendering, and unlike other types of computation, today's React gives you no options for putting that in a worker or breaking it up. Tabs' single threads mean you can't even scroll in the interim. Concurrent Mode is the only hope we have of solving that case, other than avoiding it with paging, which is what we currently do.
You can make it happen by rendering all components first as 'null' or placeholder <div> and pushing them on a queue. Then loop the queue with requestAnimationFrame() and set a batch of components visible for every frame. You can give each component a cost so that each batch will fit in a frame. It's basically a simple component that you can wrap around components that you wish to render in the queue. I have been using a solution like this for a long time in production and it works great.
You can combine this with virtualization if you have lots of non-visible components.
Since you mention scrolling, it sounds like you are rendering a lot of off-screen content... Have you looked into solutions like react-virtualized? It can definitely speed up the experience
It looks like React concurrent mode also deprioritizes the rendering of off-screen content for you. (I've been browsing the react reconciler source this morning...)
So, I'm not sure exactly how Fiber works, but given the context and what I've read/watched, here's my guess:
Confusingly, there are multiple tiers of "updates" happening in a React app. One is when your render functions are called to recompute their React element trees. Work can be avoided here via shouldComponentUpdate(), etc.
Once React has this internal data structure, it uses it to do another level of updates to the actual DOM. This part is totally black-boxed from the application code, because React has all the information it needs internally to avoid unnecessary updates at this level.
I think, maybe, that Fiber is specifically focused on this latter stage. Concurrent Mode would then be the stage 1 equivalent of what Fiber does in stage 2. This would be why it has an effect on how application code can actually be written, where Fiber didn't really.
"Fiber" was the rewrite of React's internal render logic (primarily how it processes the component tree), and was released in 16.0. It's named after the data structure that is used to track individual pieces of work (roughly on a per-component basis).
Fiber does specifically divide the overall update process into two general phases. "Render phase" involves walking the component tree, asking components what the desired UI should look like, and calculating the diff and necessary changes based on the previous requested output. Once that's been calculated, it is applied all at once in the "commit phase".
Concurrent Mode is, loosely, additional modifications to that rendering logic to enable pausing partway through the "render phase" process, tracking one or more in-progress versions of render passes, prioritizing them based on the source of the requested update (user input vs network request, etc), and recalculating in-progress renders if another higher-priority render was pushed to the front and committed to the DOM first.
Out of curiosity, how is it not still "rendering"? Do you prefer "start" because it renders a tree of children?
But yes, rendering doesn't need to be interrupted. They specifically mention architecture decisions to avoid rendering operations that take too long (debounce and throttle).
This eliminates previously necessary architecture decisions while providing a better user experience. How is that solving the wrong problem?
Also, this is still the view layer. There's nothing React-specific about data fetching, "complex manipulations" or "event processing".
Previously if you had a text input whose output you used to fetch some data and render in a list, you'd use debounce.
Now, with Concurrent Mode, you don't have to and can instead use the React API.
With the component paradigm, these things can't always easily be separated. Or, it is useful to consolidate data concerns and UI concerns, but often data concerns like UI have a hierarchy, or the concerns are spread all over the place (not in neat, clear groups).
It is very useful to declaratively specify the recipe for both UI and data, and allow the framework to coordinate these things across and within components.
>Rendering should never need to be interrupted.
Following this line of thought would lead to not rendering anything until everything is ready. Large applications have multiple levels of components and data concerns. These solutions allow for isolating responsibilities at smaller levels, and coordinating their presentation in ways that produce good UI (few loading spinners, flickering etc).
>and if limited to that use
This simply passes the responsibility somewhere else no? And any significant apps can't avoid these concerns. You'd still be left with the coordination between React and whatever else was handling these concerns, but with much more code likely to tie them together.
The disconnect here is the definition of "concerns."
One analogy is, imagine cutting a square into stripes. You're implying that there's only one way to do so, say, cutting horizontally. But you can easily cut vertically and still end up with well separated stripes.
The stripes, whether horizontal or vertical, are separated cleanly.
While you may see splitting by data fetching, view, etc. as the only way to separate by concerns, there are certainly others. The component model that has been adopted by many React users is one such way.
Unless you want a mess, usually you still separate by both. Just because you "cut" a square vertically or horizontally, doesn't mean each strip doesn't have, say, more separating lines that you didn't cut. These concerns should still be separated, even if they aren't separated at the highest levels of your application.
At the top level, we slice our projects vertically (by feature) at work. But it doesn't mean we have 1 giant god framework that handles all layers for a given feature. We still segment by layer. And we still have layer focused libraries.
> If your app is architected to have any rendering operation take long enough that it's perceptible to the user, STOP DOING THAT THING DURING RENDERING.
As is tradition, frontend developers are still slowly reinventing the techniques from other fields.
The neat thing about UI is that your end user is a human and a human can't visually process too many items at once on a screen. This means you should never really need to consider more than O(thousands) elements to render either. That should always fit under 16ms.
This has been known to game developers since forever. Don't spend resources on things that won't be displayed. If you are doing too much work that means you aren't culling invisible objects or approximating things at a lower level of detail.
In practice for frontend developers, this just means using stuff like react-virtualized so that even if you have a list of a bajillion objects, only the stuff in the viewport is rendered.
I don't think there's any slow reinvention going on in the way you're describing -- the adoption of ideas from (for example) game development has been a total non-secret since the dawn of the React age. There's definitely slow reimplementation going on though, and unfortunately it's having to be done at the scripting level, rather than the engine level.
Yea you're right, I guess there is a distinction between reinventing and reimplementing. The authors even mentioned that concurrent mode is inspired by "double buffering" from graphics.
I am just trying to support the grandparent comment that the current optimization is at the wrong level of abstraction. React is at the level of a "scene graph" (dom). If it's slow, the first technique to reach for should be stuff like "occlusion culling".
That's largely analogous to what people attempt to do with virtualisation, but the ability of things to have unpredictable resizing and reflow behaviours throws a few spanners into the works. My understanding that (at least until recently, things might be different now) is that most game engine optimisations depend on being able to make a lot of assumptions that usually hold true, or being able to calculate most of your scene graph optimisations during a build step.
The browser gives you 16ms to execute JS if you want to avoid stutter. Even with the tightest, hand rolled, vanilla JavaScript there are plenty of situations where that isn't enough time, especially on low end devices.
Seperation of concerns is all good and well, but there are some product experiences you simply can't do with plain HTML.
And yet we have games that routinely do rendering, networking, state updates, and a bunch of other things in that same time frame. Even in JS, using HTML. Let's not automatically assume the runtime is at fault.
There's nothing inherently special about rendering. If the function is an unresolved promise then it should automatically await, and treat that component as blank or nonexistent. It's just legacy architecture to make render functions sync and blocking.
Do it at the top-level. React components shouldn't be managing their own data fetches or computation. They should take event handlers that indicate something has happened on the page, and then delegate that back up the tree until the App decides what to do with them. The App is then responsible for making the relevant fetches, massaging the data into a form suitable for display on the page, and updating the state. Components, to the greatest extent possible, should be stateless and react only to changes in props. If they do need state it should be for things local to the view (eg. the position of a slider, the currently selected tab, or the hidden/shown state of an expando).
Aside from keeping rendering lightweight and never needing to block, this architecture also:
1.) Lets the app batch up RPCs to minimize network traffic.
2.) Lets you easily swap out the network layer for mock data so you can develop the UI before the server is ready (often very important because backend dev tends to be slower than frontend).
3.) Lets you handle failures sensibly. Oftentimes a failed RPC requires that you make non-local changes to the UI (eg. display a butterbar, change page layout to not display a component, display an alternate component).
4.) Lets you easily add logging, tracing, and performance profiling capability to your network layer.
5.) Keeps your UI components reusable so they aren't coupled to a specific app or backend.
6.) Optimally handles cases where data (derived) from the fetch shows up in multiple places in the UI. One classic example of this is a table where the total count of results is in a UI element elsewhere on the page; another is when logging in changes the elements you're shown.
Requires a lot of coordination code, duplication, and coupling of your entire component hierarchy to each other no?
Still doesn't solve disparities in data loading completion across different services. Render giant loading spinner or the entire app?
Plus, you will be overfetching if everything is pulled into top level components.
For exactly the reason componentizing the markup was a good idea, it is beneficial to breakup data loading and manipulation concerns across components, while also being able to reuse some of this logic through abstractions (hooks).
>Keeps your UI components reusable so they aren't coupled to a specific app or backend.
They are coupled to each other by virtue of where you define them in the hierarchy and which props they need to pass to and receive from each other.
> Requires a lot of coordination code, duplication, and coupling of your entire component hierarchy to each other no?
Only if we tunnel-vision to a narrow definition of "idiomatic patterns in React". In Angular, for example, this is solved via a DI system. In React, you actually get much higher level of coupling because all orchestration-type things need to go through context providers (meaning a given component may only work if certain providers are installed at the top level of the React tree). In Angular, a coupling mismatch issue would throw an error saying X component requires Y dependency and the solution would be to simply inject what is missing. No need to go edit your layout file or whatever.
Definitely, if you want to say that another framework itself better solves all of the very real problems that need solving... makes perfect sense.
The coupling I'm mainly referring to is within your own app hierarchy. Moving/adding components around within your app requires editing the long chain of component hierarchies and data dependencies.
Yes injection works to solve, but isn't this similar to hooks (encapsulating reusable logic/data fetching)? Whether a top level Context provider (that will also throw an error if not provided) or injection.. is there much of a difference? And does this not seem less coupled than prop drilling through the entire component chain?
In the context of a discussion about UX, loading states, spinners, concurrent rendering though, does Angular address this? I'm not that familiar with it..
Not being able to move your components without editing your component hierarchy is a code smell. Data should be bound at the level it is consumed. "Prop drilling" is an anti-pattern. Redux is the most common way to solve this problem but you can use anything built on top of the context API.
Not to mention you can still keep your pure UI components separate from your components that have data fetching logic (aka containers).
Components are highly reusable.
Containers are coupled to specific API calls or logic.
I still default to doing as much data fetching on the top-level page component as possible and trickling data down. But if there's certain logic or data fetching I re-use in multiple places that don't need to block the whole page, I'll definitely refactor that into hooks + containers and delegate that data fetching lower in the tree.
Every one has reasons for their concerns, and complexity is something to be concerned of... but it seems most critics are arguing from a view which implies they don't support the "declarative component" model itself.
These added features are simply extending that paradigm in a natural way that supports better UX, within the constraints of the javascript language.
Nobody says they are easy problems to solve, but it's sort of a non-sequitur to argue alternatives that don't solve them (or don't absolve one of having to solve them still.)
Boilerplate code goes up, method signatures get duplicated more (though JS has easy varargs & spread operators, so this can be minimized), method logic is less duplicated, and coupling is decreased.
> Still doesn't solve disparities in data loading completion across different services. Render giant loading spinner or the entire app?
You have a choice depending on product concerns. You can render a single loading spinner for the app. You can render individual spinners for different loading components (just pass the 'loading' prop into each of them separately), but have them all resolve at the same time. Or you can issue separate RPCs for each, as if the component had made the request itself, and have each component re-render as the request comes back.
The nice thing is that this choice is not embedded within the component itself, so you could for example start out by rendering a single global spinner until a single global RPC comes back, and then as you build out the backend and separate calls into different RPCs, change component rendering to incrementally show the new data.
> Plus, you will be overfetching if everything is pulled into top level components.
Again, this is a product tradeoff. Do multiple components display data that changes when an event happens on one component? It makes sense to batch up the RPCs then, because they'll always be triggered together anyway. Or is each component displaying data that is manipulated only by that component? Then give it its own RPC, but you should still kick it off at the top-level and delegate up the tree so that you can change this behavior easily.
In my experience, UIs designed by frontend engineers tend to have lots of RPCs that are both kicked off by and only affect a single component, while UIs designed by UI designers and CEOs (and users themselves) tend to have a lot of non-local changes throughout the UI in response to an event in one component. This is a source of tension between these groups, but only one side is signing the paychecks.
Perhaps I don’t fully understand your use case, but would something like MobX solve the problem you describe? Keep your data in stores, components automatically react to changes in them. You could do the same with Redux but MobX gives you finer-gained rerendering (only components who’s data has actually changed rerender)
Redux already has ways to do fine-grained rerendering. (That's why redux has you give selector callbacks that operate on the state instead of just exposing the whole redux state to components. Redux only re-renders the whole component when a selector callback returns a new value.)
Yes, but don't do it from within your view. Fetch the data you need and provide it to the view.
The standard React application renders a few levels down, investigates the document location and matches against routes, renders a few more levels, discovers there is some necessary data missing, fetches it, renders a few more levels down, realizes there's more data missing, stops and waits for it, etc.
This isn't a failing of React per se, but rather of how people use it.
As an ignorant and neutral bystander, I’m really curious why this is downvoted and would be interested in reading the rebuttal (seems like I could learn something).
Because 4 or 5 years of real world React development have proven them inadequate suggestions.
React itself was created in opposition to arbitrary separation of concerns dogma. The concern is the component, the entity encompassing both UI and data manipulation code, as well as styling. Separating all of these parts into isolation does not result in more maintainable code or better UX.
Components have always had JSX markup along with data manipulation logic. CSS in JS and styling co-located with component extended this consolidation of cross-cutting concerns.
These, along with not manually updating DOM state as you mention, all fall under declarative aspects of React.. which concurrent rendering and Suspense are consistent with and essentially an extension of.
React isnt the one mandating how its used. It can be used in the way that the GP is talking about, but you can also keep the data entirely separate. Redux et al do exactly that. The main thing that the GP is missing is that, like with video games, in order to keep the view rendering smoothly, you need something like double buffering so that if the render isnt complete, you dont block. That is independent of how the data is being provided.
But the usual way of building things is to treat React as a total web app framework. The terminology is very confused: they still call it "rendering", even though it now usually includes data fetching, complex manipulations, event processing, etc. You just `React.render` once when your page first loads and then child components take care of the rest. It should really be called `React.start`.
> The reason for the stutter is simple: once rendering begins, it can’t be interrupted.
Rendering should never need to be interrupted. If your app is architected to have any rendering operation take long enough that it's perceptible to the user, STOP DOING THAT THING DURING RENDERING.
Want a performant web app? Apply separation of concerns properly, and limit your use of React to the view layer.