DEV Community

Cover image for React: Practice of render-hooks pattern
mssknd
mssknd

Posted on

React: Practice of render-hooks pattern

Do you know about the React render-hooks pattern? It is a design pattern introduced in the following article written in Japanese.

【LINE証券 FrontEnd】コンポーネントをカスタムフックで提供してみた

The key idea is "providing components through custom hooks." Custom hooks are used to separate the logic (ts) from UI components (tsx). By having custom hooks directly provide UI components, the logic of the UI components can be made more cohesive, allowing for the writing of simpler code. I really like it!

In this article, I will introduce the Render hooks pattern using a simple example.

This is a translation of a previous article I wrote.
render hooks パターンの素振り

Example

I created a component that displays a preview of an image when an image file is specified.

We will rewrite this implementation from the usual approach to using the Render hooks pattern.

Usual implementation

Here's an example of an implementation without using custom hooks.

The input to <input type="file" /> is handled by handleInput(), which converts the image to a data URL.

// src/app.tsx
import { useState } from "react";

export default function App() {
  const [value, setValue] = useState<string | null>(null);

  const handleInput = (changeEvent: React.ChangeEvent<HTMLInputElement>) => {
    const file = changeEvent.target.files ? changeEvent.target.files[0] : null;
    if (!file) {
      return;
    }
    const reader = new FileReader();
    reader.onload = (event: ProgressEvent<FileReader>) => {
      if (typeof event?.target?.result === "string") {
        setValue(event.target.result);
      }
    };
    reader.readAsDataURL(file);
  };

  return (
    <div className="App">
      <input type="file" accept="image/*" onChange={handleInput} />
      {value && <img src={value} alt="preview" />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Implementation of render hooks pattern applied to <input>

First, let's provide <input type="file" /> from a custom hook. Please be aware that the file extension should be .tsx.

// src/use-image-file-input.tsx
import { useState } from "react";

export function useInputImageFile() {
  const [imageDataUrl, setImageDataUrl] = useState<string | null>(null);

  const handleInput = (changeEvent: React.ChangeEvent<HTMLInputElement>) => {
    const file = changeEvent.target.files ? changeEvent.target.files[0] : null;
    if (!file) {
      return;
    }
    const reader = new FileReader();
    reader.onload = (event: ProgressEvent<FileReader>) => {
      if (typeof event?.target?.result === "string") {
        setImageDataUrl(event.target.result);
      }
    };
    reader.readAsDataURL(file);
  };

  const imageFileInput = () => 
  <input type="file" accept="image/*" onChange={handleInput} />

  return {
    imageDataUrl,
    imageFileInput,
  }
}
Enter fullscreen mode Exit fullscreen mode

Most of the logic for obtaining image files that was written in app.tsx has been moved to the custom hook. At this time, app.tsx becomes as follows:

// src/app.tsx
import { useImageFileInput } from "./use-image-file-input";

export default function App() {
  const { imageDataUrl, ImageFileInput } = useImageFileInput();

  return (
    <div className="App">
      <ImageFileInput />
      {imageDataUrl && <img src={imageDataUrl} alt="preview" />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The logic has become very simple due to the aggregation in the custom hook. I also like that <ImageFileInput /> behaves like a regular useState's set function, making it easy to use.

Implementation of render hooks pattern applied to image preview

Let's also make the preview, which is displayed after obtaining the image, the responsibility of the custom hook. Rename useImageFileInput to useImageFile and provide the preview component as well.

  // tsx:src/use-image-file.tsx
  import { useState } from "react";

- export function useImageFileInput() {
+ export function useImageFile() {
    const [imageDataUrl, setImageDataUrl] = useState<string | null>(null);

    const handleInput = (changeEvent: React.ChangeEvent<HTMLInputElement>) => {
        const file = changeEvent.target.files ? changeEvent.target.files[0] : null;
        if (!file) {
        return;
        }
        const reader = new FileReader();
        reader.onload = (event: ProgressEvent<FileReader>) => {
        if (typeof event?.target?.result === "string") {
            setImageDataUrl(event.target.result);
        }
        };
        reader.readAsDataURL(file);
    };

    const ImageFileInput = () => (
        <input type="file" accept="image/*" onChange={handleInput} />
    );

+   const ImagePreview = () =>
+       imageDataUrl ? <img src={imageDataUrl} alt="preview" /> : <></>;

    return {
-       imageDataUrl,
        ImageFileInput,
+       ImagePreview
    };
  }
Enter fullscreen mode Exit fullscreen mode

The branching in app.tsx has been eliminated, making it even simpler.

  // tsx:src/app.tsx
- import { useImageFileInput } from "./use-image-file-input";
+ import { useImageFile } from "./use-image-file";

  export default function App() {
-   const { imageDataUrl, ImageFileInput } = useImageFileInput();
+   const { ImageFileInput, ImagePreview } = useImageFile();

    return (
      <div className="App">
        <ImageFileInput />
-       {imageDataUrl && <img src={imageDataUrl} alt="preview" />}
+       <ImagePreview />
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

By providing image input and preview output as a custom hook, reusability is increased. For example, if you want to display "No image has been input" in the preview when no image is selected, you can change the definition of the preview within the custom hook and reflect it in all components using it. It seems possible to create easy-to-use custom hooks, such as providing a cancel button for the selected image.

Conclusion

We have created an example of the render-hooks pattern using image file input and output as the subject matter. Personally, I understand that the core of this pattern is to increase the cohesion of UI components and their related states. I feel that it is a technique that expands the range of expression, and I plan to continue using it at key points in the future.

The contents of the code can be found in the following repository.

GitHub logo MssKnd / render-hooks-demo

Created with CodeSandbox

Top comments (0)