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
we have two ways of creating the component
- React.cloneElement
- useContext
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 theerror
div. one option is that pass the label as React Component or as i will explain in next section inuseContext
.
<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
<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 theonChange
oronBlur
Function so we need to prop drill or do the same asReact.cloneElement
to receive props which increases the complexity.
<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
useContext()
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
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 or linkedin though i personally prefer Twitter.
Top comments (0)