Skip to main content

Writing Custom Components

Custom components are written in the same way as regular React components. This document introduces the basic concepts and example code needed for writing custom components.

Basic Structure

The basic structure of a custom component is as follows:

import { memo, ReactElement } from 'react';

import { CustomComponentRenderProps, useComponentState, useLogger } from '@hops/custom-component';
import { FormControl, FormLabel, Input } from '@hops/design-system';

interface OwnProps {
label: string;
defaultValue: string;
}

function MyComponent(props: CustomComponentRenderProps<OwnProps>): ReactElement {
const { label, defaultValue } = props;

const logger = useLogger();

const [value, setValue] = useComponentState<string>('inputValue', defaultValue);

return (
<FormControl>
<FormLabel>{label}</FormLabel>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
logger.debug(`onChange called: ${e.target.value}`);
}}
/>
</FormControl>
);
}

export default memo(MyComponent);

Component Writing Steps

1. Define Props

Custom components define props to receive from the outside through the OwnProps interface:

interface OwnProps {
// Props that users can set in the editor
label: string;
placeholder: string;
required: boolean;
}

These props can be set by users in the editor and used within the component.

2. Manage State

The state of the component is managed using the useComponentState hook:

const [value, setValue] = useComponentState<string>('fieldValue', defaultValue);

The state defined by useComponentState when implementing a custom component is also externally accessible.

Access the state of a custom component with an accessor of the form component_name.state_name where needed.

customComponent1.fieldValue;

State managed in this way can be accessed and used by other components or workflows. Of course, you can also manage state that is only used internally using React's useState.

3. Implement UI

Implement UI using JSX like a regular React component:

return (
<FormControl required={required}>
<FormLabel>{label}</FormLabel>
<Input value={value} placeholder={placeholder} onChange={(e) => setValue(e.target.value)} />
</FormControl>
);

Using Hops design system components (@hops/design-system) helps maintain consistent design.

Example: Custom Input Field

Here's an example of a text field with custom validation functionality:

import { memo, ReactElement, useState } from 'react';

import { CustomComponentRenderProps, useComponentState } from '@hops/custom-component';
import { FormControl, FormLabel, Input, FormHelperText } from '@hops/design-system';

// Define component props type
interface OwnProps {
label: string;
placeholder: string;
pattern: string; // Regular expression pattern
errorMessage: string;
}

// Component implementation
function PatternValidatedInput(props: CustomComponentRenderProps<OwnProps>): ReactElement {
// Read component properties
const { label, placeholder, pattern, errorMessage } = props;

// State management
const [value, setValue] = useComponentState<string>('value', '');
const [isValid, setIsValid] = useComponentState<boolean>('isValid', true);

const validateInput = (input: string) => {
if (!pattern) return true;
const regex = new RegExp(pattern);
return regex.test(input);
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
const valid = validateInput(newValue);
setIsValid(valid);
};

return (
<FormControl isInvalid={!isValid}>
<FormLabel>{label}</FormLabel>
<Input value={value} placeholder={placeholder} onChange={handleChange} />
{!isValid && <FormHelperText color="red">{errorMessage}</FormHelperText>}
</FormControl>
);
}

export default memo(PatternValidatedInput);

Example: Star Rating Component

Here's an example of a custom star rating input component:

import { memo, ReactElement, useCallback, useState } from 'react';

import { CustomComponentRenderProps, useComponentState } from '@hops/custom-component';

// Define component props type
interface OwnProps {
label: string;
maxStars: number;
size: 'sm' | 'md' | 'lg';
color: string;
}

// Component implementation
function StarRating(props: CustomComponentRenderProps<OwnProps>): ReactElement {
// Read component properties
const { label, maxStars = 5, size = 'md', color = 'gold' } = props;

// State management
const [rating, setRating] = useComponentState<number>('rating', 0);
const [hoveredRating, setHoveredRating] = useState(0);

const handleClick = useCallback(
(index: number) => {
setRating(index);
},
[setRating],
);

const starSizes = {
sm: { fontSize: '1rem', spacing: 8 },
md: { fontSize: '1.5rem', spacing: 16 },
lg: { fontSize: '2rem', spacing: 24 },
};

return (
<div>
<label>{label}</label>
<div style={{ display: 'flex', gap: starSizes[size].spacing }}>
{Array.from({ length: maxStars }).map((_, i) => (
<span
key={i}
style={{
cursor: 'pointer',
fontSize: starSizes[size].fontSize,
color: color,
}}
onMouseEnter={() => setHoveredRating(i + 1)}
onMouseLeave={() => setHoveredRating(0)}
onClick={() => handleClick(i + 1)}
>
{(hoveredRating || rating) > i ? '★' : '☆'}
</span>
))}
</div>
<div>
{rating} / {maxStars}
</div>
</div>
);
}

export default memo(StarRating);
tip

Always apply memoization to optimize performance when writing custom components. It's a good practice to wrap all custom components with memo when exporting.