Skip to content

Latest commit

 

History

History
444 lines (358 loc) · 18.1 KB

File metadata and controls

444 lines (358 loc) · 18.1 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