Writing styles for React that occurred in business

Writing styles for React that occurred in business

I have compiled implementation patterns for React that I encountered in my work. I will explain how to make Server Components coexist with Context API, how to determine SSR with useIsSSR, the render hooks pattern, and modal implementation in Next.js.
2025.10.15

In my work, I sometimes face difficult requirements. At those times, I have no choice but to wrack my brain, piecing together code based on faint clues from memory, or carefully reading documentation from start to finish. After overcoming such challenges, I experience an indescribable sense of achievement.

Today, I'll summarize some React patterns I recently encountered at work.

Passing context values to Server Components using Children

We often need to conditionally render content based on context information. The code below works fine when all components are Client Components, displaying charts according to plan information.

export const DashboardPage = () => {
  const plan = useContext(PlanContext);

  return (
    <Section>
      {plan.personal === true && <PersonalPlanChart />}
      {plan.pro === true && <ProPlanChart />}
    </Section>
  )
}

However, when working with Server Components, we can't use this code as-is.
DashboardPage uses useContext, so it can't be a Server Component.
If we make DashboardPage a Client Component, we can't make Chart a Server Component.
This makes it difficult to meet the following requirements:

  • 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 can implement it this way:

  • DashboardPage (Server Component) calls a Wrapper (Client Component) to use Context
    • Pass Charts (Server Components) as children to the Wrapper (Client Component)
  • The Wrapper (Client Component) uses React.Children 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. We're just placing each Chart component inside a Section with a data-type.

The complex part is DashboardClientWrapper. It does the following:

  1. Gets plan information using useContext
  2. Uses React's Children.map API to manipulate each child element
  3. Checks if it's a valid ReactElement using isValidElement
  4. Identifies which component it is using the data-type attribute
  5. Returns the appropriate ReactElement if the Context value is true

This workaround bypasses the constraint that Client Components can't directly include Server Components as children. We call a Client Component from a Server Component and pass Server Components as children.

Note that as mentioned in the React documentation, using the Children API is not recommended. Both Children and isValidElement are considered legacy APIs and are not recommended for new code. Use them only as an escape hatch.

Using Children is uncommon and can lead to fragile code. See common alternatives.
https://ja.react.dev/reference/react/Children

Recommended Approach

We can express this without using the Children API by explicitly passing each content as props.
This is the recommended approach 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 enable TypeScript type checking
  • The code clearly shows which content will be displayed
  • It doesn't rely on data attributes or the Children API, making it easier to write

Using useIsSSR to determine server-side rendering

When manipulating the DOM in Client Components like this, 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 get a reference error like this, indicating it's running 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 in Next.js, the initial render also runs on the server. 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])

It might be a bit verbose, but managing a state that indicates the component has mounted (through useEffect firing) can be semantically clearer in some cases:

'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 when the codebase is small and you can see the whole picture, but React best practice is to reduce unnecessary Effects. Semantically, it's better to guarantee that we're not running in 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

As the name suggests, it returns true during SSR, allowing for this cleaner implementation. The semantics are clearer, and we've eliminated the Effect from our code, making it easier to follow:

'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:

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/ssr/src/SSRProvider.tsx#L189-L197

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. (Omitted)
  • getSnapshot: A function that returns a snapshot of the data in the store that the component needs. (Omitted)
  • Optional getServerSnapshot: A function that returns the initial snapshot of the store data. (Omitted)
  • https://ja.react.dev/reference/react/useSyncExternalStore#parameters

The second argument is a function that's called when the component accesses the store's data normally, and the third argument is a function that's called only during SSR or hydration.

This means we can determine it's SSR only when the third function is called. useIsSSR uses this to return fixed boolean values from these snapshot functions.

Render Hooks

This pattern likely originated here: LINE Securities FrontEnd: Providing Components with Custom Hooks

I designed a button component that can scan QR codes using the camera and retrieve values.
Here's what the render hook does:

  1. Call the hook to render a button
  2. Add a file input styled as a camera
  3. Execute handleFileChange when triggered
    1. Scan the file as a QR code
    2. Parse the value using the Zod Schema passed as an argument
    3. Store the parsed value as a state
  4. Use the value returned from the hook for further 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 };
};

With a straightforward implementation, you could make ScanButton reusable by accepting a handler. However, this requires the parent component to manage state and prepare handlers:

// scan-page.tsx
export const ScanPage = () => {
  const [value, setValue] = useState()
  const [loading, setLoading] = useState(false)
  // Data fetching, 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 server-fetched values as props is fine since they don't change, but managing state in the parent that the child uses can get messy.
The render hooks pattern allows state management in the hook itself, keeping it where it's actually used, while also encapsulating common logic.

If you find yourself in a similar situation, consider using this pattern.

Implementing Modals in Next.js

This is more of a personal note than an implementation pattern, as I often forget how to do this.

To implement modals in Next.js, we combine Parallel Routes and Intercepting Routes. Let's clarify these features first.

Parallel Routes

Parallel Routes allow you to create Slots and pass multiple children to a Layout.

Slots don't affect URL routing because they're not Route Segments.
Normally, a Layout receives children, but with Parallel Routes, you can pass multiple ReactNodes separately.

Directories are defined using 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>
  );
}

You can also define default.tsx as a fallback when the Slot has no content:

// @modals/default.tsx
export default function Default() {
  return null;
}

Intercepting Routes

Intercepting Routes let you display content from another page while maintaining the current layout during Soft Navigation (client-side page transitions). During Hard Navigation (direct URL access or page reload), the interception doesn't apply, and the normal page is displayed.

In 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] indicates the same segment, so transitions from /greeting will match (@modals doesn't affect the segment, so we look at the level above it).
If we used (..)[code], it would match transitions from one level up, which is /.

Intercepting Routes use (..) notation, which looks a bit like an alien face:

  • (.) 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 based on the type of navigation:

For Soft Navigation

  • A modal is displayed in the modals slot while maintaining the current layout
  • @modals/(.)[code]/page.tsx is rendered
  • The URL changes to /greeting/[code], but the background content remains /greeting

For Hard Navigation

  • The interception doesn't apply, and normal page navigation occurs
  • The @modals slot is empty, and @modals/default.tsx is displayed
  • Setting default.tsx content to null prevents the modal from showing
  • [code]/page.tsx is 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, the children passed as modals need to use position: fixed or position: absolute for overlay display. Otherwise, they'll just stack vertically.

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() in onOpenChange to return to the previous page when closing the modal
  • This ensures the modal also closes when using the browser's back button
"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>
  );
};

With this, you have a nice UI that opens as a modal when accessed from a page, while maintaining a shareable URL.

Conclusion

There's much more I'd like to write about, including existing useImperativeHandle, various features added in React 19.2, Next.js 16, etc., but I've focused on patterns that appeared in my work for now. Have you tried any interesting patterns recently? If so, please share!

Share this article

FacebookHatena blogX

Related articles