Mastering Component Encapsulation: 17 Essential Principles for React Developers

This article shares years of component‑encapsulation experience and lessons drawn from popular UI libraries, presenting seventeen practical principles—ranging from basic property binding to error handling and multilingual support—to help developers build robust, reusable React components that avoid common pitfalls.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Mastering Component Encapsulation: 17 Essential Principles for React Developers

This article summarizes years of component‑encapsulation experience and insights from well‑known UI libraries such as antd, element‑plus, vant, and fusion, offering essential principles for creating perfect reusable components in React (the ideas also apply to Vue).

Below is a React example, but the concepts are equally applicable to Vue.

1. Basic Property Binding Principle

Every component should inherit className and style properties.

import classNames from 'classnames';
export interface CommonProps {
  /** custom class name */
  className?: string;
  /** custom inline style */
  style?: React.CSSProperties;
}
export interface MyInputProps extends CommonProps {
  /** value */
  value: any;
}
const MyInput = forwardRef((props: MyInputProps, ref: React.LegacyRef<HTMLDivElement>) => {
  const { className, ...rest } = props;
  const displayClassName = classNames('chc-input', className);
  return (
    <div ref={ref} {...rest} className={displayClassName}>
      <span></span>
    </div>
  );
});
export default MyInput;

2. Comment Usage Principle

All props and ref attributes should have JSDoc comments.

Disable // comment syntax because it is not recognized by TypeScript and will not appear on hover.

Common annotation tags: @description (description), @version (starting version), @deprecated (deprecated version), @default (default value).

For internationally used components, write comments in English.

bad ❌

interface MyInputsProps {
  // custom class
  className?: string;
}
const test: MyInputsProps = {};
test.className;

after good ✅

interface MyInputsProps {
  /** custom class */
  className?: string;
  /**
   * @description Custom inline style
   * @version 2.6.0
   * @default ''
   */
  style?: React.CSSProperties;
  /**
   * @description Custom title style
   * @deprecated 2.5.0
   * @default ''
   */
  customTitleStyle?: React.CSSProperties;
}
const test: MyInputsProps = {};
test.className;

3. Export Exposure Principle

Component props types must be exported.

If useImperativeHandle is used, the ref type must also be exported.

Component functions should have a name; default export is recommended.

Component without a name causes ambiguous errors.

bad ❌

interface MyInputProps {
  // ...
}
export default (props: MyInputProps) => {
  return <div></div>;
};

after good ✅

// Export MyInputProps type
export interface MyInputProps {
  // ...
}
function MyInput(props: MyInputProps) {
  return <div></div>;
}
export default MyInput;

index.ts

export * from './input';
export { default as MyInput } from './input';

If the component does not expose its types, you can use ComponentProps and ComponentRef to retrieve props and ref types.

type DialogProps = ComponentProps<typeof Dialog>;
type DialogRef = ComponentRef<typeof Dialog>;

4. Input Type Constraint Principle

When possible, avoid using generic primitive types; prefer explicit literal unions or descriptive comments.

Avoid using plain string for status; use 'success' | 'fail' instead.

Add range information to numeric fields, e.g., /** total count 0‑999 */.

bad ❌

interface InputProps {
  status: string;
}

after good ✅

interface InputProps {
  status: 'success' | 'fail';
}

bad ❌

interface InputProps {
  /** total count */
  count: number;
}

after good ✅

interface InputProps {
  /** total count 0‑999 */
  count: number;
}

5. Class and Style Definition Rules

Avoid CSS modules that prevent users from overriding internal styles; use scoped styles in Vue if needed.

All internal class names should have a unified prefix to avoid collisions.

Class names should be semantic.

All internal classes should be overridable by external users.

Avoid !important and inline styles unless absolutely necessary.

Expose CSS variables for color‑related properties to support theme switching.

bad ❌

import styles from './index.module.less';
export default function MyInput(props: MyInputProps) {
  return (
    <div className={styles.input_box}>
      <span className={styles.detail}>21312312</span>
    </div>
  );
}

