In this blog post, I would like to address an issue I encountered in this week. This is the first time I met this kind of problem using React and first time know the startTransition
of React.
How it comes
This error related to the ReactMarkdown
component and the useDisclosure
hook in the @chakra-ui/react
.
<ReactMarkdown children={children}>
components={{
code({children, ...props }) {
// ...
return (
// some rendering based on the children, such as
// <code>{children}</code>
// <SyntaxHighlighter>{String(children)}SyntaxHighlighter>
// <Preview children={Array.isArray(children) ? children : [children]} ></Preview>
)
}
</ReactMarkdown>
const { isOpen, onToggle } = useDisclosure();
Here is the original workflow:
a button is toggled -> useDisclosure catch the changes -> ReactMarkdown's updates rendering (based on the isOpen
status)
<Button onClick={() => onToggle()}>
{isOpen ? "Show Less" : "Show More..."}
</Button>
<ReactMarkdown>
{isOpen ? veryLongText : shortText }
</ReactMarkdown>
Error occurs
Now when we have a very very long string veryLongText
, the update will gives you the error:
A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.
This indicates that a component has paused or stopped functioning while it was trying to handle synchronous input. Synchronous input refers to actions or events that are processed immediately, without waiting for other tasks to complete.
It suggests that we can wrap the updates or changes that cause the suspension of the component with a function called startTransition
How to solve
Here is the updated to solve the error by using startTransition
const { isOpen, onToggle: originalOnToggle } = useDisclosure();
const onToggle = () => {
startTransition(() => {
originalOnToggle();
});
};
// no changes to Button and ReactMarkdown code
<Button onClick={() => onToggle()}>
{isOpen ? "Show Less" : "Show More..."}
</Button>
<ReactMarkdown>
{isOpen ? veryLongText : shortText }
</ReactMarkdown>
We use the startTransition
function telling React to optimize and batch updates. This means that when the button is clicked, and the onToggle
function is called, the state update triggered by originalOnToggle()
is treated as a low-priority update. Here is the full diff
if you are interested:
diff --git a/src/components/Message/MessageBase.tsx b/src/components/Message/MessageBase.tsx
index 25b83d82..f7f27042 100644
--- a/src/components/Message/MessageBase.tsx
+++ b/src/components/Message/MessageBase.tsx
@@ -1,5 +1,6 @@
import {
memo,
+ startTransition,
useCallback,
useState,
useEffect,
@@ -33,6 +34,7 @@ import {
useClipboard,
Kbd,
Spacer,
+ useDisclosure,
} from "@chakra-ui/react";
import ResizeTextarea from "react-textarea-autosize";
import { TbDots, TbTrash } from "react-icons/tb";
@@ -111,6 +113,17 @@ function MessageBase({
const isNarrowScreen = useMobileBreakpoint();
const messageForm = useRef<HTMLFormElement>(null);
const meta = useMemo(getMetaKey, []);
+ const { isOpen, onToggle: originalOnToggle } = useDisclosure();
+ const isLongMessage = text.length > 5000;
+ const displaySummaryText = !isOpen && (summaryText || isLongMessage);
+
+ // Wrap the onToggle function with startTransition, state update should be deferred due to long message
+ // https://reactjs.org/docs/error-decoder.html?invariant=426
+ const onToggle = () => {
+ startTransition(() => {
+ originalOnToggle();
+ });
+ };
useEffect(() => {
if (settings.countTokens) {
@@ -334,6 +347,14 @@ function MessageBase({
<CardBody p={0}>
<Flex direction="column" gap={3}>
<Box maxWidth="100%" minH="2em" overflow="hidden" px={6} pb={2}>
+ {
+ // only display the button before message if the message is too long
+ isLongMessage ? (
+ <Button size="sm" variant="ghost" onClick={() => onToggle()}>
+ {isOpen ? "Show Less" : "Show More..."}
+ </Button>
+ ) : undefined
+ }
{editing ? (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<form onSubmit={handleSubmit} ref={messageForm} onKeyDown={handleKeyDown}>
@@ -381,8 +402,12 @@ function MessageBase({
</VStack>
</form>
) : (
- <Markdown previewCode={!hidePreviews} isLoading={isLoading} onPrompt={onPrompt}>
- {summaryText || text}
+ <Markdown
+ previewCode={!hidePreviews && !displaySummaryText}
+ isLoading={isLoading}
+ onPrompt={onPrompt}
+ >
+ {displaySummaryText ? summaryText || text.slice(0, 250).trim() : text}
</Markdown>
)}
</Box>
The startTransition
function is specifically designed to work with the React Concurrent Mode (or just Concurrent Mode), which is an experimental set of features in React for improving the performance and user experience of complex user interfaces. I'm very glad to know this feature. Hope it helps. See you next post!
Top comments (0)