
React writing patterns that occur in business
This page has been translated by machine translation. View original
In our work, we sometimes face difficult challenges. At such times, we have no choice but to rack our brains, writing code based on faint clues from the corners of our memory, or meticulously reading documentation from start to finish. Once we overcome these challenges, we experience an indescribable sense of accomplishment.
This time, I'd like to summarize some React patterns I happened to encounter in my work.
Passing context values to Server Components using Children
We often need to conditionally display content based on context information. The code below would successfully display different charts according to plan information if all components were Client Components.
export const DashboardPage = () => {
const plan = useContext(PlanContext);
return (
<Section>
{plan.personal === true && <PersonalPlanChart />}
{plan.pro === true && <ProPlanChart />}
</Section>
)
}
However, with Server Components in the mix, we can't use this code as is.
DashboardPage uses useContext, so it can't be a Server Component.
If DashboardPage becomes a Client Component, then Chart components can't be Server Components.
This makes it difficult to satisfy requirements like:
- PersonalPlanChart and ProPlanChart fetch data and should be Server Components
- We need to use Context information to conditionally render components
Here's a solution. We implement it like this:
- Call a Wrapper (Client Component) from DashboardPage (Server Component) to use Context
- Pass Chart components (Server Components) as children to the Wrapper (Client Component)
- Use React.Children in the Wrapper (Client Component) to handle conditional rendering
This might be hard to explain in words, but the implementation code should make it clear:
// page.tsx
export const DashboardPage = async ({ ... }: DashboardPageProps) => {
return (
<DashboardClientWrapper>
<Section data-type='personal'>
<PersonalPlanChart />
</Section>
<Section data-type='pro'>
<ProPlanChart />
</Section>
</DashboardClientWrapper>
)
}
// client.tsx
'use client'
export const DashboardClientWrapper = ({ children }: { children: ReactNode }) => {
const plan = useContext(PlanContext);
return (
<Section>
{Children.map(children, (child) => {
if (isValidElement<{ children: ReactElement; 'data-type': string }>(child)) {
const type = child.props['data-type']
if (type === 'personal' && plan.personal === true) return child
if (type === 'pro' && plan.pro === true) return child
}
return null
})}
</Section>
)
}
The DashboardPage implementation is straightforward. It simply places each Chart component inside a Section with a data-type attribute.
The complex part is the DashboardClientWrapper. It does the following:
- Retrieves plan information using useContext
- Uses React API Children.map to manipulate each element passed as children
- Verifies it's a valid ReactElement using isValidElement
- Identifies the component type using the data-type attribute
- Returns the appropriate ReactElement if the context value is true
This approach works around the limitation that Client Components can't directly place Server Components as children. To do this, we call a Client Component from a Server Component and pass Server Components as children.
Note that as mentioned in the React official documentation, using the Children API is not recommended. Along with isValidElement, these are treated as legacy APIs, and their use in new code is discouraged. Use them only as escape hatches.
Using Children is uncommon and can lead to fragile code. See common alternatives.
https://ja.react.dev/reference/react/Children
Recommended approach
You can achieve the same result without using the Children API by explicitly passing each content as props.
This is recommended when there are no constraints:
// page.tsx
export const DashboardPage = async ({ ... }: DashboardPageProps) => {
return (
<DashboardClientWrapper
personalContent={<PersonalPlanChart />}
proContent={<ProPlanChart />}
/>
)
}
// client.tsx
'use client'
export const DashboardClientWrapper = ({
personalContent,
proContent
}: {
personalContent: ReactNode;
proContent: ReactNode;
}) => {
const plan = useContext(PlanContext);
return (
<Section>
{plan.personal && personalContent}
{plan.pro && proContent}
</Section>
)
}
This implementation approach has the following benefits:
- Explicit props allow TypeScript type checking
- The code clearly shows which content will be displayed
- No dependency on data attributes or Children API, making it easier to write
Using useIsSSR to determine server-side rendering
When manipulating the DOM in Client Components as shown below, errors can occur:
'use client'
export const TableOfContentList = ({
html,
}: {
html: string
}) => {
let headings: Element[] = useMemo(() => [], [])
const dom = new DOMParser().parseFromString(html, 'text/html')
headings = Array.from(dom.querySelectorAll('h2, h3'))
You'll encounter a reference error like this, indicating it's being run in a Node.js environment:
⨯ ReferenceError: DOMParser is not defined
at ...
26 | })
27 |
> 28 | const dom = new DOMParser().parseFromString(html, 'text/html')
| ^
29 | const h = Array.from(dom.querySelectorAll('h2, h3'))
This happens because Next.js also executes on the server during the initial rendering. The error occurs because DOMParser doesn't exist in the Node.js environment.
The simplest solution is to use it inside useEffect:
'use client'
export const TableOfContentList = ({
html,
}: {
html: string
}) => {
const [headings, setHeadings] = useState<Element[]>([])
useEffect(() => {
const dom = new DOMParser().parseFromString(html, 'text/html')
setHeadings(Array.from(dom.querySelectorAll('h2, h3')))
}, [html])
A bit more verbose, but managing a state that indicates whether useEffect has fired (i.e., the component is mounted) can sometimes be more semantically clear:
'use client'
export const TableOfContentList = ({
html,
}: {
html: string
}) => {
const [mount, setMount] = useState(false)
let headings: Element[] = []
if (mount) {
const dom = new DOMParser().parseFromString(html, 'text/html')
headings = Array.from(dom.querySelectorAll('h2, h3'))
}
useEffect(() => {
setMount(true)
}, [])
This works fine for small components where you can grasp the whole picture, but React best practices encourage reducing unnecessary Effects. Semantically, it's better to guarantee that we're not executing during SSR. This is where the useIsSSR Hook comes in.
I encountered this implementation in the React Aria package. According to the official description, useIsSSR "delays browser-specific rendering until after hydration."
Returns whether the component is currently being server side rendered or hydrated on the client. Can be used to delay browser-specific rendering until after hydration.
https://react-spectrum.adobe.com/react-aria/useIsSSR.htm
True to its name, it returns true during SSR, allowing for the following implementation. This is semantically cleaner and eliminates Effects from the code, making it easier to understand:
'use client'
import { useIsSSR } from 'react-aria'
export const TableOfContentList = ({
html,
}: {
html: string
}) => {
const isSSR = useIsSSR()
let headings: Element[] = []
if (!isSSR) {
const dom = new DOMParser().parseFromString(html, 'text/html')
headings = Array.from(dom.querySelectorAll('h2, h3'))
}
useIsSSR implementation
The code looks like this:
Explaining the React 18+ part (before the context part), useSyncExternalStore takes three arguments:
- subscribe: A function that begins subscribing to the store and accepts a callback argument. (Abbreviated)
- getSnapshot: A function that returns a snapshot of the data in the store that the component needs. (Abbreviated)
- getServerSnapshot (optional): A function that returns the initial snapshot of the store data. (Abbreviated)
- https://ja.react.dev/reference/react/useSyncExternalStore#parameters
The second argument is a function called during normal component access to get store data, while the third is only called during SSR or hydration.
In other words, we can determine it's SSR only when the third function is called. useIsSSR uses this to return fixed boolean values from the snapshot functions.
Render hooks
The origin of this pattern is probably here. It's a design where React Custom Hooks return Components.
【LINE SECURITIES FRONTEND】Providing components with custom hooks
I designed a button component that captures QR codes with a camera and retrieves values.
The render hook pattern does the following:
- Call Hooks to render a button
- Add a file input styled as a camera
- Execute handleFileChange when triggered
- Scan the file as a QR code
- Parse the value using the Zod Schema passed as an argument
- Store the parsed value
- Use the value returned by Hooks for processing
export const useScan = <Schema extends z.ZodType>(
schema: Schema,
) => {
const [loading, setLoading] = useState(false);
const [value, setValue] = useState<z.infer<Schema> | null>(null);
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
const [file] = event.target.files ?? [];
const decoded = await decodeQrFromFile(file);
const parsed = schema.parse(JSON.parse(decoded));
setValue(parsed);
};
const button = () => {
return (
<label className="flex cursor-pointer" aria-disabled={loading}>
<input
type="file"
disabled={loading}
accept="image/*"
capture="environment"
className="sr-only"
onChange={handleFileChange}
/>
<Camera className="size-1/2" />
</label>
);
};
return { button, value };
};
A straightforward implementation might have ScanButton receive a handler as a prop. However, this requires state management and handler preparation in the calling component:
// scan-page.tsx
export const ScanPage = () => {
const [value, setValue] = useState()
const [loading, setLoading] = useState(false)
// Data retrieval, validation, etc.
const handler = () => {}
return (
<Section>
<ScanButton handler={handler} loading={loading} />
</Section>
)
}
// scan-button.tsx
interface Props {
handler: () => {}
}
export const ScanButton = ({handler}: Props) => {
return (
<label className="flex cursor-pointer" aria-disabled={loading}>
<input
type="file"
disabled={loading}
accept="image/*"
capture="environment"
className="sr-only"
onChange={handler}
/>
<Camera className="size-1/2" />
</label>
);
}
Passing down server-retrieved values as props is fine since those props don't change, maintaining readability. However, managing state for child components in the parent becomes cumbersome.
With the render hooks pattern, state can be managed where it's used, while also encapsulating common logic.
If you find yourself in a similar situation, please consider using this pattern.
Implementing modals in Next.js
This is more of a reminder for myself than an implementation pattern, as I often forget it.
To implement modals in Next.js, we combine Parallel Routes and Intercepting Routes. Let's first understand what these are.
Parallel Routes
Parallel Routes allow creating Slots to pass multiple children to a Layout.
Slots are not Route Segments, so they don't affect URL routing.
Normally, children are passed to a Layout, but Parallel Routes allow passing multiple ReactNodes separately.
Directories are defined with the @ prefix. For example, /app/greeting/@modals can be displayed in the Layout:
export default function GreetingLayout(
// For /greeting/@modals, route is /greeting
props: LayoutProps<"/greeting">,
) {
return (
<div>
{props.children}
{props.modals}
</div>
);
}
Also, defining default.tsx provides a fallback when the Slot has no content:
// @modals/default.tsx
export default function Default() {
return null;
}
Intercepting Routes
Intercepting Routes allow displaying content from another page while maintaining the current layout during Soft Navigation (client-side page transitions). For Hard Navigation (direct URL access or page reload), interception doesn't occur and the normal page is displayed.
With the following directory structure, interception occurs during Soft Navigation from /greeting to /greeting/[code]:
/app/greeting
├── [code]
│ └── page.tsx
├── @modals
│ ├── (.)[code]
│ │ └── page.tsx
│ └── default.tsx
├── layout.tsx
└── page.tsx
In this example, (.)[code] matches the same segment, so transitions from /greeting match (@modals doesn't affect segmentation, so its parent is targeted).
If we used (..)[code], it would match transitions from one level up, i.e., /.
Intercepting Routes use notation with (..). They look a bit like alien faces:
(.)matches the same level segment(..)matches one level up(..)(..)matches two levels up(...)matches the root app directory segment
Combining Parallel Routes and Intercepting Routes
By combining these features, we can create a system that displays modals according to the navigation type.
For Soft Navigation
- The modal appears in the modals slot while maintaining the current layout
@modals/(.)[code]/page.tsxis rendered- The URL changes to
/greeting/[code], but background content remains/greeting
For Hard Navigation
- Interception doesn't occur, resulting in normal page transition
- The
@modalsslot is empty, and@modals/default.tsxis displayed - With default.tsx content as null, no modal appears
[code]/page.tsxis displayed normally, full-screen
The Layout implementation looks like this:
export default function GreetingLayout(
props: LayoutProps<"/greeting">,
) {
return (
<div>
{props.children}
{props.modals}
</div>
);
}
Because of this structure, children passed as modals need to use position: fixed or position: absolute for overlay display. Otherwise, they'll appear in the normal vertical flow.
Modal component implementation example
Here's an implementation example using Shadcn UI:
- Set defaultOpen and open to true to display the modal in an open state
- Call router.back() on onOpenChange to return to the previous page when closing the modal
- This enables the browser's back button to also close the modal
"use client";
import { useRouter } from "next/navigation";
import {
Dialog,
DialogContent,
DialogOverlay,
DialogTitle,
} from "@/features/ui/components/dialog";
export const GreetingDialog = () => {
const router = useRouter();
const handleOpenChange = () => {
router.back();
};
return (
<Dialog defaultOpen={true} open={true} onOpenChange={handleOpenChange}>
<DialogOverlay>
<DialogContent className="overflow-y-hidden">
<DialogTitle>Greeting</DialogTitle>
</DialogContent>
</DialogOverlay>
</Dialog>
);
};
This creates a nice UI with modals that open from page access while maintaining shareable URLs.
Conclusion
There are many more topics I'd like to cover, such as the existing useImperativeHandle, various features added in React 19.2, Next.js 16, but I've limited this to patterns I've encountered in my work. Have you found any interesting patterns recently? If so, please share them.