after good ✅

import './index.less';
const prefixCls = 'my-input'; // unified prefix
export default function MyInput(props: MyInputProps) {
  return (
    <div className={`${prefixCls}-box`}>
      <span className={`${prefixCls}-detail`}>21312312</span>
    </div>
  );
}
.my-input-box {
  height: 100px;
  background: var(--my-input-box-background, #000);
}

6. Inheritance and Prop‑Pass‑Through Principle

When wrapping a base component, extend its props and spread ...rest to forward all received attributes.

bad ❌

import { Input } from 'some-lib';
export interface MyInputProps {
  /** value */
  value: string;
  /** limit */
  limit: number;
  /** state */
  state: string;
}
const MyInput = (props: Partial<MyInputProps>) => {
  const { value, limit, state } = props;
  // ...
  return <Input value={value} limit={limit} state={state} />;
};
export default MyInput;

after good ✅

import { Input, InputProps } from 'some-lib';
export interface MyInputProps extends InputProps {
  /** value */
  value: string;
}
const MyInput = (props: Partial<MyInputProps>) => {
  const { value, ...rest } = props;
  // ...
  return <Input value={value} {...rest} />;
};
export default MyInput;

7. Event Pairing Principle

Any UI change inside a component must expose a corresponding event hook for users.

bad ❌

export default function MyInput(props: MyInputProps) {
  const [open, setOpen] = useState(false);
  const [showDetail, setShowDetail] = useState(false);
  const currClassName = classNames(className, {
    `${prefixCls}-box`: true,
    `${prefixCls}-open`: open,
  });
  const onCheckOpen = () => setOpen(!open);
  const onShowDetail = () => setShowDetail(!showDetail);
  return (
    <div className={currClassName} onClick={onCheckOpen}>
      <span onClick={onShowDetail}>{showDetail ? '123' : '...'}</span>
    </div>
  );
};

after good ✅

export default function MyInput(props: MyInputProps) {
  const { onChange, onShowChange } = props;
  const [open, setOpen] = useState(false);
  const [showDetail, setShowDetail] = useState(false);
  const currClassName = classNames(className, {
    `${prefixCls}-box`: true,
    `${prefixCls}-open`: open,
  });
  const onCheckOpen = () => {
    setOpen(!open);
    onChange?.(!open); // emit open change
  };
  const onShowDetail = () => {
    setShowDetail(!showDetail);
    onShowChange?.(!showDetail); // emit detail change
  };
  return (
    <div className={currClassName} onClick={onCheckOpen}>
      <span onClick={onShowDetail}>{showDetail ? '123' : '...'}</span>
    </div>
  );
};

8. Ref Binding Principle

Components that may receive a ref must expose a corresponding ref property; otherwise users will see console warnings.

Original component: use useImperativeHandle or bind ref to the root element.

interface ChcInputRef {
  /** set view visibility */
  setValidView?: (isShow?: boolean) => void;
  /** field */
  field: Field;
}
const ChcInput = forwardRef<ChcInputRef, MyProps>((props, ref) => {
  const { className, ...rest } = props;
  useImperativeHandle(ref, () => ({
    setValidView(isShow = false) {
      setIsCheckBalloonVisible(isShow);
    },
    field,
  }), []);
  return <div className={displayClassName}>...</div>;
});
export default ChcInput;
const ChcInput = forwardRef((props: MyProps, ref: React.LegacyRef<HTMLDivElement>) => {
  const { className, ...rest } = props;
  const displayClassName = classNames('chc-input', className);
  return (
    <div ref={ref} className={displayClassName}>...</div>
  );
});
export default ChcInput;

9. Custom Extensibility Principle

Provide a user‑defined entry point (e.g., a render prop) so that complex internal logic can be overridden.

bad ❌

export default function MyInput(props: MyInputProps) {
  const { value } = props;
  const detailText = useMemo(() =>
    value.split(',').map(item => `Component internal logic: ${item}`).join('
')
  , [value]);
  return (
    <div>
      <span>{detailText}</span>
    </div>
  );
};

after good ✅

export default function MyInput(props: MyInputProps) {
  const { value, render } = props;
  const detailText = useMemo(() => {
    return render ? render(value) : value.split(',').map(item => `Component internal logic: ${item}`).join('
');
  }, [value, render]);
  return (
    <div>
      <span>{detailText}</span>
    </div>
  );
};

10. Controlled vs. Uncontrolled Mode Principle

Components should support both controlled and uncontrolled usage.

bad ❌ (only controlled)

export default function MyInput(props: MyInputProps) {
  const { value, className, style, onChange } = props;
  const currClassName = classNames(className, {
    `${prefixCls}-box`: true,
    `${prefixCls}-open`: value,
  });
  const onCheckOpen = () => onChange?.(!value);
  return (
    <div className={currClassName} style={style} onClick={onCheckOpen}>12312</div>
  );
};

after good ✅

export default function MyInput(props: MyInputProps) {
  const { value, defaultValue = true, className, style, onChange } = props;
  const [open, setOpen] = useState(value ?? defaultValue);
  useEffect(() => {
    if (typeof value === 'boolean') setOpen(value);
  }, [value]);
  const currClassName = classNames(className, {
    `${prefixCls}-box`: true,
    `${prefixCls}-open`: open,
  });
  const onCheckOpen = () => {
    onChange?.(!open);
    if (typeof value !== 'boolean') setOpen(!open);
  };
  return (
    <div className={currClassName} style={style} onClick={onCheckOpen}>12312</div>
  );
};

11. Minimal Dependency Principle

Avoid adding new dependencies unless absolutely necessary; prefer hand‑written solutions when possible.

bad ❌ (adds ahooks)

import { useLatest } from 'ahooks';
import classNames from 'classnames';
const ChcInput = forwardRef((props: InputProps, ref) => {
  const { className, ...rest } = props;
  const displayClassName = classNames('chc-input', className);
  const funcRef = useLatest(func); // new dependency
  return <div className={displayClassName} {...rest} />;
});
export default ChcInput;

after good ✅ (internal useLatest implementation)

// hooks/index.tsx
export function useLatest(value) {
  const ref = useRef(value);
  ref.current = value;
  return ref;
}
// component
import { useLatest } from '@/hooks';
import classNames from 'classnames';
const ChcInput = forwardRef((props: InputProps, ref) => {
  const { className, ...rest } = props;
  const displayClassName = classNames('chc-input', className);
  const funcRef = useLatest(func);
  return <div className={displayClassName} {...rest} />;
});
export default ChcInput;

12. Single Responsibility Principle

Split complex components into smaller, single‑purpose components to improve reusability and flexibility.

bad ❌ (combined table and image logic)

const MyShowPage = forwardRef((props: MyTableProps, ref) => {
  const { data, imgList, ...rest } = props;
  return (
    <div>
      <Table ref={ref} data={data} {...rest} />
      <div>{/* image logic */}</div>
    </div>
  );
});

after good ✅

const MyShowPage = forwardRef((props: MyTableProps, ref) => {
  const { data, imgList, ...rest } = props;
  return (
    <div>
      <MyTable ref={ref} data={data} {...rest} />
      <MyImg data={imgList} />
    </div>
  );
});

13. Distinguish Generic and Business Components

Generic components should contain only universal UI logic, while business components embed specific domain logic.

bad ❌ (generic table contains business formatting)

const MyTable = forwardRef((props: MyTableProps, ref) => {
  const { data, columns, ...rest } = props;
  const dataRender = item => Math.abs(item.value);
  const styleRender = item => (item.value < 0 ? { color: 'red' } : undefined);
  const tableColumns = useMemo(() =>
    columns.map(item => {
      if (item.name === 'value') {
        return { ...item, render: dataRender, styleRender };
      }
      return { ...item };
    })
  , [columns]);
  return <Table ref={ref} data={data} {...rest}>...</Table>;
});

after good ✅ (business logic moved to usage side)

// Generic component
const MyTable = forwardRef((props: MyTableProps, ref) => {
  const { data, columns, ...rest } = props;
  return <Table ref={ref} data={data} {...rest}>...</Table>;
});
// Usage
const columns = [
  { title: 'Name', name: 'name' },
  {
    title: 'Value',
    name: 'value',
    render: item => Math.abs(item.value),
    style: item => (item.value < 0 ? { color: 'red' } : undefined),
  },
];
<MyTable data={list} columns={columns} />;

14. Maximum Depth Extensibility

When a component receives tree‑structured data, implement recursive rendering to support unlimited depth.

bad ❌ (only one level)

interface Columns extends TableColumnProps {
  columns: TableColumnProps[];
}
const MyTable = forwardRef((props: MyTableProps, ref) => {
  const { data, columns = [], ...rest } = props;
  const renderColumn = useMemo(() =>
    columns.map(item =>
      item.columns ? (
        <Table.Column {...item}>
          {item.columns.map(col => <Table.Column {...col} />)}
        </Table.Column>
      ) : (
        <Table.Column {...item} />
      )
    )
  , [columns]);
  return <Table ref={ref} data={data} {...rest}>{renderColumn}</Table>;
});

after good ✅ (recursive component)

interface Columns extends TableColumnProps {
  columns: Columns[]; // recursive type
}
const MyTable = forwardRef((props: MyTableProps, ref) => {
  const { data, columns = [], ...rest } = props;
  return (
    <Table ref={ref} data={data} {...rest}>
      <MyColumn columns={columns} />
    </Table>
  );
});
const MyColumn = (props: { columns: Columns[] }) => {
  const { columns } = props;
  return columns.map(item =>
    item.columns ? (
      <Table.Column {...item}>
        <MyColumn columns={item.columns} />
      </Table.Column>
    ) : (
      <Table.Column {...item} />
    )
  );
};

15. Multilingual Configurability

All textual content inside a component should be configurable to support multiple languages, with English as the default.

When many strings are needed, expose a strings prop object containing all replaceable keys.

bad ❌ (hard‑coded Chinese strings)

const prefixCls = 'my-input';
export default function MyInput(props: MyInputProps) {
  const { title = '标题' } = props;
  return (
    <div className={`${prefixCls}-box`}>
      <span className={`${prefixCls}-title`}>{title}</span>
      <span className={`${prefixCls}-detail`}>详情</span>
    </div>
  );
}

after good ✅

const prefixCls = 'my-input';
export default function MyInput(props: MyInputProps) {
  const { title = 'title', detail = 'detail' } = props;
  return (
    <div className={`${prefixCls}-box`}>
      <span className={`${prefixCls}-title`}>{title}</span>
      <span className={`${prefixCls}-detail`}>{detail}</span>
    </div>
  );
}

16. Error Handling and Messaging

When user‑provided parameters may cause errors, log a console.error message instead of throwing.

Use console.warn for non‑critical issues.

bad ❌ (no validation)

export default function MyCanvas(props: MyCanvasProps) {
  const { instanceId } = props;
  useEffect(() => {
    initDom(instanceId);
  }, []);
  return (
    <div>
      <canvas id={instanceId} />
    </div>
  );
}

after good ✅

export default function MyCanvas(props: MyCanvasProps) {
  const { instanceId } = props;
  useEffect(() => {
    if (!instanceId) {
      console.error('missing instanceId!');
      return;
    }
    initDom(instanceId);
  }, []);
  return (
    <div>
      <canvas id={instanceId} />
    </div>
  );
}

17. Semantic Naming Principle

Component names, APIs, methods, and internal variables must be semantically meaningful and accurately reflect their purpose.

Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.