
Cheatsheets para desarrolladores experimentados de React que estan empezando con TypeScript
Básico | Avanzado | Migrando | HOC | 中文翻译 | ¡Contribuir! | ¡Preguntas!
Este HOC Cheatsheet agrupa todo el conocimiento disponible para escribir Componentes de Orden Superior (HOC por las siglas en inglés de higher-order component) con React y Typescript.
- Inicialmente haremos un mapa detallado de la documentación oficial sobre HOC.
- Si bien existen hooks, muchas librerias y bases de código aún necesitan escribir HOC
- Render props podrian ser considerado en el futuro
- El objetivo es escribir HOC que ofrezcan un tipado seguro sin interferir
Expandir Tabla de Contenido
Este es un ejemplo de un HOC para copiar y pegar. Si ciertos pedazos no tienen sentido para ti, ve a la Sección 1 para obtener un tutorial detallado a través de una traducción completa de la documentación de React en Typescript.
A veces quieres una forma sencilla de pasar props desde otro lugar (ya sea el store global o un provider) y no quieres continuamente pasar los props hacia abajo. Context es excelente para eso, pero entonces los valores desde el context solo pueden ser usado desde tu función render
. Un HOC proveerá esos valores como props.
Los props inyectados
interface WithThemeProps {
primaryColor: string;
}
Uso en el componente
El objetivo es tener los props disponibles en la interfaz para el componente, pero sustraído para los consumidores del componente cuando estén envuelto en el HoC.
interface Props extends WithThemeProps {
children: React.ReactNode;
}
class MyButton extends React.Component<Props> {
public render() {
// Renderiza un elemento usando el tema y otros props.
}
private someInternalMethod() {
// Los valores del tema tambien estan aqui disponibles como props.
}
}
export default withTheme(MyButton);
Consumiendo el componente
Ahora, al consumir el componente puedes omitir el prop primaryColor
o anular el que fue proporcionado a través del context.
<MyButton>Hello button</MyButton> // Valido
<MyButton primaryColor="#333">Hello Button</MyButton> // Tambien valido
Declarando el HoC
El HoC actual.
export function withTheme<T extends WithThemeProps = WithThemeProps>(
WrappedComponent: React.ComponentType<T>
) {
// Intenta crear un buen displayName para React Dev Tools.
const displayName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
// Creando el component interno. El tipo de prop calculado aqui es donde ocurre la magia.
return class ComponentWithTheme extends React.Component<
Optionalize<T, WithThemeProps>
> {
public static displayName = `withPages(${displayName})`;
public render() {
// Obten los props que quiere inyectar. Esto podria ser hecho con context.
const themeProps = getThemePropsFromSomeWhere();
// this.props viene despues para que puedan anular los props predeterminados.
return <WrappedComponent {...themeProps} {...(this.props as T)} />;
}
};
}
Tenga en cuenta que la aserción {...this.props as T}
es necesaria debido a un error en TS 3.2 microsoft/TypeScript#28938 (comment)
Para obtener detalles de Optionalize
consulte la sección de tipos de utilidad
Aquí hay un ejemplo más avanzado de un Componente Dinámico de Orden Superior (HOC por las siglas en inglés de higher-order component) que basa algunos de sus parametros en los props del componente que está siendo pasado.
// Inyecta valores estaticos a un componente de tal manera que siempre son proporcionados.
export function inject<TProps, TInjectedKeys extends keyof TProps>(
Component: React.JSXElementConstructor<TProps>,
injector: Pick<TProps, TInjectedKeys>
) {
return function Injected(props: Omit<TProps, TInjectedKeys>) {
return <Component {...(props as TProps)} {...injector} />;
};
}
Para una reutilización "verdadera", tambien debes considerar exponer una referencia para tus HOC. Puedes utilizar React.forwardRef<Ref, Props>
como está documentado en el cheatsheet basico, pero estamos interesados en más ejemplos del mundo real. Aquí hay un buen ejemplo en práctica de @OliverJAsh.
En esta primera seccion nos referimos de cerca a la documentacion de React sobre HOC y ofrecemos un paralelo directo en TypeScript.
Ejemplo de la documentacion: Usa HOCs para preocupaciones transversales
Variables a las que se hace referencia en el siguiente ejemplo
/** Componentes hijos ficticios que pueden recibir cualquiera cosa */
const Comment = (_: any) => null;
const TextBlock = Comment;
/** Data ficticia */
type CommentType = { text: string; id: number };
const comments: CommentType[] = [
{
text: "comment1",
id: 1
},
{
text: "comment2",
id: 2
}
];
const blog = "blogpost";
/** simulacion de la data */
const DataSource = {
addChangeListener(e: Function) {
// Hace algo
},
removeChangeListener(e: Function) {
// Hace algo
},
getComments() {
return comments;
},
getBlogPost(id: number) {
return blog;
}
};
/** Escriba alias solo para deduplicar */
type DataType = typeof DataSource;
// type TODO_ANY = any;
/** Tipos de utilidad que utilizamos */
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// type Optionalize<T extends K, K> = Omit<T, keyof K>;
/** Componentes reescritos de la documentacion de React que solo utilizan datos props inyectados */
function CommentList({ data }: WithDataProps<typeof comments>) {
return (
<div>
{data.map((comment: CommentType) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
interface BlogPostProps extends WithDataProps<string> {
id: number;
// children: ReactNode;
}
function BlogPost({ data, id }: BlogPostProps) {
return (
<div key={id}>
<TextBlock text={data} />;
</div>
);
}
Ejemplo de un HOC de la documentacion de React traducido a TypeScript
// Estos son los props que seran inyectados por el HOC
interface WithDataProps<T> {
data: T; // data es generico
}
// T es el tipo de data
// P son los props del componente envuelto que es inferido
// C es la interfaz real del component envuelto (utilizado para tomar los defaultProps)
export function withSubscription<T, P extends WithDataProps<T>, C>(
// this type allows us to infer P, but grab the type of WrappedComponent separately without it interfering with the inference of P
WrappedComponent: React.JSXElementConstructor<P> & C,
// selectData is a functor for T
// props is Readonly because it's readonly inside of the class
selectData: (
dataSource: typeof DataSource,
props: Readonly<JSX.LibraryManagedAttributes<C, Omit<P, "data">>>
) => T
) {
// the magic is here: JSX.LibraryManagedAttributes will take the type of WrapedComponent and resolve its default props
// against the props of WithData, which is just the original P type with 'data' removed from its requirements
type Props = JSX.LibraryManagedAttributes<C, Omit<P, "data">>;
type State = {
data: T;
};
return class WithData extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount = () => DataSource.addChangeListener(this.handleChange);
componentWillUnmount = () =>
DataSource.removeChangeListener(this.handleChange);
handleChange = () =>
this.setState({
data: selectData(DataSource, this.props)
});
render() {
// the typing for spreading this.props is... very complex. best way right now is to just type it as any
// data will still be typechecked
return (
<WrappedComponent data={this.state.data} {...(this.props as any)} />
);
}
};
// return WithData;
}
/** HOC usage with Components */
export const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource: DataType) => DataSource.getComments()
);
export const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource: DataType, props: Omit<BlogPostProps, "data">) =>
DataSource.getBlogPost(props.id)
);
Docs Example: Don’t Mutate the Original Component. Use Composition.
This is pretty straightforward - make sure to assert the passed props as T
due to the TS 3.2 bug.
function logProps<T>(WrappedComponent: React.ComponentType<T>) {
return class extends React.Component {
componentWillReceiveProps(
nextProps: React.ComponentProps<typeof WrappedComponent>
) {
console.log("Current props: ", this.props);
console.log("Next props: ", nextProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...(this.props as T)} />;
}
};
}
Ejemplo de documentacion: [Pasa los props no relacionados al componente envuelto]Pass Unrelated Props Through to the Wrapped Component
No se necesitan consejos especificos de Typescript aqui.
Ejemplo de documentacion: Maximizar la componibilidad
los HOC pueden tomar la forma de functiones que retornan Componentes de Orden Superior que devuelven Componentes
la funcion connect
de react-redux
tiene una serie de sobrecargas del que puedes obtener inspiracion []
connect
from react-redux
has a number of overloads you can take inspiration fuente.
Construiremos nuestro propio connect
para entender los HOC:
Variables a las que se hace referencia en el siguiente ejemplo:
/** tipos de utilidad que utilizamos */
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
/** datos ficticios */
type CommentType = { text: string; id: number };
const comments: CommentType[] = [
{
text: "comment1",
id: 1
},
{
text: "comment2",
id: 2
}
];
/** componentes ficticios que reciben cualquier cosa */
const Comment = (_: any) => null;
/** Componentes reescritos de la documentacion de React que solo utilizan props de datos inyectados **/
function CommentList({ data }: WithSubscriptionProps<typeof comments>) {
return (
<div>
{data.map((comment: CommentType) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
const commentSelector = (_: any, ownProps: any) => ({
id: ownProps.id
});
const commentActions = () => ({
addComment: (str: string) => comments.push({ text: str, id: comments.length })
});
const ConnectedComment = connect(
commentSelector,
commentActions
)(CommentList);
// prop que son inyectadas por el HOC.
interface WithSubscriptionProps<T> {
data: T;
}
function connect(mapStateToProps: Function, mapDispatchToProps: Function) {
return function<T, P extends WithSubscriptionProps<T>, C>(
WrappedComponent: React.ComponentType<T>
) {
type Props = JSX.LibraryManagedAttributes<C, Omit<P, "data">>;
// Creando el componente interno. El tipo de propiedades calculadas, es donde ocurre la magia
return class ComponentWithTheme extends React.Component<Props> {
public render() {
// Obten los props que desea inyectar. Esto podria hacerse con context
const mappedStateProps = mapStateToProps(this.state, this.props);
const mappedDispatchProps = mapDispatchToProps(this.state, this.props);
// this props viene despues de tal modo que anula los predeterminados
return (
<WrappedComponent
{...this.props}
{...mappedStateProps}
{...mappedDispatchProps}
/>
);
}
};
};
}
Ver en el TypeScript Playground
Ejemplo de documentacion: Envuelve el nombre a mostrar para una depuración fácil
Este es bastante sencillo
interface WithSubscriptionProps {
data: any;
}
function withSubscription<
T extends WithSubscriptionProps = WithSubscriptionProps
>(WrappedComponent: React.ComponentType<T>) {
class WithSubscription extends React.Component {
/* ... */
public static displayName = `WithSubscription(${getDisplayName(
WrappedComponent
)})`;
}
return WithSubscription;
}
function getDisplayName<T>(WrappedComponent: React.ComponentType<T>) {
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}
No escrito: seccion de consideraciones
- No utilice HOCs dentro del metodo render
- Los metodos estaticos deben ser copiados
- Las Refs no son pasadas