Skip to content

Latest commit

 

History

History
531 lines (423 loc) · 21.4 KB

File metadata and controls

531 lines (423 loc) · 21.4 KB
react + ts logo

Cheatsheets para desarrolladores experimentados de React que estan empezando con TypeScript

Básico | Avanzado | Migrando | HOC | 中文翻译 | ¡Contribuir! | ¡Preguntas!


HOC Cheatsheet

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

HOC Cheatsheet Tabla de Contenido

Expandir Tabla de Contenido

Sección 0: Ejemplo completo de un HOC

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} />;
  };
}

Usando forwardRef

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.

Seccion 1: Documentacion de React sobre HOC en TypeScript

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>
  );
}

Ver en TypeScript Playground

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)
);

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

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 utilice HOCs dentro del metodo render
  • Los metodos estaticos deben ser copiados
  • Las Refs no son pasadas

Seccion 2: Excluyendo Props

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?

  1. Obtenemos los tipos del componente: keyof T
  2. 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
  3. (Opcional) Utilice los tipo de interseccion si tiene mas para excluir: Exclude<keyof T, 'owner' | 'otherprop' | 'moreprop'>
  4. 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 }
  5. (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>>
  6. 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!

Aprende más

Tendremos que extraer las lecciones de aqui en el futuro pero aqui estan: