React 18: Concurrent Render Magic

  • Concurrent Rendering: Rendering is no longer a synchronous process. React can interrupt rendering between individual component renders to process events. If these events result in state updates, the course of rendering may be altered — in some cases aborting the rest of the render or yielding to render more important updates.
  • Transitions: Transitions are the key to using concurrent rendering. In a transition state updates are marked as non-urgent. These updates trigger non-urgent renders for dependent components, readying content for the transition. Meanwhile the same components may also be re-rendered without the non-urgent updates so more urgent updates can be shown. In effect components have a “dual life” during the transition.
  • State Branching: The ability to maintain multiple versions of state is what enables this “dual life”. React maintains the old and new values of state that is part of the transition.
  • Suspense: Allows components to “suspend” and display fall-back content while a suspense-compatible API is fetching data. If the suspended components are part of a transition, the transition is extended while the component is suspended and components continues to render on-screen without the state update, allowing the previous version of content to remain on the screen and more urgent updates to be reflected.
  • Responsive UI — You have a complex component tree. It renders slow enough that it may be out-of-date before rendering is complete because the user signals further changes in an input field or slider. Previously you waited for the render of all components to complete before updating the input element. With concurrent rendering you can ensure the input is immediately responsive to changes and that only the final render is visible.
  • Orderly Transitions— When a screen transition occurs that causes data to be fetched you may not wish to update the screen until the data is fetched and all elements of the transition are ready. This makes transitions smoother with less “flashing” of intermediate states.

Keeping Your UI Responsive

import {memoize, observable} from "proxily";const minX = 0;
const maxX = 400;
const minY = 0;
const maxY = 600;
class Forest {
color = "#239923";
treeCount = 70;
minSize = 50;
maxSize = 150;
get trees() {
console.log("Computing trees");
const trees : Array<any> = [];
for (let ix = 0; ix < this.treeCount; ++ix)
trees.push([
Math.random() * (maxX - minX) + minX,
Math.random() * (maxY - minY) + minY,
]);
console.log("reorg trees");
return trees;
}
}
memoize(Forest, f => f.trees);

export const state = observable(new Forest());
import { observer } from "proxily";
import { state } from "./state";
import Tree from "./Tree";

function Trees() {
return (
<>
{state.trees.map((t, ix) => (
<Tree n={ix} posX={t[0]} posY={t[1]} />
))}
</>
);
};

export default observer(Trees);
let [, startTransition] = useObservableTransition();
<RangeSlider
className="RangeSlider"
value={getCurrentValue(state, (state) => state.maxSize)}
min={110}
max={250}
size="lg"
onChange={(e: any) =>
startTransition(
() => (state.maxSize = parseInt(e.target.value, 10))
)
}
/>
  1. During the transition the Trees will render as a non-urgent render. The results won’t be shown on the screen until every Tree is rendered.
  2. The slider is also re-rendered during the transition. Since we used getCurrentValue so it will show the updated value and remain responsive.
  3. As sliding continues the size is updated again and a new transition starts. The rendering of trees is aborted and rendering starts with the new value.

Orderly Transitions

const MyContainer = ({contentId}) => {
<Suspense fallback={<Spinner />}>
<MyContent contentId/>
</Suspense>
}
function MyContent ({contentId}) {
const content = SuspenseCompatibleFetch(contentId);
return (<div>{content}</div>);
}
import { observable, suspendable } from "proxily";
import { fetchSentence } from "./sentences";
const sentence = {
seq: 0,
nextSentence(seq: number) {
return new Promise((r) =>
setTimeout(() => r(fetchSentence(seq)), 3000));
},
next: function () {
this.seq++;
}
};
suspendable(sentence, s => s.nextSentence);export const state = observable(sentence);
import { observer } from "proxily";
import { Suspense } from "react";
import Sentence from "./Sentence";

function ContentContainer () {
return (
<Suspense fallback={<p>Fetching next Sentence...</p>}>
<Sentence />
</Suspense>
);
};

export default observer(ContentContainer);
import { observer } from 'proxily';
import {state} from './state';

function Sentence () {
return <p>{state.nextSentence(state.seq)}</p>;
};

export default observer(Sentence);
import { observer } from "proxily";
import { state } from "./state";

function Controls() {
return (
<div>
<button onClick={() => state.next()}>Next Sentence</button>
</div>
);
};
export default observer(Controls);
import { observer, useObservableTransition } from "proxily";
import { state } from "./state";

function ControlsWithTransition() {
const [pending, startTransition] = useObservableTransition();
return (
<button disabled={pending}
onClick={() => startTransition(() => state.next())}>
Next Sentence
</button>
);
}

export default observer(ControlsWithTransition);

Summing it Up

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Sam Elsamman

Sam Elsamman

Retired technology entrepreneur who loves to hike, travel, cook and write music.