React 18 features
What's new in React 18
Concurrent React
Many of the features in React 18 are built on top of the brand-new mechanism – concurrent renderer, that allows to interrupt rendering. It is increase performance of the client rendering overall.
Worth noting, that concurrent rendering automatically enables only for parts, those using it. That's why migration to React 18 will not be a problem.
Batching updates
Batching is the process of grouping multiple updates into one single render. Previously updates inside of promises, setTimeout or native eventHandlers were not batched by default, but now such updates will be batched automatically.
Also, if you are using unstable_batchedUpdates
you can stop using it safely.
You can see code examples here.
Transitions
Transition is a new concept in React to distinguish between urgent and non-urgent updates:
- urgent updates - direct interaction, like typing, clicking, pressing, and so on;
- non-urgent updates - transition the UI from one view to another;
Urgent updates supposed to be handled immediately and show updated UI. In opposite, non-urgent updates are not showing every intermediate value on screen, but final result only. Such separation helps to cope with blocked user interactions due to non-urgent updates.
Any updates are urgent by default, but startTransition
and useTransition
let you mark some updates as not urgent. If an urgent
update will happen during a non-urgent
one React stop the current rendering and render only the latest update.
As far as you know, content wrapped in Suspense
can be suspended for different reasons. To recall:
- there is lazy component render, wrapped in
Suspense
; - there is data loading in a component, wrapped in
Suspense
. Tramvai does not support it yet, but you can look into Relay implementation, or origin RFC; - content, wrapped in
Suspense
is waiting for corresponding CSS (currently in development by React team);
So, if a component will be suspended during non-urgent update React will prevent already-visible content from being replaced by a fallback. Instead, React will delay render until data has loaded.
To recap: any update of the state (const [, setState] = useState(true)
), wrapped in startTransition
will be considered as non-urgent and will not block user input.
Suspense
Suspense
lets to display fallback
content for the parts of UI, that not ready to be displayed yet. In Tramvai it is suitable only for lazy components at the moment.
As far as you know Tramvai ships its own way for lazy code loading – @tramvai/lazy
. In comparison to classic React.lazy
, Tramvai version has some advantages:
- it optimized for SSR;
- code of a component loads on the client before hydration;
That's why we recommend using only @tramvai/lazy
in Tramvai apps.
Note, that all the content, wrapped in Suspense
, even if it not suspends by self reasons, will be suspended.
Also, Suspense
usage enables selective hydration. We have a guide about it already. Check it out!
In addition, Suspense
allows to intercept server-side and hydration errors.
Strict mode
In order for components to work properly in the future, a new behavior has been added to StrictMode
called StrictEffects
. The idea is to catch effects that don't work properly during development by React mounting the component twice, i.e.: mount => unmount => mount
, calling corresponding effects and unsubscribe functions. It works only in DEV environment. You can enable it such way.
Tramvai integration
When you are using React 18, several features will be allowed to use:
Streaming rendering
We have an article about streaming rendering, check it out.
Selective hydration
hydrateRoot
wraps in startTransition
on the client automatically. See more.
SPA-navigations with startTransition
We have experimental support for concurrent rendering with SPA-transitions - all navigations will be wrapped in startTransition
automatically, and while rendering the next screen, the page will be responsive because the rendering process will be interruptible.
To enable this feature, you need to provide experiments.reactTransitions
to tramvai.json
config:
{
"experiments": {
"reactTransitions": true
}
}
:::warn
reactTransitions
is incompatible with direct router
store usage in React components.
Bad:
const route = useStore(RouterStore);
const route = useSelector('router', (state) => state.router);
Good:
const route = useRoute();
const route = pageService.getCurrentRoute();
:::
Suspense in Child App
Each Child App wraps into Suspense
automatically.
Tramvai use cases
Suspense
Use cases
- displaying a fallback UI, during
lazy
component load - handling server-side errors;
Look to the code example here.
startTransition/useTransition
Mark a state update inside it as non-urgent
, that allows to not block user input during subsequent updates urgent
updates.
Outside the components you should use startTransition
function, instead of useTransition
hook.
Use cases
- wrap a navigation between tabs, when you have heavy to render tab components;
- don't show fallback UI for suspended components;
Take a look to the example. Try to switch to the Slow
tab, and then to the Another
tab immediately. Navigation will happen straight away, despite slow rendering of the Slow
tab. Also, there is no loading state for suspended content, when click to the Switch
button.
useId
Hook to generate same identifiers on server and client. Without it, if you will do it by yourself, e.g. just calling uuid
, result values will be different on server and client. Also, you can't use simple counter (nextId++
) for it, because React does not guarantee the order in which the client components are hydrated. Don't use it for generating keys in a list.
There is an example in our repo. Run the app, go to the /hooks
and look to the console. There is a hydration error for the WithoutHook
component, and no such error for WithHook
.
useDeferredValue
Lets you defer re-rendering a non-urgent part of the UI.
Look at example.
How it works?
There are two steps for it:
- First, when the update happens, React makes a render with updated main state, but with not updated deferred state;
- Then in background, React tries to re-render with both main and deferred states updated. However, if it suspends or a new state update received, React will cancel the background render and retry it with a new value.
Use cases
- defer the rendering of a heavy UI;
- don't show
Suspense
fallback for suspended components;
Run the example app and go to the /hooks
. Note, that user input does not block by the heavy results render.
Difference from debounce/throttling
useDeferredValue
:
- Designed for the rendering optimizations and integrated with React deeply;
- There is no fixed time delay, so React will adjust to the user;
- Can abandon the rendering, by contrast
throttle
anddebounce
just postpone the moment when rendering blocks.
However, throttle
and debounce
are still useful. For example, they can let you fire fewer network requests.
Error handling
Given the new features, handling rendering errors in React 18 looks like this:
- If an error happens during SSR, then instead of throwing it to the whole application, React will find nearest
Suspense
boundary up to tree and include it fallback to the server response. - After that, on the client, React determines what could not be rendered and starts a client-side render from scratch for these parts.
- If the client render was successful, React will render it, otherwise it will throw an error that can be caught by the
Error Boundary
.
Note, that same logic applies to the hydration errors, that is React discard server results to nearest Suspense
boundary and renders in on the client from scratch.
That's why we are strongly recommend to wrap in Suspense
boundary important parts of your application to improve performance. You can use the next structure as a reference:
<ErrorBoundary fallback>
<Suspense fallback>
<Component />
</Suspense>
</ErrorBoundary>
Note, that Tramvai add ErrorBoundary by default. See more.
Also, Tramvai log the rendering errors and deduplicate them, to avoid noise. In general, we are recommend do not ignore hydration errors, because they are affect performance. As the last resort you can use suppressHydrationWarning={true}
on the React component, e.g. to display time.