React Pattern - Build Better Component with Compound component
Tags: react, webdev, typescript, productivity
Published: 2022-12-27 13:02:16


React is a popular JavaScript library for building user interfaces, and one of the key concepts in React is the ability to create reusable components.

Build Better Component with One pattern that can help with building reusable components is the "Compound Component" pattern. i am sure you have heard of this term before and i am going to explain this to you now.

In the Compound Component pattern, a parent component passes data and behaviour down to a group of child components through props. The child components then use this data and behaviour to render themselves, and can also pass data back up to the parent through event handlers.

> Think of compound components like the `_<select>_` and `_<option>_` elements in HTML. Apart they don’t do too much, but together they allow you to create the complete experience.  — [Kent C. Dodds](https://blog.kentcdodds.com/advanced-react-component-patterns-56af2b74bc5f)


we have two ways of creating the component

1. React.cloneElement
2. useContext

```ts
import { useState } from 'react';

const FormControl = ({ label, children }) => {
  const [value, setValue] = useState('');
  const [touched, setTouched] = useState(false);
  const [valid, setValid] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.value.length > 0) {
      setValid(true);
      setValue(event.target.value);
      setErrorMessage('');
    } else {
      setValid(false);
      setErrorMessage('Value is required');
    }
  };

  const handleBlur = () => {
    setTouched(true);
  };

  return (
    <div>
      {label && <Label>{label}</Label>}
      {React.cloneElement(children, {
        value,
        onChange: handleChange,
        onBlur: handleBlur,
      })}
      {touched && !valid && <ErrorMessage>{errorMessage}</ErrorMessage>}
    </div>
  );
};

const Label = ({ children }: React.ComponentPropsWithoutRef<'label'>) => {
  return <label>{children}</label>;
};

const ErrorMessage = ({ children }: React.ComponentPropsWithoutRef<'div'>) => {
  return <div className="error">{children}</div>;
};

const TextInput = ({
  value,
  onChange,
  onBlur,
}: {
  value: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
}) => {
  return <input type="text" value={value} onChange={onChange} onBlur={onBlur} />
}

const Form = () => {
  return (
    <form>
      <FormControl label="Name">
        <TextInput />
      </FormControl>
      <FormControl label="Email">
        <TextInput />
      </FormControl>
      <button type="submit">Submit</button>
    </form>
  );
};

```


this example, the `FormControl` component is the parent, and the `TextInput` component is the child. The `FormControl` component uses the `useState` hook to manage its own state values for the input value, touched state, valid state, and error message.

The `FormControl` component also has separate components for the label and the error message, which allows you to customize the appearance of these elements separately.

The `TextInput` component receives the value, onChange, and onBlur props from the `FormControl` component, which allows it to update the form state values and trigger the validation logic when the input value changes or when the input is blurred.

The `FormControl` component also renders a label and an error message based on the state values. If the input has been touched and is not valid, it will display an error message.

This compound component pattern allows the `FormControl` component to handle the common logic for all form inputs, such as validation and error handling, while also allowing developers to customize the specific input element with the `TextInput` component.


### Problems

Now it's does the job that we like to do but the problem with this will changing the behavior of our sub-components.

- if we like to pass diffrent value to the `label` we don't have access to that or the `error` div. one option is that pass the label as React Component or as i will explain in next section in `useContext`.

```ts
	<FormControl label="Name">// <- we don't have access to label 
		<TextInput />
    </FormControl>
```

- if we pass a two component in the `FormControl` than both will get the all the props

```ts
	<FormControl label="Name">
	    <TextInput />
	    <OtherComponent/> // <- this will also receive the props which can lead to bugs
    </FormControl>
```

- If I pass Wrap around the `<TextInput />` than it will not get the `onChange` or `onBlur` Function so we need to prop drill or do the same as `React.cloneElement` to receive props which increases the complexity.

```ts
	<FormControl label="Name">
	    <Wrapper> // <- this will receive the props we will need prop drill
		    <TextInput />
	    </Wrapper>
    </FormControl>
```



You can look into Official React-beta docs to learn about the alternatives to `cloneElement` - [Link](https://beta.reactjs.org/reference/react/cloneElement#alternatives)


## useContext()


```ts
import React, { createContext, useState, useContext } from 'react';

// Create a context object
const FormControlContext = createContext<{
   touched: boolean;
   value: string;
   handleBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
   handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
   valid: boolean;
   errorMessage: string;
}>({
   touched: false,
   value: '',
   handleBlur: () => {},
   handleChange: () => {},
   valid: false,
   errorMessage: '',
});

interface FormControlProps {
   CustomHandleChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
   CustomHandleBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
   IsInvalid?: boolean;
   children: React.ReactNode;
}

const FormControl = ({
   CustomHandleChange,
   CustomHandleBlur,
   IsInvalid = true,
   children,
}: FormControlProps) => {
   const [value, setValue] = useState('');
   const [touched, setTouched] = useState(false);
   const [valid, setValid] = useState(IsInvalid);
   const [errorMessage, setErrorMessage] = useState('');

   const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      if (event.target.value.length > 0) {
         setValid(true);
         setValue(event.target.value);
         setErrorMessage('');
      } else {
         setValid(false);
         setErrorMessage('Value is required');
      }
   };

   const handleBlur = () => {
      setTouched(true);
   };

   return (
      <FormControlContext.Provider
         value={{
            touched,
            value,
            handleBlur: CustomHandleBlur ? CustomHandleBlur : handleBlur,
            handleChange: CustomHandleChange
               ? CustomHandleChange
               : handleChange,
            valid,
            errorMessage,
         }}
      >
         {children}
      </FormControlContext.Provider>
   );
};

const Label = ({ children }: React.ComponentPropsWithoutRef<'label'>) => {
   const { touched } = useContext(FormControlContext);
   return (
      <label
         style={{
            fontWeight: touched ? 'bolder' : 'lighter',
         }}
      >
         {children}
      </label>
   );
};

const ErrorMessage = ({
   CustomError,
   children,
}: React.ComponentPropsWithoutRef<'div'> & { CustomError?: string }) => {
   const { touched, valid, errorMessage } = useContext(FormControlContext);
   if (!touched && valid) {
      return (
         <>
            <div className="error">
               <div className="message">
                  {CustomError ? CustomError : errorMessage}
               </div>
            </div>
            {children}
         </>
      );
   }
   return null;
};

const TextInput = ({ ...props }: React.ComponentPropsWithoutRef<'input'>) => {
   const { handleBlur, handleChange, value } = useContext(FormControlContext);
   return (
      <input
         type="text"
         value={value}
         onChange={handleChange}
         onBlur={handleBlur}
         {...props}
      />
   );
};

const Form = () => {
   return (
      <form>
         <FormControl>
            <Label />
            <TextInput />
            <ErrorMessage />
         </FormControl>
         <FormControl>
            <Label />
            <TextInput />
            <ErrorMessage />
         </FormControl>
         <button type="submit">Submit</button>
      </form>
   );
};

```


In this example, the `FormControl` component is the parent, and the child components are the ones that will consume the context. i know it got little bigger than the previus one but we got way more control over our components. 

Also we have added validation and custom handlers. The resone we could not have added in the previus because it will lead to bugs as i mention in the problem section.

Explanation -

`FormControl` will pass the context values and function all the childrens without prop drilling. `Label`, 

The `Label` component is a simple wrapper around a `label` element that receives its children as props. It uses the `touched` state from the `FormControl` context to style the label differently when the form control has been touched by the user.

The `TextInput` component is a simple wrapper around an `input` element that receives all other props as an object. It uses the `handleBlur`, `handleChange`, and `value` states from the `FormControl` context to manage the value and event handlers for the input element.

The `ErrorMessage` component is a wrapper around a `div` element that receives its children as props. It uses the `touched`, `valid`, and `errorMessage` states from the `FormControl` context to render an error message when the form control has been touched and is currently invalid. It also accepts a `CustomError` prop that can be used to override the default error message.

Using the useContext() hook will prevent the problem caused by `React.cloneElement`. 

That's it 

![That's Pretty Much Gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uip8kw6v0n36z9g25yn4.gif)

Hope you enjoy the reading and if you want to have something to correct or feedback or just want to ask for help you can reach out to me on [Twitter](https://twitter.com/Priyang_Bawa)  or [linkedin](https://www.linkedin.com/in/priyang-patel-948685176/) though i personally prefer Twitter.