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>
);
}
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,
}
}
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>
);
}
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
};
}
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>
);
}
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.
MssKnd / render-hooks-demo
Created with CodeSandbox
render-hooks-demo
Created with CodeSandbox
Top comments (0)