Component variant with View-Hook Pair
Tags: react, designpatterns, codequality, webdev
Published: 2021-09-05 12:55:58

---
title: Component variant with View-Hook Pair
published: true
description: Naive implementation for the component with variation would not be maintainable by branch condition or duplicated code. I describe how to implement with View-Hook Pair for that.
tags: react, designpatterns, codequality, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3p67qmqhzberwxnaq1rs.png
---

[Japanese Version | 日本語版](https://blog.masuqat.net/2021/08/21/component-variant-with-view-hook-pair/)

In [my previous post](https://dev.to/occar421/view-hook-pair-pattern-draft-5fjd), I introduced View-Hook Pair pattern. In this post, I describe how to implement a component with some variations with View-Hook Pair. Using it, we can achieve the modular frontend component without lacking the flexibility of spec change or refactoring-ability.

[Example code](https://github.com/MasuqaT-NET/BlogExamples/tree/master/Web/React/component-variant-with-view-hook-pair)

## Variation of the component

During creating a web application, there are the visually similar component with almost the same usecase and the same place. It is because that, some parts look/behave a bit different according to its attribute, feature flags, customize by users, and so on.

In this post, I bring up the example of a simple file viewer app with the component with the 2 variations. This app has 2 panes, the file list on the left and the detail on the right. Especially for an image file or a movie file, the detail pane additionaly shows the thumbnails (actually a made-up image).

![Initial look](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xyvsiptzp7asx88jxdf9.jpeg)

![For a normal file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ik1t0684x2dhe5pzee0j.jpeg)

![For a media file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sr781pa07fsq754b5bex.jpeg)

At this time, I regard the difference of the detail pane as a variation.

## Problem

Of course we hope all the components are highly maintainable. However, for the component shared part of the variants, it is hard. The spec change could leads code changes in many place due to duplication and/or forgetting the necessary changes. It costs more and has higher congnitive load than the usual components. We need to avoid them somehow.

Though there are several naive implementation for the component with the variants, it would cause problems when the component gets complex/large.

* Code changes in many place or miss by the duplication when the variants are implemented with the respective components.
* Difficult to change by the branch conditions when the varitants are implemented as the single component which changes UI/behavior by the parameters.
* The component requires unnecessary props for some variants and it is difficult to express as the type.
* Though we initially achieve the good shared parts or the good branch flows, it won't be that good after it adds many features.

I assume View-Hook Pair ease these problems.

## View-Hook Pair

[View-Hook Pair](https://dev.to/occar421/view-hook-pair-pattern-draft-5fjd) is, I previously proposed, an idea for divide-and-conquar method which consists with the formmaly defined pair of View and Hook. The former covers UI code and the latter covers state and logic.

In this post, I defined the "Standard form" against the problem like this.

```tsx
function useObject({}: Props, {}: Dependencies): [ViewProps, Exports] {
  return [{}, {}] as const;
}
function View({}: ViewProps): ReactElement {
  return <div />;
}
export const Component = { useObject, View };
```

I newly propose the objects `Dependencies` and `Exports`. These are optional to use and we are also be able to implement the example project without these objects.

The former object `Dependencies` would be the port of Dependency Injection pattern. This is to the component free from the (static and technically detailed) external dependency with the side-effect and/or resource fetching, like the server layered architecture. 

The latter object `Exports` would be the public methods/properties of Object-Oriented Programming class. These `useObject` "class" exchange messages with others through their "public" interfaces.

When to integrate the pair, integrate it in where it is used. For example, the component which uses `Component` with **non** View-Hook Pair form and the unit tests.

## Tactics

From here, I describe how to implement the pair using child pairs.

First, make the normal component for the shared part. We don't need to use View-Hook Pair for a (simple) Presentational component or the one only with small logic and scoped state. If not and necessary, like "Standard form" above, split the component into the pair. That said, usually we just put the component states and callbacks to `ViewProps`, the functions run in `useEffect` like `componentWillMount` to `Exports`, and declarative UI (including animation state) to `View`, respectively.

In a View-Hook Pair form component which uses View-Hook Pair form components, combine Views and Hooks independently.

```tsx
function useObject({}: Props, {}: Dependencies): [ViewProps, Exports] {
  const [childProps, childExports] = Child.useObject({}, {});
  return [{ childProps }, {}] as const;
}
function View({ childProps }: ViewProps) {
  return <Child.View {...childProps} />;
}
export const Parent = { useObject, View };
```

In `useObject` in the parent component, it combines its `Prop`, `Dependencies`, and children's `useObject`s and `Exports`s to implement its responsibility. In `View`, using HTML element and other component, it places children's `View`s and mark up its whole look. Maybe, its `ViewProps` has some structure to prevent the flood of flat children's `ViewProps`s.

At the top component which finally integrates the pair to the normal form, like `Component_` above, call `useObject`, do necessary process, then finally call `View`.

The flow of process is like this.

![Flow of process](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3p67qmqhzberwxnaq1rs.png)

(Actually, the order is arbitrary for the `View`s in the same depth.)

In the tree of Hook side, the parent Hook can create its `ViewProps` and `Exports` by referencing children's encapsulated states from `Exports`s, in postorder. It is difficult to achieve with normal React component method except with redundant(duplicated) statea. In the tree of View side, the tree structure is almost the same and rendered in the similar order.

## Example through the project

I pick some code for the component implementing variants from [Example code](https://github.com/MasuqaT-NET/BlogExamples/tree/master/Web/React/component-variant-with-view-hook-pair). For the rest of the component, check 'src/Panel'. I don't mention the other components because it is off-topic.

As I mentioned the example project in the first section of this post, its detail pane has 2 variations which is for media files and for others. In this example, they are implemented separatedly as `MediaPanel` and `TextPanel` respectively (both components are located in 'src/Panel'). Both are "Parent" in the figure above and they share their contents in 'src/Panel/parts', except the spacing and the behavior/UI only in one.

First, I show the `TextPanel` which is easier. (Style definitions are excluded because it is off-topic.)

```tsx
type Props = { name: string };
type Dependencies = {};
function useObject({ name }: Props, {}: Dependencies) {
  const [attributesProps] = Attributes.useObject({ name }, {});
  return [{ name, attributesProps }];
}
function View({ name, attributesProps }: ReturnType<typeof useObject>[0]) {
  return (
    <div>
      <div>
        <Header name={name} />
      </div>
      <div>
        <Attributes.View {...attributesProps} />
      </div>
    </div>
  );
}
export const TextPanel = { useObject, View };
```

It uses normal form `Header` shared component and View-Hook Pair form `Attributes` shared component. I placed `<div />` to add spacing because it is responsibility of this component.

Then, I show the Hook of `MediaPanel`.

```tsx
type Props = { id: string; name: string };
type Dependencies = { getPreviewUrl: (id: string) => Promise<string> };
function useObject({ id, name }: Props, { getPreviewUrl }: Dependencies) {
  const [previewUrl, setPreviewUrl] = useState<string>();
  const [previewProps] = Preview.useObject({ previewUrl }, {});
  const [attributesProps, { editing: attributesEditing }] = Attributes.useObject({ name }, {});
  const load = useCallback(async () => {
    setPreviewUrl(undefined);
    setPreviewUrl(await getPreviewUrl(id));
  }, [id, getPreviewUrl]);
  return [{ name, attributesEditing, previewProps, attributesProps }, { load }] as const;
}
```

It additionally uses `Preview` shared component  because `MediaPanel` has a preview not like `TextPanel`. Then, `MediaPanel` has a requirement to stop the animation during editing `Attributes`, so it uses `Exports` of `Attributes.useObject`. Furthermore, it exports `load` in `Exports` to make the parent be able to control the load timing of it.

Finally, I show the View of `MediaPanel`.

```tsx
function View({
  name,
  attributesEditing,
  previewProps,
  attributesProps,
}: ReturnType<typeof useObject>[0]) {
  const [previewHovered, setPreviewHovered] = useState(false);
  return (
    <div>
      <div className={previewHovered && !attributesEditing && style}>
        <Header name={name} />
      </div>
      <div onMouseEnter={() => setPreviewHovered(true)} onMouseLeave={() => setPreviewHovered(false)}>
        <Preview.View {...previewProps} />
      </div>
      <div>
        <Attributes.View {...attributesProps} />
      </div>
    </div>
  );
}
```

It just pass the props for the children's `ViewProps`s. Apart from that, it has `previewHovered` state for the UI decoration. (View-Hook Pair is to sweep out information state and logic from `View` and it doesn't mean to prohibit any kind of state. The state and logic are allowed which is only for the decoration.)

## Cons

* Though it helps the essential part (in sence the of spec) to be well shared, this pattern forces many boilerplates.
* Conditional or loop process. This is due to React Hooks restriction. It also could be a problem when the View and Hook is located in the different depth in the tree.
* Typing for the pair. Declaring all the type and impose the pair or referring the actual implementation dynamically. (This example uses latter one.)

## Summary

To explain the usage of View-Hook Pair, I describe the implementation for the component with variations to prevent the maintainability problem.