
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
Esto es cubierto en la seccion 1 pero aqui nos centraremos en el ya que es un problema muy comun. Los HOC a menudo inyectan props a componentes pre-fabricados. El problema que queremos resolver es que el component envuelto en HOC exponga un tipo que refleje el area de superficie reducida de los props - sin tener que volver a escribir manualmente el HOC cada vez. Esto implica algunos genericos, afortunadamente con algunas utilidades auxiliares.
Digamos que tenemos un componente
type DogProps {
name: string
owner: string
}
function Dog({name, owner}: DogProps) {
return <div> Woof: {name}, Owner: {owner}</div>
}
Y tenemos un HOC withOwner
que inyecta el owner
:
And we have a withOwner
HOC that injects the owner
:
const OwnedDog = withOwner("swyx")(Dog);
Queremos escribir withOwner
de tal modo que pase por los tipos de cualquier component como Dog
, en el tipo de OwnedDog
, menos la propiedad owner
que inyecta:
typeof OwnedDog; // queremos que sea igual a { name: string }
<Dog name="fido" owner="swyx" />; // este debe estar bien
<OwnedDog name="fido" owner="swyx" />; // este debe tener un typeError
<OwnedDog name="fido" />; // este debe estar bien
// y el HOC debe ser reusable por tipos de props totalmente diferentes!
type CatProps = {
lives: number;
owner: string;
};
function Cat({ lives, owner }: CatProps) {
return (
<div>
{" "}
Meow: {lives}, Owner: {owner}
</div>
);
}
const OwnedCat = withOwner("swyx")(Cat);
<Cat lives={9} owner="swyx" />; // este debe estar bien
<OwnedCat lives={9} owner="swyx" />; // este debe tener un typeError
<OwnedCat lives={9} />; // este debe estar bien
Entonces, como escribirmos withOwner
?
- Obtenemos los tipos del componente:
keyof T
- Nosotros
Exclude
las propiedades que queremos encamascarar:Exclude<keyof T, 'owner'>
, esto te deje con una lista de nombre de propiedades que quieres en el componente envuelto ejm:name
- (Opcional) Utilice los tipo de interseccion si tiene mas para excluir:
Exclude<keyof T, 'owner' | 'otherprop' | 'moreprop'>
- Los nombres de las propiedades no son exactamente iguales a las propiedades en si, los cuales tambien tienen un tipo asociado. Asi que utilizamos esta lista generada de nombre para elegir
Pick
de los props originales:Pick<keyof T, Exclude<keyof T, 'owner'>>
, this leaves you with the new, filtered props, e.g.{ name: string }
- (opcional) En lugar de escribir esto manualmente cada vez, podemos utilizar esta utilidad:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
- Ahora escribimos el HOC como un funcion generica:
function withOwner(owner: string) {
return function<T extends { owner: string }>(
Component: React.ComponentType<T>
) {
return function(props: Omit<T, "owner">): React.ReactNode {
return <Component owner={owner} {...props} />;
};
};
}
Nota: el de arriba es un ejemplo incompleto y no funcional. PR una solucion!
Tendremos que extraer las lecciones de aqui en el futuro pero aqui estan: