From c5ef5486a6c3eb767f2f1f652bb73d5e9f57354d Mon Sep 17 00:00:00 2001 From: Theodorus Clarence Date: Sun, 3 Mar 2024 15:10:52 +0700 Subject: [PATCH] feat(blog): add advanced react patterns --- src/contents/blog/advanced-react-patterns.mdx | 501 ++++++++++++++++++ src/pages/index.tsx | 2 +- 2 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 src/contents/blog/advanced-react-patterns.mdx diff --git a/src/contents/blog/advanced-react-patterns.mdx b/src/contents/blog/advanced-react-patterns.mdx new file mode 100644 index 00000000..06cca941 --- /dev/null +++ b/src/contents/blog/advanced-react-patterns.mdx @@ -0,0 +1,501 @@ +--- +title: 'Advanced React Patterns' +publishedAt: '2024-03-03' +description: 'List of react advanced patterns complete with examples.' +englishOnly: 'true' +banner: 'robert-horvick-1R4uPYipCFM-unsplash' +tags: 'nextjs,react' +--- + +## Introduction + +Honestly speaking, react patterns are kinda **weird**. The pattern that we use in React doesn't come as naturally. Which is why I created this article. My main goal is to let you know that **this pattern exists**. + +I can't give you an exact answer of when to use these patterns, because it differs case by case. Or as we might say: **It depends**. After you know that this pattern exists, my hope is when you get the specific use case you'll remember this article, and use it as one of the solutions. So bookmarking this article might be a great idea. + +Before you read this article, I want you to know that the example that I provided might be solved with another pattern. You can treat the examples as my way of showing you the syntaxes. At the end of the day, all roads lead to Rome. + +There are 4 patterns that I'll cover, you can skip ahead if you like: + +- [Component with Context](#component-with-context-pattern) +- [Function as Child](#function-as-child-pattern) +- [Forward Ref](#forward-ref-pattern) +- [Higher-Order Components](#higher-order-components-pattern) + +## Component with Context Pattern + +Components that share some UI states could benefit from react context. + +### Demo App + + + +Video description: + +- When the eye button is toggled, both the card title and subtitle change to bullets + +This is a pretty simple component, we have a `hidden` state that we need to share between the title and subtitle components. The first thing that comes to mind is to simply pass the props, but it might not be the best when the component grows. This is where component with context shines. + +### Creating a Context + +First, we need to create the context. Here's the basic boilerplate that you can use. + +```tsx title="hidable-card/context.tsx" showLineNumbers +import * as React from 'react'; + +type HidableCardContextType = { + hidden: boolean; + toggle: () => void; +}; +const HidableCardContext = React.createContext( + null +); + +export function HidableCardContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [hidden, setHidden] = React.useState(false); + + function toggle() { + setHidden((prev) => !prev); + } + + return ( + + {children} + + ); +} +export const useHidableCardContext = () => { + const context = React.useContext(HidableCardContext); + + if (!context) { + throw new Error( + 'useHidableCardContext must be used inside the HidableCardContextProvider' + ); + } + + return context; +}; +``` + +Note: + +- I like separating context declaration to another file since multiple child components might share it +- (line 11) Create a context provider for cleaner usage, and for declaring the states we are going to share +- (line 28) Lastly, create a custom hook for using the context. This is where you can add a checker if the component is inside the provider or not. + +### Composing the Context into a Component + +When creating the base component, we can directly integrate the context provider right inside it. + +```tsx title="hidable-card/index.tsx" /HidableCardContextProvider/ +export function HidableCard({ + className, + ...rest +}: Pick, 'className' | 'children'>) { + return ( + +
+ + ); +} +``` + +Then you can create the child components by utilizing the hook that we created + +```tsx title="hidable-card/index.tsx" /useHidableCardContext()/ +//#region //*=========== Title =========== +export function HidableCardTitle({ + className, + children, +}: Pick, 'className' | 'children'>) { + const { hidden } = useHidableCardContext(); + + return ( +

+ {hidden ? •••••••• : children} +

+ ); +} +//#endregion //*======== Title =========== + +//#region //*=========== Subtitle =========== +export function HidableCardSubtitle({ + className, + children, +}: Pick, 'className' | 'children'>) { + const { hidden } = useHidableCardContext(); + + return ( +

+ {hidden ? •••••••• : children} +

