Ввод компонента React, который клонирует своих дочерних элементов, чтобы добавить дополнительные свойства в TypeScript

Предполагая, что у вас есть следующий компонент, который принимает один или несколько дочерних элементов JSX.Elements и передает дополнительный обратный вызов своим реквизитам, когда они отображаются с использованием React.cloneElement(child, { onCancel: () => {} }.

Рассматриваемый компонент (отрывок):

interface XComponentProps {
    onCancel: (index: number) => void;
}

class XComponent extends React.Component<XComponentProps> {
    render() {
        const { onCancel } = this.props;
        const children = typeof this.props.children === 'function' ?
            React.cloneElement(this.props.children, { onCancel: () => onCancel() }) :
            this.props.children.map((child, idx) => (
                React.cloneElement(child, { onCancel: () => onCancel(idx) })
            ));
        return <div>{children}</div>;
    }
}

Рассматриваемый компонент в действии (отрывок):

interface UserComponentProps { caption: string }
const UserComponent = (props: UserComponentProps) => (
    <button onClick={props.onClose}>{props.caption || "close"}</button>
);

ReactDOM.render(
    <XComponent onCancel={handleCancel}>
        <UserComponent />
        <UserComponent />
        <UserComponent />
    </XComponent>
);

Прямо сейчас TSC жалуется, что UserComponent не имеет onCancel в определении интерфейса его реквизита, чего на самом деле нет. Самое простое решение - вручную определить интерфейс с onCancel по UserComponentProps.

Однако я хочу исправить это, не изменяя определение свойства дочернего узла, чтобы компонент мог принимать произвольный набор элементов React. В таком сценарии есть способ определить типизацию возвращаемых элементов, у которых есть дополнительные неявные реквизиты, переданные на уровне XComponent (родительский)?


person shuntksh    schedule 26.01.2017    source источник


Ответы (4)


Это невозможно. Невозможно статически узнать, какие реквизиты UserComponent получает от родительского XComponent в вашем контексте ReactDOM.render.

Если вам нужно типобезопасное решение, используйте дочерние элементы как функции:

Вот XComponent определение

interface XComponentProps {
    onCancel: (index: number) => void;
    childrenAsFunction: (props: { onCancel: (index: number) => void }) => JSX.Element;
}

class XComponent extends React.Component<XComponentProps> {
    render() {
        const { onCancel } = this.props;
        return <div>{childrenAsFunction({ onCancel })}</div>;
    }
}

Теперь вы можете использовать его для рендеринга ваших UserComponents

<XComponent onCancel={handleCancel} childrenAsFunction={props => 
  <span>
    <UserComponent {...props} />
    <UserComponent {...props} />
  </span>
} />

Это будет хорошо работать, я часто использую этот шаблон без проблем. Вы можете выполнить рефакторинг XComponentProps, чтобы ввести свойство childrenAsFunction с соответствующей частью (здесь функция onCancel).

person Benoit B.    schedule 20.04.2017
comment
Спасибо за интересное решение! Имеет смысл передать как функцию, чтобы TSC мог знать отношения между родителем и потомками. - person shuntksh; 26.04.2017

Вы можете использовать наследование интерфейсов (см. Расширение интерфейсов) и заставить UserComponentProps расширять XComponentProps:

interface UserComponentProps extends XComponentProps { caption: string }

Это даст UserComponentProps все свойства XComponentProps в дополнение к его собственным свойствам.

Если вы не хотите, чтобы в UserComponentProps были определены все свойства XComponentProps, вы также можете использовать частичные типы (см. Сопоставленные типы):

interface UserComponentProps extends Partial<XComponentProps> { caption: string }

Это даст UserComponentProps все свойства XComponentProps, но сделает их необязательными.

person erikamjoh    schedule 28.02.2017
comment
Спасибо @erikamjoh, это мой текущий обходной путь, но я хотел добиться его, не изменяя реквизиты дочерних компонентов ... - person shuntksh; 25.03.2017

Вы можете передавать дочерние элементы как функцию даже в JSX. Таким образом, вы всегда будете правильно печатать. Интерфейс свойств UserComponents должен расширять ChildProps.

interface ChildProps {
    onCancel: (index: number) => void;
}

interface XComponentProps {
    onCancel: (index: number) => void;
    children: (props: ChildProps) => React.ReactElement<any>;
}

class XComponent extends React.Component<XComponentProps> {
    render() {
        const { onCancel } = this.props;
        return children({ onCancel })};
    }
}


<XComponent onCancel={handleCancel}>
    { props => <UserComponent {...props} /> } 
</XComponent>
person Mattias Andersson    schedule 14.09.2017

Я знаю, что это было решено, но другое решение - ограничить тип детей, которые могут быть переданы. Ограничивает функциональность оболочки, но может быть тем, что вам нужно.

Итак, если вы сохранили исходный пример:

interface XComponentProps {
   onCancel: (index: number) => void;
   children: ReactElement | ReactElement[]
}

class XComponent extends React.Component<XComponentProps> {
    render() {
        const { onCancel } = this.props;
        const children = !Array.isArray(this.props.children) ?
            React.cloneElement(this.props.children, { onCancel: () => onCancel() }) :
            this.props.children.map((child, idx) => (
                React.cloneElement(child, { onCancel: () => onCancel(idx) })
            ));
        return <div>{children}</div>;
    }
}

interface UserComponentProps { caption: string }
const UserComponent: React.FC = (props: UserComponentProps) => (
    <button onClick={props.onClose}>{props.caption || "close"}</button>
);

ReactDOM.render(
    <XComponent onCancel={handleCancel}>
        <UserComponent />
        <UserComponent />
        <UserComponent />
    </XComponent>
);
person moto    schedule 20.11.2019