+ ); +} +//#endregion //*======== Subtitle =========== + +//#region //*=========== Hide Button =========== +export function HidableCardHideButton({ className }: { className?: string }) { + const { hidden, toggle } = useHidableCardContext(); + + return ( + + ); +} +//#endregion //*======== Hide Button =========== +``` + +What's nice about this is when you use `HidableCardTitle` outside of the base component, it will throw out an error since you don't have access to the context. Providing a safe developer experience. + +### Example Usage + +```tsx + +
+ Card Title + Card Subtitle +
+ +
+``` + +Pretty nice right? Make sure to prefix the child component with the base name so it is distinguishable. + +Here's the full [source code](https://react-patterns.thcl.dev/component-with-context) + +### Bonus: Compound Components + +At the end of the file, you can export it this way to make it into a compound component. + +```tsx +export const HidableCard = Object.assign(HidableCard, { + Title: HidableCardTitle, + Subtitle: HidableCardSubtitle, + HideButton: HidableCardHideButton, +}); +``` + +Then you can call it like so + +```tsx + +
+ Card Title + Card Subtitle +
+ +
+``` + +ps: this won't work in react server components. + +Compound components pattern is used by [Headless UI](https://headlessui.com/react/dialog). It's nice since we only need to import one component and get access to all the child components. However, it's not tree-shakeable. It's fine for Headless UI's case since you'll use all of the components anyway. + +--- + +## Function as Child Pattern + +This pattern exposes props and allows you to render custom UI to the component. A bit hard to explain in words, let's just see the example. + +### Demo App + + + +Video description + +- A dialog boolean state which is shown in the text (”Dialog is closed/open”) +- A button that when clicked will set the boolean to true and opens a dialog. + +### Example Usage + +```tsx {4} +export function GreetingDialogDemo() { + return ( + + {({ isOpen, openDialog }) => ( +
+
+ Dialog is{' '} + {isOpen ? ( + opened + ) : ( + closed + )} +
+ + +
+ )} +
+ ); +} +``` + +As you can see on line 4, the states inside are exposed to the children—where you can render anything you like based on the state. + +When dealing with this kind of behavior, it's natural to put the state outside of the component like so: + +```tsx {2} +export function UsualWayDemo() { + const [isOpen, setIsOpen] = React.useState(false); + + return ( + + {isOpen && ...custom ui here} + + ) +} +``` + +Well this works, but you now have something I like to call a **hidden convention**. + +A hidden convention is when you have to fulfill a requirement to use a component. Which in this case is declaring the `isOpen` state and passing it to the component. + +With function as a child pattern, the state lives inside the component itself, and can be accessed by the children. Amazing. + +### Implementation + +```tsx showLineNumbers {28} +'use client'; + +import * as React from 'react'; +import { PiHandWaving } from 'react-icons/pi'; + +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; + +type ReturnProps = { + isOpen: boolean; + openDialog: () => void; +}; + +export default function GreetingDialog({ + children, +}: { + children: (props: ReturnProps) => React.ReactNode; +}) { + const [open, setOpen] = React.useState(false); + + function openDialog() { + setOpen(true); + } + + return ( + + {/* If you're using radix primitives, you could actually just use {children} */} + {/* However, this pattern is useful if the children need to access states inside the component e.g. isOpen */} + {children({ openDialog, isOpen: open })} + + +
+ + Hello from a dialog! +
+
+
+ ); +} +``` + +ps: incompatible with react server components, you need to create a client wrapper + +Note: + +- The only special thing in the code is in line 28. Where instead of only returning `{children}`, we now return `children({props})` +- For the children type, you need to change it into a function that returns `ReactNode` + +Here's the full [source code](https://react-patterns.thcl.dev/function-as-child) + +--- + +## Forward Ref Pattern + +React components **do not expose ref by default**. While it is a bummer, it kinda makes sense since it's quite rare that we need to access the ref of a component. + +When you need to access them, we need to do ref forwarding. This does not completely fit to be categorized as a pattern since it is a feature of React, but I want to include this because it's uncommon. + +### Demo App + + + +Description: + +Logs showing two different results of accessing ref between components: + +- SimpleButton is not forwarded, hence resulting null +- The button with forwardRef has the ref value + +### Why bother forwarding? + +I can't fully cover the use of ref in this article, usually, it's used when you are building a basic design system element like buttons or links. + +As I said, it's rare that you directly access the ref yourself. However, **component libraries like Radix and Headless UI do access them**. If you don't forward your ref, the tooltip from Radix UI won't work because they rely on the [trigger ref](https://github.com/radix-ui/primitives/blob/7d884d2bddf9501187be77ae1ba406b8ea15ce24/packages/react/popover/src/Popover.tsx#L271). + +```tsx + + {/* Does not pass ref */} + + +``` + +This is why if you are creating a button for the design system, forwarding the ref is mandatory. + +### Implementation + +```tsx +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +/** Notice the use of ComponentProps_With_Ref */ +type ButtonWithRefProps = React.ComponentPropsWithRef<'button'>; + +// forward ref +export const ButtonWithRef = React.forwardRef< + HTMLButtonElement, + ButtonWithRefProps +>(({ className, ...rest }, ref) => ( +