1654632000
En esta guía detallada (y explicativa), discutiré cómo construir componentes React polimórficos fuertemente tipados con TypeScript.
Como puede ver, esto es bastante largo, así que siéntase libre de saltearlo. Si desea seguir, marque el repositorio de código oficial en mi GitHub como referencia.
Existe una probabilidad distinta de cero de que ya haya usado un componente polimórfico. Las bibliotecas de componentes de código abierto normalmente implementan algún tipo de componente polimórfico.
Consideremos algunos con los que puede estar familiarizado: el as
accesorio Chakra UI y MUI component
.
as
UI¿Cómo implementa Chakra UI accesorios polimórficos? La respuesta es exponer un as
accesorio. El as
accesorio se pasa a un componente para determinar qué elemento contenedor debería representar eventualmente.
Todo lo que necesita hacer para usar el as
accesorio es pasarlo al componente, que en este caso es Box
:
<Box as='button' borderRadius='md' bg='tomato' color='white' px={4} h={8}>
Button
</Box>
Ahora, el componente representará un button
elemento.
Si cambiaste el as
accesorio a un h1
:
<Box as="h1"> Hello </Box>
Ahora, el Box
componente representa un h1
:
¡Eso es un componente polimórfico en el trabajo! Se puede representar en elementos completamente únicos, todo al pasar un solo accesorio.
component
Similar a Chakra UI, MUI permite un accesorio polimórfico llamado component
, que se implementa de manera similar: lo pasa a un componente y establece el elemento o componente personalizado que le gustaría representar.
Aquí hay un ejemplo de los documentos oficiales :
<List component="nav">
<ListItem button>
<ListItemText primary="Trash" />
</ListItem>
</List>
List
se pasa un componente prop de nav
; cuando esto se represente, generará un nav
elemento contenedor.
Otro usuario puede usar el mismo componente, pero no para la navegación; en su lugar, es posible que deseen generar una lista de tareas pendientes:
<List component="ol">
...
</List>
Y en este caso, List
representará un elemento de lista ordenada ol
.
¡Hablando de flexibilidad! Consulte un resumen de los casos de uso de los componentes polimórficos.
Como verá en las siguientes secciones de este artículo, los componentes polimórficos son poderosos. Además de aceptar un accesorio de un tipo de elemento, también pueden aceptar componentes personalizados como accesorios.
Esto se discutirá en una próxima sección de este artículo. Por ahora, ¡construyamos nuestro primer componente polimórfico!
Al contrario de lo que pueda pensar, construir su primer componente polimórfico es bastante sencillo. Aquí hay una implementación básica:
const MyComponent = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta aquí que el accesorio polimórfico as
es similar a la interfaz de usuario de Chakra. Este es el accesorio que exponemos para controlar el elemento de representación del componente polimórfico.
En segundo lugar, tenga en cuenta que el as
accesorio no se representa directamente. Lo siguiente estaría mal:
const MyComponent = ({ as, children }) => {
// wrong render below
return <as>{children}</as>;
};
Al renderizar un tipo de elemento en tiempo de ejecución , primero debe asignarlo a una variable en mayúsculas y luego renderizar la variable en mayúsculas.
Ahora puede continuar y usar este componente de la siguiente manera:
<MyComponent as="button">Hello Polymorphic!<MyComponent>
<MyComponent as="div">Hello Polymorphic!</MyComponent>
<MyComponent as="span">Hello Polymorphic!</MyComponent>
<MyComponent as="em">Hello Polymorphic!</MyComponent>
Tenga en cuenta que el as
accesorio diferente se pasa a los componentes representados arriba.
La implementación de la sección anterior, aunque bastante estándar, tiene muchos inconvenientes. Exploremos algunos de estos.
as
accesorio puede recibir elementos HTML no válidosActualmente, es posible que un usuario escriba lo siguiente:
<MyComponent as="emmanuel">Hello Wrong Element</MyComponent>
El as
prop pasado aquí es emmanuel
. Emmanuel es obviamente un elemento HTML incorrecto, pero el navegador también intenta representar este elemento.
Una experiencia de desarrollo ideal es mostrar algún tipo de error durante el desarrollo. Por ejemplo, un usuario puede cometer un error tipográfico simple, divv
en lugar de div
, y no obtendría ninguna indicación de lo que está mal.
Considere el siguiente uso de componentes:
<MyComponent as="span" href="https://www.google.com">
Hello Wrong Attribute
</MyComponent>
Un consumidor puede pasar un span
elemento a la as
propiedad y href
también una propiedad.
Esto es técnicamente inválido. Un span
elemento no toma (y no debería tomar) un href
atributo. Esa es una sintaxis HTML no válida. Sin embargo, un consumidor del componente que hemos creado aún podría escribir esto y no recibir errores durante el desarrollo.
Considere la implementación simple nuevamente:
const MyComponent = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Los únicos accesorios que acepta este componente son as
y children
, nada más. No hay compatibilidad con atributos ni siquiera as
para accesorios de elementos válidos, es decir, si as
fuera un elemento de anclaje a
, también deberíamos admitir pasar an href
al componente.
<MyComponent as="a" href="...">A link </MyComponent>
Aunque href
se pasa en el ejemplo anterior, la implementación del componente no recibe otros apoyos. Sólo as
y children
se deconstruyen.
Sus pensamientos iniciales pueden ser seguir adelante y distribuir todos los demás apoyos pasados al componente de la siguiente manera:
const MyComponent = ({ as, children, ...rest }) => {
const Component = as || "span";
return <Component {...rest}>{children}</Component>;
};
Esto parece una solución decente, pero ahora resalta el segundo problema mencionado anteriormente. Los atributos incorrectos ahora también se transmitirán al componente.
Considera lo siguiente:
<MyComponent as="span" href="https://www.google.com">
Hello Wrong Attribute
</MyComponent>
Y tenga en cuenta el marcado renderizado eventual:
A span
con un href
es HTML no válido.
En resumen, los problemas actuales con nuestra implementación simple son insatisfactorios porque:
¿Cómo resolvemos estas preocupaciones? Para ser claros, no hay una varita mágica para agitar aquí. Sin embargo, aprovecharemos TypeScript para garantizar que construya componentes polimórficos fuertemente tipados.
Al finalizar, los desarrolladores que usen sus componentes evitarán los errores de tiempo de ejecución anteriores y, en su lugar, los detectarán durante el desarrollo o el tiempo de compilación, todo gracias a TypeScript.
Si está leyendo esto, un requisito previo es que ya sepa algo de TypeScript, al menos los conceptos básicos. Si no tiene idea de qué es TypeScript, le recomiendo que lea este documento primero.
En esta sección, utilizaremos TypeScript para resolver los problemas antes mencionados y construir componentes polimórficos fuertemente tipados.
Los primeros dos requisitos con los que comenzaremos incluyen:
as
propiedad no debe recibir cadenas de elementos HTML no válidas.En la siguiente sección, presentaremos los genéricos de TypeScript para hacer que nuestra solución sea más robusta, fácil de usar para desarrolladores y digna de producción.
as
propiedad solo reciba cadenas de elementos HTML válidasEsta es nuestra solución actual:
const MyComponent = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Para que las próximas secciones de esta guía sean prácticas, cambiaremos el nombre del componente de MyComponent
a Text
y supondremos que estamos construyendo un Text
componente polimórfico.
const Text = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Ahora, con su conocimiento de los genéricos, se vuelve obvio que es mejor representar as
con un tipo genérico, es decir, un tipo de variable basado en lo que el usuario pase.
Sigamos adelante y demos el primer paso de la siguiente manera:
export const Text = <C>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta cómo se define el genérico C
y luego se transmite en la definición de tipo para el accesorio as
.
Sin embargo, si escribió este código aparentemente perfecto, TypeScript gritará numerosos errores con más líneas rojas onduladas de lo que le gustaría.
Lo que está pasando aquí es una falla en la sintaxis de los genéricos en los .tsx
archivos. Hay dos formas de resolver esto.
Esta es la sintaxis para declarar varios genéricos. Una vez que haga esto, el compilador de TypeScript entiende claramente su intención y los errores desaparecen.
// note the comma after "C" below
export const Text = <C,>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
La segunda opción es restringir el genérico como mejor le parezca. Para empezar, puede usar el unknown
tipo de la siguiente manera:
// note the extends keyword below
export const Text = <C extends unknown>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Por ahora, me ceñiré a la segunda solución porque está más cerca de nuestra solución final. Sin embargo, en la mayoría de los casos, uso la sintaxis genérica múltiple y solo agrego una coma.
Sin embargo, con nuestra solución actual, obtenemos otro error de TypeScript:
El tipo de elemento JSX 'Componente' no tiene ninguna construcción ni firma de llamada. ts(2604)
Esto es similar al error que tuvimos cuando trabajamos con la echoLength
función. Al igual que acceder a la length
propiedad de un tipo de variable desconocido, se puede decir lo mismo aquí: tratar de representar cualquier tipo genérico como un componente React válido no tiene sentido.
Necesitamos restringir el genérico solo para que se ajuste al molde de un tipo de elemento React válido.
Para lograr esto, aprovecharemos el tipo React interno: React.ElementType
y nos aseguraremos de que el genérico esté restringido para adaptarse a ese tipo:
// look just after the extends keyword
export const Text = <C extends React.ElementType>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta que si está utilizando una versión anterior de React, es posible que deba importar una versión más nueva de React de la siguiente manera:
import React from 'react'
¡Con esto, no tenemos más errores!
Ahora, si continúa y usa este componente de la siguiente manera, funcionará bien:
<Text as="div">Hello Text world</Text>
Sin embargo, si pasa una propiedad no válida as
, ahora obtendrá un error de TypeScript apropiado. Considere el siguiente ejemplo:
<Text as="emmanuel">Hello Text world</Text>
Y el error arrojado:
El tipo '”emmanuel”' no se puede asignar al tipo 'ElementType | indefinido'.
¡Esto es excelente! Ahora tenemos una solución que no acepta galimatías para el as
accesorio y también evitará errores tipográficos desagradables, por ejemplo, divv
en lugar de div
.
¡Esta es una experiencia de desarrollador mucho mejor!
Al resolver este segundo caso de uso, llegará a apreciar cuán poderosos son realmente los genéricos. Primero, entendamos lo que estamos tratando de lograr aquí.
Una vez que recibimos un as
tipo genérico, queremos asegurarnos de que los accesorios restantes pasados a nuestro componente sean relevantes, según el as
accesorio.
Entonces, por ejemplo, si un usuario pasó un as
accesorio de img
, ¡ href
querríamos ser igualmente un accesorio válido!
Para darle una idea de cómo lograríamos esto, eche un vistazo al estado actual de nuestra solución:
export const Text = <C extends React.ElementType>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
La propiedad de este componente ahora está representada por el tipo de objeto:
{
as?: C;
children: React.ReactNode;
}
En pseudocódigo, lo que nos gustaría sería lo siguiente:
{
as?: C;
children: React.ReactNode;
} & {
...otherValidPropsBasedOnTheValueOfAs
}
Este requisito es suficiente para dejar a uno aferrado a un clavo ardiendo. No podemos escribir una función que determine los tipos apropiados en función del valor de as
, y no es inteligente enumerar un tipo de unión manualmente.
Bueno, ¿qué pasaría si hubiera un tipo proporcionado React
que actuara como una "función" que devolverá tipos de elementos válidos en función de lo que le pases?
Antes de presentar la solución, refactoricemos un poco. Saquemos los accesorios del componente en un tipo separado:
// See TextProps pulled out below
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
}
export const Text = <C extends React.ElementType>({
as,
children,
}: TextProps<C>) => { // see TextProps used
const Component = as || "span";
return <Component>{children}</Component>;
};
Lo importante aquí es observar cómo se transmite el genérico a TextProps<C>
. Similar a una llamada de función en JavaScript, pero con llaves angulares.
La varita mágica aquí es aprovechar el React.ComponentPropsWithoutRef
tipo como se muestra a continuación:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; // look here
export const Text = <C extends React.ElementType>({
as,
children,
}: TextProps<C>) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta que estamos introduciendo una intersección aquí. Esencialmente, estamos diciendo que el tipo de TextProps
es un tipo de objeto que contiene as
, children
y algunos otros tipos representados por React.ComponentPropsWithoutRef
.
Si lee el código, quizás se haga evidente lo que está pasando aquí.
Según el tipo de as
, representado por el genérico C
, React.componentPropsWithoutRef
devolverá accesorios de componente válidos que se correlacionan con el atributo de cadena pasado al as
accesorio.
Hay un punto más importante a tener en cuenta.
Si recién comenzó a escribir y confía en IntelliSense de su editor, se dará cuenta de que hay tres variantes del React.ComponentProps...
tipo:
React.ComponentProps
React.ComponentPropsWithRef
React.ComponentPropsWithoutRef
Si intentara usar el primero, ComponentProps
vería una nota relevante que dice:
Preferir
ComponentPropsWithRef
, siref
se reenvía oComponentPropsWithoutRef
cuando no se admiten referencias.
Esto es precisamente lo que hemos hecho. Por ahora, ignoraremos el caso de uso para admitir un ref
accesorio y nos ceñiremos a ComponentPropsWithoutRef
.
¡Ahora, probemos la solución!
Si sigue adelante y usa este componente incorrectamente, por ejemplo, pasando un as
accesorio válido con otros accesorios incompatibles, obtendrá un error.
<Text as="div" href="www.google.com">Hello Text world</Text>
Un valor de div
es perfectamente válido para la as
propiedad, pero a div
no debería tener un href
atributo.
Eso está mal, y TypeScript lo detectó correctamente con el error: Property 'href' does not exist on type ...
.
¡Esto es genial! Tenemos una solución aún mejor y más robusta.
Finalmente, asegúrese de pasar otros accesorios al elemento renderizado:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
export const Text = <C extends React.ElementType>({
as,
children,
...restProps, // look here
}: TextProps<C>) => {
const Component = as || "span";
// see restProps passed
return <Component {...restProps}>{children}</Component>;
};
Avancemos.
as
atributos predeterminadosConsidere nuevamente nuestra solución actual:
export const Text = <C extends React.ElementType>({
as,
children,
...restProps
}: TextProps<C>) => {
const Component = as || "span"; // look here
return <Component {...restProps}>{children}</Component>;
};
En particular, preste atención a dónde se proporciona un elemento predeterminado si as
se omite la propiedad.
const Component = as || "span"
Esto se representa correctamente en el mundo de JavaScript mediante la implementación: si as
es opcional, por defecto será un span
.
La pregunta es, ¿cómo maneja TypeScript este caso cuando as
no se pasa? ¿Estamos igualmente pasando un tipo predeterminado?
Bueno, la respuesta es no, pero a continuación hay un ejemplo práctico. Digamos que siguió adelante para usar el Text
componente de la siguiente manera:
<Text>Hello Text world</Text>
Tenga en cuenta que no hemos pasado ningún as
accesorio aquí. ¿TypeScript estará al tanto de los accesorios válidos para este componente?
Sigamos adelante y agreguemos un href
:
<Text href="https://www.google.com">Hello Text world</Text>
Si continúa y hace esto, no obtendrá errores. Eso es malo.
A span
no debería recibir un href
accesorio/atributo. Si bien tenemos un valor predeterminado span
en la implementación, TypeScript desconoce este valor predeterminado. Arreglemos esto con una asignación predeterminada simple y genérica:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
/**
* See default below. TS will treat the rendered element as a
span and provide typings accordingly
*/
export const Text = <C extends React.ElementType = "span">({
as,
children,
...restProps
}: TextProps<C>) => {
const Component = as || "span";
return <Component {...restProps}>{children}</Component>;
};
El bit importante se destaca a continuación:
<C extends React.ElementType = "span">
Et voila! El ejemplo anterior que teníamos ahora debería arrojar un error cuando pasa href
al Text
componente sin un as
accesorio.
El error debería decir: Property 'href' does not exist on type ...
.
Nuestra solución actual es mucho mejor que con la que comenzamos. Date una palmadita en la espalda por haber llegado tan lejos; a partir de aquí solo se vuelve más interesante.
El caso de uso para atender en esta sección es muy aplicable en el mundo real. Existe una gran posibilidad de que si está creando algún tipo de componente, ese componente también incluirá algunos accesorios específicos que son exclusivos del componente.
Nuestra solución actual tiene en cuenta el as
, children
y los otros accesorios de componentes basados en el as
accesorio. Sin embargo, ¿qué pasaría si quisiéramos que este componente manejara sus propios accesorios?
Hagamos esto práctico. Haremos que el Text
componente reciba un color
accesorio. El color
aquí será cualquiera de los colores del arco iris o black
.
Continuaremos y representaremos esto de la siguiente manera:
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
A continuación, debemos definir el color
accesorio en el TextProps
objeto de la siguiente manera:
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black"; // look here
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
Antes de continuar, hagamos un poco de refactorización. Representemos los accesorios reales del Text
componente por un Props
objeto, y escribamos específicamente solo los accesorios específicos de nuestro componente en el TextProps
objeto.
Esto se volverá obvio, como verás a continuación:
// new "Props" type
type Props <C extends React.ElementType> = TextProps<C>
export const Text = <C extends React.ElementType = "span">({
as,
children,
...restProps,
}: Props<C>) => {
const Component = as || "span";
return <Component {...restProps}>{children}</Component>;
};
Ahora vamos a limpiar TextProps
:
// before
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black"; // look here
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
// after
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black";
};
Ahora, TextProps
solo debe contener los accesorios que son específicos de nuestro Text
componente: as
y color
.
Ahora debemos actualizar la definición de Props
para incluir los tipos que hemos eliminado de TextProps
, es decir, children
y React.ComponentPropsWithoutRef<C>
.
Para la children
utilería, aprovecharemos la React.PropsWithChildren
utilería.
PropsWithChildren
es bastante fácil de razonar. Le pasa los accesorios de su componente y le inyectará la definición de accesorios para niños:
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>>
Tenga en cuenta cómo usamos las llaves angulares; esta es la sintaxis para pasar genéricos. Esencialmente, React.PropsWithChildren
acepta los accesorios de su componente como un genérico y lo aumenta con el children
accesorio. ¡Dulce!
Para React.ComponentPropsWithoutRef<C>
, seguiremos adelante y aprovecharemos un tipo de intersección aquí:
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
Y aquí está la solución actual completa:
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black";
};
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
export const Text = <C extends React.ElementType = "span">({
as,
children,
}: Props<C>) => {
const Component = as || "span";
return <Component> {children} </Component>;
};
Sé que esto puede parecer mucho, pero cuando miras más de cerca, todo tendrá sentido. ¡Realmente es solo juntar todo lo que has aprendido hasta ahora!
Habiendo hecho este refactor necesario, ahora podemos continuar con nuestra solución. Lo que tenemos ahora realmente funciona. Hemos escrito explícitamente el color
accesorio, y puede usarlo de la siguiente manera:
<Text color="violet">Hello world</Text>
Solo hay una cosa con la que no me siento particularmente cómodo: color
resulta ser también un atributo válido para numerosas etiquetas HTML, como era el caso antes de HTML5. Entonces, si lo eliminamos color
de nuestra definición de tipo, se aceptará como cualquier cadena válida.
Vea abajo:
type TextProps<C extends React.ElementType> = {
as?: C;
// remove color from the definition here
};
Ahora, si continúa usando Text
como antes, es igualmente válido:
<Text color="violet">Hello world</Text>
La única diferencia aquí es cómo se escribe. color
ahora está representado por la siguiente definición:
color?: string | undefined
Nuevamente, ¡esta NO es una definición que escribimos en nuestros tipos!
Este es un tipo de HTML predeterminado, donde color
es un atributo válido para la mayoría de los elementos HTML. Consulte esta pregunta de desbordamiento de pila para obtener más contexto.
Ahora, hay dos maneras de ir aquí. El primero es mantener nuestra solución inicial, donde declaramos explícitamente la color
propiedad:
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black"; // look here
};
Podría decirse que la segunda opción proporciona algo más de seguridad tipográfica. Para lograr esto, debes darte cuenta de dónde color
vino la definición predeterminada anterior: el React.ComponentPropsWithoutRef<C>
. Esto es lo que agrega otros accesorios según el tipo as
.
Entonces, con esta información, podemos eliminar explícitamente cualquier definición que exista en nuestros tipos de componentes de React.ComponentPropsWithoutRef<C>
.
Esto puede ser difícil de entender antes de verlo en acción, así que vayamos paso a paso.
React.ComponentPropsWithoutRef<C>
, como se indicó anteriormente, contiene todas las demás propiedades válidas basadas en el tipo de as
, por ejemplo, href
, color
, etc., donde estos tipos tienen todas sus propias definiciones, por ejemplo, color?: string | undefined
, etc.:
Es posible que algunos valores que existen en React.ComponentPropsWithoutRef<C>
también existan en nuestra definición de tipo de accesorios de componente. En nuestro caso, color
existe en ambos!
En lugar de confiar en nuestra color
definición para anular lo que proviene de React.ComponentPropsWithoutRef<C>
, eliminaremos explícitamente cualquier tipo que también exista en nuestra definición de tipos de componentes.
Por lo tanto, si existe algún tipo en nuestra definición de tipos de componentes, eliminaremos explícitamente esos tipos de React.ComponentPropsWithoutRef<C>
.
React.ComponentPropsWithoutRef<C>
Esto es lo que teníamos antes:
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
En lugar de tener un tipo de intersección donde agregamos todo lo que viene React.ComponentPropsWithoutRef<C>
, seremos más selectivos. Usaremos los tipos de utilidad Omit
y keyof
TypeScript para realizar algo de magia de TypeScript.
Echar un vistazo:
// before
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
// after
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
Esta es la parte importante:
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
Omit
toma en dos genéricos. El primero es un tipo de objeto y el segundo es una unión de tipos que le gustaría "omitir" del tipo de objeto.
Aquí está mi ejemplo favorito. Considere un Vowel
tipo de objeto de la siguiente manera:
type Vowels = {
a: 'a',
e: 'e',
i: 'i',
o: 'o',
u: 'u'
}
Este es un tipo de objeto de clave y valor. Digamos que quería derivar un nuevo tipo de Vowels
called VowelsInOhans
.
Bueno, sé que el nombre Ohans
contiene dos vocales o
y a
. En lugar de declarar manualmente estos:
type VowelsInOhans = {
a: 'a',
o: 'o'
}
Puedo seguir adelante para aprovechar Omit
de la siguiente manera:
type VowelsInOhans = Omit<Vowels, 'e' | 'i' | 'u'>
Omit
"omitirá" las teclas e
, i
y u
del tipo de objeto Vowels
.
Por otro lado, el operador de TypeScriptkeyof
funciona como te imaginas. Piense Object.keys
en JavaScript: dado un object
tipo, keyof
devolverá un tipo de unión de las claves del objeto.
¡Uf! Eso es un bocado. Aquí hay un ejemplo:
type Vowels = {
a: 'a',
e: 'e',
i: 'i',
o: 'o',
u: 'u'
}
type Vowel = keyof Vowels
Ahora, Vowel
será un tipo de unión de las claves de Vowels
, es decir:
type Vowel = 'a' | 'e' | 'i' | 'o' | 'u'
Si los junta y echa un segundo vistazo a nuestra solución, todo encajará muy bien:
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
keyof TextProps<C>
devuelve un tipo de unión de las claves de nuestro componente props. Esto a su vez se pasa a Omit
para omitirlos de React.ComponentPropsWithoutRef<C>
.
¡Dulce!
Para terminar, avancemos y pasemos el color
apoyo al elemento renderizado:
export const Text = <C extends React.ElementType = "span">({
as,
color, // look here
children,
...restProps
}: Props<C>) => {
const Component = as || "span";
// compose an inline style object
const style = color ? { style: { color } } : {};
// pass the inline style to the rendered element
return (
<Component {...restProps} {...style}>
{children}
</Component>
);
};
Finalmente tenemos una solución que funciona bien. Ahora, sin embargo, demos un paso más allá.
La solución que tenemos funciona muy bien para nuestro Text
componente. Sin embargo, ¿qué sucede si prefiere tener una solución que pueda reutilizar en cualquier componente de su elección, de modo que pueda tener una solución reutilizable para cada caso de uso?
Empecemos. Primero, aquí está la solución completa actual sin anotaciones:
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black";
};
type Props<C extends React.ElementType> = React.PropsWithChildren<
TextProps<C>
> &
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
export const Text = <C extends React.ElementType = "span">({
as,
color,
children,
...restProps
}: Props<C>) => {
const Component = as || "span";
const style = color ? { style: { color } } : {};
return (
<Component {...restProps} {...style}>
{children}
</Component>
);
};
Sucinto y práctico.
Si hicimos esto reutilizable, entonces tiene que funcionar para cualquier componente. Esto significa eliminar el codificado TextProps
y representarlo con un genérico, para que cualquiera pueda pasar los accesorios de componentes que necesite.
Actualmente, representamos nuestros accesorios de componentes con la definición Props<C>
. Donde C
representa el tipo de elemento pasado para la as
propiedad.
Ahora cambiaremos eso a:
// before
Props<C>
// after
PolymorphicProps<C, TextProps>
PolymorphicProps
representa el tipo de utilidad que escribiremos en breve. Sin embargo, tenga en cuenta que esto acepta dos tipos genéricos, siendo el segundo los accesorios de componente en cuestión: TextProps
.
Continúe y defina el PolymorphicProps
tipo:
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = {} // empty object for now
La definición anterior debe ser comprensible. C
representa el tipo de elemento pasado en as
, y Props
es el componente real props, TextProps
.
Primero, dividamos lo TextProps
que teníamos antes en lo siguiente:
type AsProp<C extends React.ElementType> = {
as?: C;
};
type TextProps = { color?: Rainbow | "black" };
Entonces, hemos separado el AsProp
del TextProps
. Para ser justos, representan dos cosas diferentes. Esta es una mejor representación.
Ahora, cambiemos la PolymorphicComponentProp
definición de la utilidad para incluir la as
propiedad, las propiedades del componente y children
la propiedad, como lo hemos hecho en el pasado:
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>
Estoy seguro de que ahora entiendes lo que está pasando aquí: tenemos un tipo de intersección de Props
(que representa los accesorios del componente) y AsProp
representa el as
accesorio. Todos estos se pasan PropsWithChildren
para agregar la children
definición de prop. ¡Excelente!
Ahora, necesitamos incluir el bit donde agregamos la React.ComponentPropsWithoutRef<C>
definición. Sin embargo, debemos recordar omitir accesorios que existen en nuestra definición de componente.
Vamos a encontrar una solución robusta.
Escriba un nuevo tipo que solo comprenda los accesorios que nos gustaría omitir. Es decir, las teclas del AsProp
y los accesorios del componente también.
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
¿Recuerdas el keyof
tipo de utilidad?
PropsToOmit
ahora comprenderá un tipo de unión de los accesorios que queremos omitir, que es cada accesorio de nuestro componente representado por P
y el accesorio polimórfico real as
, representado por AsProps
.
Ponga todo esto junto muy bien en la PolymorphicComponentProp
definición:
type AsProp<C extends React.ElementType> = {
as?: C;
};
// before
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>
// after
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>,
PropsToOmit<C, Props>>;
Lo importante aquí es que hemos agregado la siguiente definición:
Omit<React.ComponentPropsWithoutRef<C>,
PropsToOmit<C, Props>>;
Esto básicamente omite los tipos correctos de React.componentPropsWithoutRef
. ¿Todavía recuerdas cómo omit
funciona ?
Por simple que parezca, ¡ahora tiene una solución que puede reutilizar en múltiples componentes en diferentes proyectos!
Aquí está la implementación completa:
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
Ahora podemos continuar y usar PolymorphicComponentProp
en nuestro Text
componente de la siguiente manera:
export const Text = <C extends React.ElementType = "span">({
as,
color,
children,
// look here
}: PolymorphicComponentProp<C, TextProps>) => {
const Component = as || "span";
const style = color ? { style: { color } } : {};
return <Component {...style}>{children}</Component>;
};
¡Que agradable! Si crea otro componente, puede continuar y escribirlo así:
PolymorphicComponentProp<C, MyNewComponentProps>
¿Escuchas ese sonido? Ese es el sonido de la victoria: ¡has llegado tan lejos!
¿Recuerdas todas las referencias React.ComponentPropsWithoutRef
hasta ahora?Puntales de componentes... sin referencias. Bueno, ¡ahora es el momento de poner los árbitros!
Esta es la parte final y más compleja de nuestra solución. Necesitaré que seas paciente aquí, pero también haré todo lo posible para explicar cada paso en detalle.
Lo primero es lo primero, ¿recuerdas cómo refs
funciona React ? El concepto más importante aquí es que simplemente no pasa ref
como accesorio y espera que se transmita a su componente como cualquier otro accesorio. La forma recomendada de manejar refs
sus componentes funcionales es usar la forwardRef
función .
Comencemos con una nota práctica.
Si continúa y pasa un ref
a nuestro Text
componente ahora, obtendrá un error que dice Property 'ref' does not exist on type ...
.
// Create the ref object
const divRef = useRef<HTMLDivElement | null>(null);
...
// Pass the ref to the rendered Text component
<Text as="div" ref={divRef}>
Hello Text world
</Text>
Esto se espera.
Nuestra primera oportunidad de admitir referencias será usarlas forwardRef
en el Text
componente como se muestra a continuación:
// before
export const Text = <C extends React.ElementType = "span">({
as,
color,
children,
}: PolymorphicComponentProp<C, TextProps>) => {
...
};
// after
import React from "react";
export const Text = React.forwardRef(
<C extends React.ElementType = "span">({
as,
color,
children,
}: PolymorphicComponentProp<C, TextProps>) => {
...
}
);
Esto es esencialmente envolver el código anterior en React.forwardRef
, eso es todo.
Ahora, React.forwardRef
tiene la siguiente firma:
React.forwardRef((props, ref) ... )
Esencialmente, el segundo argumento recibido es el ref
objeto. Sigamos adelante y manejemos eso:
type PolymorphicRef<C extends React.ElementType> = unknown;
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: PolymorphicComponentProp<C, TextProps>,
// look here
ref?: PolymorphicRef<C>
) => {
...
}
);
Lo que hemos hecho aquí es agregar el segundo argumento, ref
y declarar su tipo como PolymorphicRef
, que solo apunta a unknown
por ahora.
Tenga en cuenta que PolymorphicRef
toma en el genérico C
. Esto es similar a las soluciones anteriores: el ref
objeto de a div
difiere del de a span
, por lo que debemos tener en cuenta el tipo de elemento que se pasa a la as
propiedad.
Apunte su atención al PolymorphicRef
tipo. ¿Cómo podemos obtener el ref
tipo de objeto en función de la as
propiedad?
Déjame darte una pista: React.ComponentPropsWithRef
!
Tenga en cuenta que esto dice con ref. No sin ref.
Esencialmente, si se tratara de un paquete de claves (que, de hecho, lo es), incluirá todos los accesorios de componentes relevantes según el tipo de elemento, además del objeto ref.
Entonces, si sabemos que este tipo de objeto contiene la ref
clave, también podemos obtener ese tipo de referencia haciendo lo siguiente:
// before
type PolymorphicRef<C extends React.ElementType> = unknown;
// after
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>["ref"];
Esencialmente, React.ComponentPropsWithRef<C>
devuelve un tipo de objeto, por ejemplo,
{
ref: SomeRefDefinition,
// ... other keys,
color: string
href: string
// ... etc
}
Para seleccionar solo el ref
tipo, podemos hacer esto:
React.ComponentPropsWithRef<C>["ref"];
Tenga en cuenta que la sintaxis es similar a la sintaxis del acceso a la propiedad en JavaScript, es decir, ["ref"]
. Ahora que tenemos el ref
accesorio escrito, podemos continuar y pasarlo al elemento renderizado:
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: PolymorphicComponentProp<C, TextProps>,
ref?: PolymorphicRef<C>
) => {
//...
return (
<Component {...style} ref={ref}> // look here
{children}
</Component>
);
}
);
¡Hemos hecho un progreso decente! De hecho, si continúa y verifica el uso de Text
como lo hicimos antes, no habrá más errores:
// create the ref object
const divRef = useRef<HTMLDivElement | null>(null);
...
// pass ref to the rendered Text component
<Text as="div" ref={divRef}>
Hello Text world
</Text>
Sin embargo, nuestra solución todavía no está tan fuertemente tipada como me gustaría. Avancemos y cambiemos la referencia pasada a Text
como se muestra a continuación:
// create a "button" ref object
const buttonRef = useRef<HTMLButtonElement | null>(null);
...
// pass a button ref to a "div". NB: as = "div"
<Text as="div" ref={buttonRef}>
Hello Text world
</Text>
TypeScript debería arrojar un error aquí, pero no lo hace. Estamos creando una button
referencia, pero pasándola a un div
elemento. Eso no está bien.
Si observa el tipo exacto de ref
, se ve así:
React.RefAttributes<unknown>.ref?: React.Ref<unknown>
¿Ves el unknown
de ahí? Esa es una señal de tipeo débil. Idealmente, deberíamos tener HTMLDivElement
allí para definir explícitamente el objeto ref como un div
elemento ref.
Tenemos trabajo que hacer. Primero veamos los tipos de los otros accesorios del Text
componente, que aún hacen referencia al PolymorphicComponentProp
tipo. Cambie esto a un nuevo tipo llamado PolymorphicComponentPropWithRef
. Esto será solo una unión de PolymorphicComponentProp
y el ref prop. (Lo adivinaste.)
Aquí está:
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
> = PolymorphicComponentProp<C, Props> &
{ ref?: PolymorphicRef<C> };
Esta es solo una unión de la anterior PolymorphicComponentProp
y { ref?: PolymorphicRef<C> }
.
Ahora necesitamos cambiar los accesorios del componente para hacer referencia al nuevo PolymorphicComponentPropWithRef
tipo:
// before
type TextProps = { color?: Rainbow | "black" };
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: PolymorphicComponentProp<C, TextProps>,
ref?: PolymorphicRef<C>
) => {
...
}
);
// now
type TextProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<
C,
{ color?: Rainbow | "black" }
>;
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: TextProps<C>, // look here
ref?: PolymorphicRef<C>
) => {
...
}
);
Hemos actualizado TextProps
a la referencia PolymorphicComponentPropWithRef
y eso ahora se pasa como accesorios para el Text
componente. ¡Hermoso!
Queda una última cosa por hacer: proporcione una anotación de tipo para el Text
componente. Se parece a:
export const Text : TextComponent = ...
TextComponent
es el tipo de anotación que escribiremos. Aquí está completamente escrito:
type TextComponent = <C extends React.ElementType = "span">(
props: TextProps<C>
) => React.ReactElement | null;
Este es esencialmente un componente funcional que toma TextProps
y devuelve React.ReactElement | null
, donde TextProps
es como se definió anteriormente:
type TextProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<
C,
{ color?: Rainbow | "black" }
>;
¡Con esto, ahora tenemos una solución completa!
Voy a compartir la solución completa ahora. Puede parecer abrumador al principio, pero recuerde que hemos trabajado línea por línea en todo lo que ve aquí. Léalo con esa confianza.
import React from "react";
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
// This is the first reusable type utility we built
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
// This is a new type utitlity with ref!
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
// This is the type for the "ref" only
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>["ref"];
/**
* This is the updated component props using PolymorphicComponentPropWithRef
*/
type TextProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<
C,
{ color?: Rainbow | "black" }
>;
/**
* This is the type used in the type annotation for the component
*/
type TextComponent = <C extends React.ElementType = "span">(
props: TextProps<C>
) => React.ReactElement | null;
export const Text: TextComponent = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: TextProps<C>,
ref?: PolymorphicRef<C>
) => {
const Component = as || "span";
const style = color ? { style: { color } } : {};
return (
<Component {...style} ref={ref}>
{children}
</Component>
);
}
);
¡Y ahí tienes!
Ha creado con éxito una solución robusta para manejar componentes polimórficos en React con TypeScript. Sé que no fue un viaje fácil, pero lo lograste.
Gracias por seguirnos.
Esta historia se publicó originalmente en https://blog.logrocket.com/build-strongly-typed-polymorphic-components-react-typescript/
1598839687
If you are undertaking a mobile app development for your start-up or enterprise, you are likely wondering whether to use React Native. As a popular development framework, React Native helps you to develop near-native mobile apps. However, you are probably also wondering how close you can get to a native app by using React Native. How native is React Native?
In the article, we discuss the similarities between native mobile development and development using React Native. We also touch upon where they differ and how to bridge the gaps. Read on.
Let’s briefly set the context first. We will briefly touch upon what React Native is and how it differs from earlier hybrid frameworks.
React Native is a popular JavaScript framework that Facebook has created. You can use this open-source framework to code natively rendering Android and iOS mobile apps. You can use it to develop web apps too.
Facebook has developed React Native based on React, its JavaScript library. The first release of React Native came in March 2015. At the time of writing this article, the latest stable release of React Native is 0.62.0, and it was released in March 2020.
Although relatively new, React Native has acquired a high degree of popularity. The “Stack Overflow Developer Survey 2019” report identifies it as the 8th most loved framework. Facebook, Walmart, and Bloomberg are some of the top companies that use React Native.
The popularity of React Native comes from its advantages. Some of its advantages are as follows:
Are you wondering whether React Native is just another of those hybrid frameworks like Ionic or Cordova? It’s not! React Native is fundamentally different from these earlier hybrid frameworks.
React Native is very close to native. Consider the following aspects as described on the React Native website:
Due to these factors, React Native offers many more advantages compared to those earlier hybrid frameworks. We now review them.
#android app #frontend #ios app #mobile app development #benefits of react native #is react native good for mobile app development #native vs #pros and cons of react native #react mobile development #react native development #react native experience #react native framework #react native ios vs android #react native pros and cons #react native vs android #react native vs native #react native vs native performance #react vs native #why react native #why use react native
1654632000
En esta guía detallada (y explicativa), discutiré cómo construir componentes React polimórficos fuertemente tipados con TypeScript.
Como puede ver, esto es bastante largo, así que siéntase libre de saltearlo. Si desea seguir, marque el repositorio de código oficial en mi GitHub como referencia.
Existe una probabilidad distinta de cero de que ya haya usado un componente polimórfico. Las bibliotecas de componentes de código abierto normalmente implementan algún tipo de componente polimórfico.
Consideremos algunos con los que puede estar familiarizado: el as
accesorio Chakra UI y MUI component
.
as
UI¿Cómo implementa Chakra UI accesorios polimórficos? La respuesta es exponer un as
accesorio. El as
accesorio se pasa a un componente para determinar qué elemento contenedor debería representar eventualmente.
Todo lo que necesita hacer para usar el as
accesorio es pasarlo al componente, que en este caso es Box
:
<Box as='button' borderRadius='md' bg='tomato' color='white' px={4} h={8}>
Button
</Box>
Ahora, el componente representará un button
elemento.
Si cambiaste el as
accesorio a un h1
:
<Box as="h1"> Hello </Box>
Ahora, el Box
componente representa un h1
:
¡Eso es un componente polimórfico en el trabajo! Se puede representar en elementos completamente únicos, todo al pasar un solo accesorio.
component
Similar a Chakra UI, MUI permite un accesorio polimórfico llamado component
, que se implementa de manera similar: lo pasa a un componente y establece el elemento o componente personalizado que le gustaría representar.
Aquí hay un ejemplo de los documentos oficiales :
<List component="nav">
<ListItem button>
<ListItemText primary="Trash" />
</ListItem>
</List>
List
se pasa un componente prop de nav
; cuando esto se represente, generará un nav
elemento contenedor.
Otro usuario puede usar el mismo componente, pero no para la navegación; en su lugar, es posible que deseen generar una lista de tareas pendientes:
<List component="ol">
...
</List>
Y en este caso, List
representará un elemento de lista ordenada ol
.
¡Hablando de flexibilidad! Consulte un resumen de los casos de uso de los componentes polimórficos.
Como verá en las siguientes secciones de este artículo, los componentes polimórficos son poderosos. Además de aceptar un accesorio de un tipo de elemento, también pueden aceptar componentes personalizados como accesorios.
Esto se discutirá en una próxima sección de este artículo. Por ahora, ¡construyamos nuestro primer componente polimórfico!
Al contrario de lo que pueda pensar, construir su primer componente polimórfico es bastante sencillo. Aquí hay una implementación básica:
const MyComponent = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta aquí que el accesorio polimórfico as
es similar a la interfaz de usuario de Chakra. Este es el accesorio que exponemos para controlar el elemento de representación del componente polimórfico.
En segundo lugar, tenga en cuenta que el as
accesorio no se representa directamente. Lo siguiente estaría mal:
const MyComponent = ({ as, children }) => {
// wrong render below
return <as>{children}</as>;
};
Al renderizar un tipo de elemento en tiempo de ejecución , primero debe asignarlo a una variable en mayúsculas y luego renderizar la variable en mayúsculas.
Ahora puede continuar y usar este componente de la siguiente manera:
<MyComponent as="button">Hello Polymorphic!<MyComponent>
<MyComponent as="div">Hello Polymorphic!</MyComponent>
<MyComponent as="span">Hello Polymorphic!</MyComponent>
<MyComponent as="em">Hello Polymorphic!</MyComponent>
Tenga en cuenta que el as
accesorio diferente se pasa a los componentes representados arriba.
La implementación de la sección anterior, aunque bastante estándar, tiene muchos inconvenientes. Exploremos algunos de estos.
as
accesorio puede recibir elementos HTML no válidosActualmente, es posible que un usuario escriba lo siguiente:
<MyComponent as="emmanuel">Hello Wrong Element</MyComponent>
El as
prop pasado aquí es emmanuel
. Emmanuel es obviamente un elemento HTML incorrecto, pero el navegador también intenta representar este elemento.
Una experiencia de desarrollo ideal es mostrar algún tipo de error durante el desarrollo. Por ejemplo, un usuario puede cometer un error tipográfico simple, divv
en lugar de div
, y no obtendría ninguna indicación de lo que está mal.
Considere el siguiente uso de componentes:
<MyComponent as="span" href="https://www.google.com">
Hello Wrong Attribute
</MyComponent>
Un consumidor puede pasar un span
elemento a la as
propiedad y href
también una propiedad.
Esto es técnicamente inválido. Un span
elemento no toma (y no debería tomar) un href
atributo. Esa es una sintaxis HTML no válida. Sin embargo, un consumidor del componente que hemos creado aún podría escribir esto y no recibir errores durante el desarrollo.
Considere la implementación simple nuevamente:
const MyComponent = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Los únicos accesorios que acepta este componente son as
y children
, nada más. No hay compatibilidad con atributos ni siquiera as
para accesorios de elementos válidos, es decir, si as
fuera un elemento de anclaje a
, también deberíamos admitir pasar an href
al componente.
<MyComponent as="a" href="...">A link </MyComponent>
Aunque href
se pasa en el ejemplo anterior, la implementación del componente no recibe otros apoyos. Sólo as
y children
se deconstruyen.
Sus pensamientos iniciales pueden ser seguir adelante y distribuir todos los demás apoyos pasados al componente de la siguiente manera:
const MyComponent = ({ as, children, ...rest }) => {
const Component = as || "span";
return <Component {...rest}>{children}</Component>;
};
Esto parece una solución decente, pero ahora resalta el segundo problema mencionado anteriormente. Los atributos incorrectos ahora también se transmitirán al componente.
Considera lo siguiente:
<MyComponent as="span" href="https://www.google.com">
Hello Wrong Attribute
</MyComponent>
Y tenga en cuenta el marcado renderizado eventual:
A span
con un href
es HTML no válido.
En resumen, los problemas actuales con nuestra implementación simple son insatisfactorios porque:
¿Cómo resolvemos estas preocupaciones? Para ser claros, no hay una varita mágica para agitar aquí. Sin embargo, aprovecharemos TypeScript para garantizar que construya componentes polimórficos fuertemente tipados.
Al finalizar, los desarrolladores que usen sus componentes evitarán los errores de tiempo de ejecución anteriores y, en su lugar, los detectarán durante el desarrollo o el tiempo de compilación, todo gracias a TypeScript.
Si está leyendo esto, un requisito previo es que ya sepa algo de TypeScript, al menos los conceptos básicos. Si no tiene idea de qué es TypeScript, le recomiendo que lea este documento primero.
En esta sección, utilizaremos TypeScript para resolver los problemas antes mencionados y construir componentes polimórficos fuertemente tipados.
Los primeros dos requisitos con los que comenzaremos incluyen:
as
propiedad no debe recibir cadenas de elementos HTML no válidas.En la siguiente sección, presentaremos los genéricos de TypeScript para hacer que nuestra solución sea más robusta, fácil de usar para desarrolladores y digna de producción.
as
propiedad solo reciba cadenas de elementos HTML válidasEsta es nuestra solución actual:
const MyComponent = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Para que las próximas secciones de esta guía sean prácticas, cambiaremos el nombre del componente de MyComponent
a Text
y supondremos que estamos construyendo un Text
componente polimórfico.
const Text = ({ as, children }) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Ahora, con su conocimiento de los genéricos, se vuelve obvio que es mejor representar as
con un tipo genérico, es decir, un tipo de variable basado en lo que el usuario pase.
Sigamos adelante y demos el primer paso de la siguiente manera:
export const Text = <C>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta cómo se define el genérico C
y luego se transmite en la definición de tipo para el accesorio as
.
Sin embargo, si escribió este código aparentemente perfecto, TypeScript gritará numerosos errores con más líneas rojas onduladas de lo que le gustaría.
Lo que está pasando aquí es una falla en la sintaxis de los genéricos en los .tsx
archivos. Hay dos formas de resolver esto.
Esta es la sintaxis para declarar varios genéricos. Una vez que haga esto, el compilador de TypeScript entiende claramente su intención y los errores desaparecen.
// note the comma after "C" below
export const Text = <C,>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
La segunda opción es restringir el genérico como mejor le parezca. Para empezar, puede usar el unknown
tipo de la siguiente manera:
// note the extends keyword below
export const Text = <C extends unknown>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Por ahora, me ceñiré a la segunda solución porque está más cerca de nuestra solución final. Sin embargo, en la mayoría de los casos, uso la sintaxis genérica múltiple y solo agrego una coma.
Sin embargo, con nuestra solución actual, obtenemos otro error de TypeScript:
El tipo de elemento JSX 'Componente' no tiene ninguna construcción ni firma de llamada. ts(2604)
Esto es similar al error que tuvimos cuando trabajamos con la echoLength
función. Al igual que acceder a la length
propiedad de un tipo de variable desconocido, se puede decir lo mismo aquí: tratar de representar cualquier tipo genérico como un componente React válido no tiene sentido.
Necesitamos restringir el genérico solo para que se ajuste al molde de un tipo de elemento React válido.
Para lograr esto, aprovecharemos el tipo React interno: React.ElementType
y nos aseguraremos de que el genérico esté restringido para adaptarse a ese tipo:
// look just after the extends keyword
export const Text = <C extends React.ElementType>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta que si está utilizando una versión anterior de React, es posible que deba importar una versión más nueva de React de la siguiente manera:
import React from 'react'
¡Con esto, no tenemos más errores!
Ahora, si continúa y usa este componente de la siguiente manera, funcionará bien:
<Text as="div">Hello Text world</Text>
Sin embargo, si pasa una propiedad no válida as
, ahora obtendrá un error de TypeScript apropiado. Considere el siguiente ejemplo:
<Text as="emmanuel">Hello Text world</Text>
Y el error arrojado:
El tipo '”emmanuel”' no se puede asignar al tipo 'ElementType | indefinido'.
¡Esto es excelente! Ahora tenemos una solución que no acepta galimatías para el as
accesorio y también evitará errores tipográficos desagradables, por ejemplo, divv
en lugar de div
.
¡Esta es una experiencia de desarrollador mucho mejor!
Al resolver este segundo caso de uso, llegará a apreciar cuán poderosos son realmente los genéricos. Primero, entendamos lo que estamos tratando de lograr aquí.
Una vez que recibimos un as
tipo genérico, queremos asegurarnos de que los accesorios restantes pasados a nuestro componente sean relevantes, según el as
accesorio.
Entonces, por ejemplo, si un usuario pasó un as
accesorio de img
, ¡ href
querríamos ser igualmente un accesorio válido!
Para darle una idea de cómo lograríamos esto, eche un vistazo al estado actual de nuestra solución:
export const Text = <C extends React.ElementType>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
La propiedad de este componente ahora está representada por el tipo de objeto:
{
as?: C;
children: React.ReactNode;
}
En pseudocódigo, lo que nos gustaría sería lo siguiente:
{
as?: C;
children: React.ReactNode;
} & {
...otherValidPropsBasedOnTheValueOfAs
}
Este requisito es suficiente para dejar a uno aferrado a un clavo ardiendo. No podemos escribir una función que determine los tipos apropiados en función del valor de as
, y no es inteligente enumerar un tipo de unión manualmente.
Bueno, ¿qué pasaría si hubiera un tipo proporcionado React
que actuara como una "función" que devolverá tipos de elementos válidos en función de lo que le pases?
Antes de presentar la solución, refactoricemos un poco. Saquemos los accesorios del componente en un tipo separado:
// See TextProps pulled out below
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
}
export const Text = <C extends React.ElementType>({
as,
children,
}: TextProps<C>) => { // see TextProps used
const Component = as || "span";
return <Component>{children}</Component>;
};
Lo importante aquí es observar cómo se transmite el genérico a TextProps<C>
. Similar a una llamada de función en JavaScript, pero con llaves angulares.
La varita mágica aquí es aprovechar el React.ComponentPropsWithoutRef
tipo como se muestra a continuación:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; // look here
export const Text = <C extends React.ElementType>({
as,
children,
}: TextProps<C>) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Tenga en cuenta que estamos introduciendo una intersección aquí. Esencialmente, estamos diciendo que el tipo de TextProps
es un tipo de objeto que contiene as
, children
y algunos otros tipos representados por React.ComponentPropsWithoutRef
.
Si lee el código, quizás se haga evidente lo que está pasando aquí.
Según el tipo de as
, representado por el genérico C
, React.componentPropsWithoutRef
devolverá accesorios de componente válidos que se correlacionan con el atributo de cadena pasado al as
accesorio.
Hay un punto más importante a tener en cuenta.
Si recién comenzó a escribir y confía en IntelliSense de su editor, se dará cuenta de que hay tres variantes del React.ComponentProps...
tipo:
React.ComponentProps
React.ComponentPropsWithRef
React.ComponentPropsWithoutRef
Si intentara usar el primero, ComponentProps
vería una nota relevante que dice:
Preferir
ComponentPropsWithRef
, siref
se reenvía oComponentPropsWithoutRef
cuando no se admiten referencias.
Esto es precisamente lo que hemos hecho. Por ahora, ignoraremos el caso de uso para admitir un ref
accesorio y nos ceñiremos a ComponentPropsWithoutRef
.
¡Ahora, probemos la solución!
Si sigue adelante y usa este componente incorrectamente, por ejemplo, pasando un as
accesorio válido con otros accesorios incompatibles, obtendrá un error.
<Text as="div" href="www.google.com">Hello Text world</Text>
Un valor de div
es perfectamente válido para la as
propiedad, pero a div
no debería tener un href
atributo.
Eso está mal, y TypeScript lo detectó correctamente con el error: Property 'href' does not exist on type ...
.
¡Esto es genial! Tenemos una solución aún mejor y más robusta.
Finalmente, asegúrese de pasar otros accesorios al elemento renderizado:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
export const Text = <C extends React.ElementType>({
as,
children,
...restProps, // look here
}: TextProps<C>) => {
const Component = as || "span";
// see restProps passed
return <Component {...restProps}>{children}</Component>;
};
Avancemos.
as
atributos predeterminadosConsidere nuevamente nuestra solución actual:
export const Text = <C extends React.ElementType>({
as,
children,
...restProps
}: TextProps<C>) => {
const Component = as || "span"; // look here
return <Component {...restProps}>{children}</Component>;
};
En particular, preste atención a dónde se proporciona un elemento predeterminado si as
se omite la propiedad.
const Component = as || "span"
Esto se representa correctamente en el mundo de JavaScript mediante la implementación: si as
es opcional, por defecto será un span
.
La pregunta es, ¿cómo maneja TypeScript este caso cuando as
no se pasa? ¿Estamos igualmente pasando un tipo predeterminado?
Bueno, la respuesta es no, pero a continuación hay un ejemplo práctico. Digamos que siguió adelante para usar el Text
componente de la siguiente manera:
<Text>Hello Text world</Text>
Tenga en cuenta que no hemos pasado ningún as
accesorio aquí. ¿TypeScript estará al tanto de los accesorios válidos para este componente?
Sigamos adelante y agreguemos un href
:
<Text href="https://www.google.com">Hello Text world</Text>
Si continúa y hace esto, no obtendrá errores. Eso es malo.
A span
no debería recibir un href
accesorio/atributo. Si bien tenemos un valor predeterminado span
en la implementación, TypeScript desconoce este valor predeterminado. Arreglemos esto con una asignación predeterminada simple y genérica:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
/**
* See default below. TS will treat the rendered element as a
span and provide typings accordingly
*/
export const Text = <C extends React.ElementType = "span">({
as,
children,
...restProps
}: TextProps<C>) => {
const Component = as || "span";
return <Component {...restProps}>{children}</Component>;
};
El bit importante se destaca a continuación:
<C extends React.ElementType = "span">
Et voila! El ejemplo anterior que teníamos ahora debería arrojar un error cuando pasa href
al Text
componente sin un as
accesorio.
El error debería decir: Property 'href' does not exist on type ...
.
Nuestra solución actual es mucho mejor que con la que comenzamos. Date una palmadita en la espalda por haber llegado tan lejos; a partir de aquí solo se vuelve más interesante.
El caso de uso para atender en esta sección es muy aplicable en el mundo real. Existe una gran posibilidad de que si está creando algún tipo de componente, ese componente también incluirá algunos accesorios específicos que son exclusivos del componente.
Nuestra solución actual tiene en cuenta el as
, children
y los otros accesorios de componentes basados en el as
accesorio. Sin embargo, ¿qué pasaría si quisiéramos que este componente manejara sus propios accesorios?
Hagamos esto práctico. Haremos que el Text
componente reciba un color
accesorio. El color
aquí será cualquiera de los colores del arco iris o black
.
Continuaremos y representaremos esto de la siguiente manera:
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
A continuación, debemos definir el color
accesorio en el TextProps
objeto de la siguiente manera:
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black"; // look here
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
Antes de continuar, hagamos un poco de refactorización. Representemos los accesorios reales del Text
componente por un Props
objeto, y escribamos específicamente solo los accesorios específicos de nuestro componente en el TextProps
objeto.
Esto se volverá obvio, como verás a continuación:
// new "Props" type
type Props <C extends React.ElementType> = TextProps<C>
export const Text = <C extends React.ElementType = "span">({
as,
children,
...restProps,
}: Props<C>) => {
const Component = as || "span";
return <Component {...restProps}>{children}</Component>;
};
Ahora vamos a limpiar TextProps
:
// before
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black"; // look here
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
// after
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black";
};
Ahora, TextProps
solo debe contener los accesorios que son específicos de nuestro Text
componente: as
y color
.
Ahora debemos actualizar la definición de Props
para incluir los tipos que hemos eliminado de TextProps
, es decir, children
y React.ComponentPropsWithoutRef<C>
.
Para la children
utilería, aprovecharemos la React.PropsWithChildren
utilería.
PropsWithChildren
es bastante fácil de razonar. Le pasa los accesorios de su componente y le inyectará la definición de accesorios para niños:
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>>
Tenga en cuenta cómo usamos las llaves angulares; esta es la sintaxis para pasar genéricos. Esencialmente, React.PropsWithChildren
acepta los accesorios de su componente como un genérico y lo aumenta con el children
accesorio. ¡Dulce!
Para React.ComponentPropsWithoutRef<C>
, seguiremos adelante y aprovecharemos un tipo de intersección aquí:
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
Y aquí está la solución actual completa:
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black";
};
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
export const Text = <C extends React.ElementType = "span">({
as,
children,
}: Props<C>) => {
const Component = as || "span";
return <Component> {children} </Component>;
};
Sé que esto puede parecer mucho, pero cuando miras más de cerca, todo tendrá sentido. ¡Realmente es solo juntar todo lo que has aprendido hasta ahora!
Habiendo hecho este refactor necesario, ahora podemos continuar con nuestra solución. Lo que tenemos ahora realmente funciona. Hemos escrito explícitamente el color
accesorio, y puede usarlo de la siguiente manera:
<Text color="violet">Hello world</Text>
Solo hay una cosa con la que no me siento particularmente cómodo: color
resulta ser también un atributo válido para numerosas etiquetas HTML, como era el caso antes de HTML5. Entonces, si lo eliminamos color
de nuestra definición de tipo, se aceptará como cualquier cadena válida.
Vea abajo:
type TextProps<C extends React.ElementType> = {
as?: C;
// remove color from the definition here
};
Ahora, si continúa usando Text
como antes, es igualmente válido:
<Text color="violet">Hello world</Text>
La única diferencia aquí es cómo se escribe. color
ahora está representado por la siguiente definición:
color?: string | undefined
Nuevamente, ¡esta NO es una definición que escribimos en nuestros tipos!
Este es un tipo de HTML predeterminado, donde color
es un atributo válido para la mayoría de los elementos HTML. Consulte esta pregunta de desbordamiento de pila para obtener más contexto.
Ahora, hay dos maneras de ir aquí. El primero es mantener nuestra solución inicial, donde declaramos explícitamente la color
propiedad:
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black"; // look here
};
Podría decirse que la segunda opción proporciona algo más de seguridad tipográfica. Para lograr esto, debes darte cuenta de dónde color
vino la definición predeterminada anterior: el React.ComponentPropsWithoutRef<C>
. Esto es lo que agrega otros accesorios según el tipo as
.
Entonces, con esta información, podemos eliminar explícitamente cualquier definición que exista en nuestros tipos de componentes de React.ComponentPropsWithoutRef<C>
.
Esto puede ser difícil de entender antes de verlo en acción, así que vayamos paso a paso.
React.ComponentPropsWithoutRef<C>
, como se indicó anteriormente, contiene todas las demás propiedades válidas basadas en el tipo de as
, por ejemplo, href
, color
, etc., donde estos tipos tienen todas sus propias definiciones, por ejemplo, color?: string | undefined
, etc.:
Es posible que algunos valores que existen en React.ComponentPropsWithoutRef<C>
también existan en nuestra definición de tipo de accesorios de componente. En nuestro caso, color
existe en ambos!
En lugar de confiar en nuestra color
definición para anular lo que proviene de React.ComponentPropsWithoutRef<C>
, eliminaremos explícitamente cualquier tipo que también exista en nuestra definición de tipos de componentes.
Por lo tanto, si existe algún tipo en nuestra definición de tipos de componentes, eliminaremos explícitamente esos tipos de React.ComponentPropsWithoutRef<C>
.
React.ComponentPropsWithoutRef<C>
Esto es lo que teníamos antes:
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
En lugar de tener un tipo de intersección donde agregamos todo lo que viene React.ComponentPropsWithoutRef<C>
, seremos más selectivos. Usaremos los tipos de utilidad Omit
y keyof
TypeScript para realizar algo de magia de TypeScript.
Echar un vistazo:
// before
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
React.ComponentPropsWithoutRef<C>
// after
type Props <C extends React.ElementType> =
React.PropsWithChildren<TextProps<C>> &
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
Esta es la parte importante:
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
Omit
toma en dos genéricos. El primero es un tipo de objeto y el segundo es una unión de tipos que le gustaría "omitir" del tipo de objeto.
Aquí está mi ejemplo favorito. Considere un Vowel
tipo de objeto de la siguiente manera:
type Vowels = {
a: 'a',
e: 'e',
i: 'i',
o: 'o',
u: 'u'
}
Este es un tipo de objeto de clave y valor. Digamos que quería derivar un nuevo tipo de Vowels
called VowelsInOhans
.
Bueno, sé que el nombre Ohans
contiene dos vocales o
y a
. En lugar de declarar manualmente estos:
type VowelsInOhans = {
a: 'a',
o: 'o'
}
Puedo seguir adelante para aprovechar Omit
de la siguiente manera:
type VowelsInOhans = Omit<Vowels, 'e' | 'i' | 'u'>
Omit
"omitirá" las teclas e
, i
y u
del tipo de objeto Vowels
.
Por otro lado, el operador de TypeScriptkeyof
funciona como te imaginas. Piense Object.keys
en JavaScript: dado un object
tipo, keyof
devolverá un tipo de unión de las claves del objeto.
¡Uf! Eso es un bocado. Aquí hay un ejemplo:
type Vowels = {
a: 'a',
e: 'e',
i: 'i',
o: 'o',
u: 'u'
}
type Vowel = keyof Vowels
Ahora, Vowel
será un tipo de unión de las claves de Vowels
, es decir:
type Vowel = 'a' | 'e' | 'i' | 'o' | 'u'
Si los junta y echa un segundo vistazo a nuestra solución, todo encajará muy bien:
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
keyof TextProps<C>
devuelve un tipo de unión de las claves de nuestro componente props. Esto a su vez se pasa a Omit
para omitirlos de React.ComponentPropsWithoutRef<C>
.
¡Dulce!
Para terminar, avancemos y pasemos el color
apoyo al elemento renderizado:
export const Text = <C extends React.ElementType = "span">({
as,
color, // look here
children,
...restProps
}: Props<C>) => {
const Component = as || "span";
// compose an inline style object
const style = color ? { style: { color } } : {};
// pass the inline style to the rendered element
return (
<Component {...restProps} {...style}>
{children}
</Component>
);
};
Finalmente tenemos una solución que funciona bien. Ahora, sin embargo, demos un paso más allá.
La solución que tenemos funciona muy bien para nuestro Text
componente. Sin embargo, ¿qué sucede si prefiere tener una solución que pueda reutilizar en cualquier componente de su elección, de modo que pueda tener una solución reutilizable para cada caso de uso?
Empecemos. Primero, aquí está la solución completa actual sin anotaciones:
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
type TextProps<C extends React.ElementType> = {
as?: C;
color?: Rainbow | "black";
};
type Props<C extends React.ElementType> = React.PropsWithChildren<
TextProps<C>
> &
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;
export const Text = <C extends React.ElementType = "span">({
as,
color,
children,
...restProps
}: Props<C>) => {
const Component = as || "span";
const style = color ? { style: { color } } : {};
return (
<Component {...restProps} {...style}>
{children}
</Component>
);
};
Sucinto y práctico.
Si hicimos esto reutilizable, entonces tiene que funcionar para cualquier componente. Esto significa eliminar el codificado TextProps
y representarlo con un genérico, para que cualquiera pueda pasar los accesorios de componentes que necesite.
Actualmente, representamos nuestros accesorios de componentes con la definición Props<C>
. Donde C
representa el tipo de elemento pasado para la as
propiedad.
Ahora cambiaremos eso a:
// before
Props<C>
// after
PolymorphicProps<C, TextProps>
PolymorphicProps
representa el tipo de utilidad que escribiremos en breve. Sin embargo, tenga en cuenta que esto acepta dos tipos genéricos, siendo el segundo los accesorios de componente en cuestión: TextProps
.
Continúe y defina el PolymorphicProps
tipo:
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = {} // empty object for now
La definición anterior debe ser comprensible. C
representa el tipo de elemento pasado en as
, y Props
es el componente real props, TextProps
.
Primero, dividamos lo TextProps
que teníamos antes en lo siguiente:
type AsProp<C extends React.ElementType> = {
as?: C;
};
type TextProps = { color?: Rainbow | "black" };
Entonces, hemos separado el AsProp
del TextProps
. Para ser justos, representan dos cosas diferentes. Esta es una mejor representación.
Ahora, cambiemos la PolymorphicComponentProp
definición de la utilidad para incluir la as
propiedad, las propiedades del componente y children
la propiedad, como lo hemos hecho en el pasado:
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>
Estoy seguro de que ahora entiendes lo que está pasando aquí: tenemos un tipo de intersección de Props
(que representa los accesorios del componente) y AsProp
representa el as
accesorio. Todos estos se pasan PropsWithChildren
para agregar la children
definición de prop. ¡Excelente!
Ahora, necesitamos incluir el bit donde agregamos la React.ComponentPropsWithoutRef<C>
definición. Sin embargo, debemos recordar omitir accesorios que existen en nuestra definición de componente.
Vamos a encontrar una solución robusta.
Escriba un nuevo tipo que solo comprenda los accesorios que nos gustaría omitir. Es decir, las teclas del AsProp
y los accesorios del componente también.
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
¿Recuerdas el keyof
tipo de utilidad?
PropsToOmit
ahora comprenderá un tipo de unión de los accesorios que queremos omitir, que es cada accesorio de nuestro componente representado por P
y el accesorio polimórfico real as
, representado por AsProps
.
Ponga todo esto junto muy bien en la PolymorphicComponentProp
definición:
type AsProp<C extends React.ElementType> = {
as?: C;
};
// before
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>
// after
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>,
PropsToOmit<C, Props>>;
Lo importante aquí es que hemos agregado la siguiente definición:
Omit<React.ComponentPropsWithoutRef<C>,
PropsToOmit<C, Props>>;
Esto básicamente omite los tipos correctos de React.componentPropsWithoutRef
. ¿Todavía recuerdas cómo omit
funciona ?
Por simple que parezca, ¡ahora tiene una solución que puede reutilizar en múltiples componentes en diferentes proyectos!
Aquí está la implementación completa:
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
Ahora podemos continuar y usar PolymorphicComponentProp
en nuestro Text
componente de la siguiente manera:
export const Text = <C extends React.ElementType = "span">({
as,
color,
children,
// look here
}: PolymorphicComponentProp<C, TextProps>) => {
const Component = as || "span";
const style = color ? { style: { color } } : {};
return <Component {...style}>{children}</Component>;
};
¡Que agradable! Si crea otro componente, puede continuar y escribirlo así:
PolymorphicComponentProp<C, MyNewComponentProps>
¿Escuchas ese sonido? Ese es el sonido de la victoria: ¡has llegado tan lejos!
¿Recuerdas todas las referencias React.ComponentPropsWithoutRef
hasta ahora?Puntales de componentes... sin referencias. Bueno, ¡ahora es el momento de poner los árbitros!
Esta es la parte final y más compleja de nuestra solución. Necesitaré que seas paciente aquí, pero también haré todo lo posible para explicar cada paso en detalle.
Lo primero es lo primero, ¿recuerdas cómo refs
funciona React ? El concepto más importante aquí es que simplemente no pasa ref
como accesorio y espera que se transmita a su componente como cualquier otro accesorio. La forma recomendada de manejar refs
sus componentes funcionales es usar la forwardRef
función .
Comencemos con una nota práctica.
Si continúa y pasa un ref
a nuestro Text
componente ahora, obtendrá un error que dice Property 'ref' does not exist on type ...
.
// Create the ref object
const divRef = useRef<HTMLDivElement | null>(null);
...
// Pass the ref to the rendered Text component
<Text as="div" ref={divRef}>
Hello Text world
</Text>
Esto se espera.
Nuestra primera oportunidad de admitir referencias será usarlas forwardRef
en el Text
componente como se muestra a continuación:
// before
export const Text = <C extends React.ElementType = "span">({
as,
color,
children,
}: PolymorphicComponentProp<C, TextProps>) => {
...
};
// after
import React from "react";
export const Text = React.forwardRef(
<C extends React.ElementType = "span">({
as,
color,
children,
}: PolymorphicComponentProp<C, TextProps>) => {
...
}
);
Esto es esencialmente envolver el código anterior en React.forwardRef
, eso es todo.
Ahora, React.forwardRef
tiene la siguiente firma:
React.forwardRef((props, ref) ... )
Esencialmente, el segundo argumento recibido es el ref
objeto. Sigamos adelante y manejemos eso:
type PolymorphicRef<C extends React.ElementType> = unknown;
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: PolymorphicComponentProp<C, TextProps>,
// look here
ref?: PolymorphicRef<C>
) => {
...
}
);
Lo que hemos hecho aquí es agregar el segundo argumento, ref
y declarar su tipo como PolymorphicRef
, que solo apunta a unknown
por ahora.
Tenga en cuenta que PolymorphicRef
toma en el genérico C
. Esto es similar a las soluciones anteriores: el ref
objeto de a div
difiere del de a span
, por lo que debemos tener en cuenta el tipo de elemento que se pasa a la as
propiedad.
Apunte su atención al PolymorphicRef
tipo. ¿Cómo podemos obtener el ref
tipo de objeto en función de la as
propiedad?
Déjame darte una pista: React.ComponentPropsWithRef
!
Tenga en cuenta que esto dice con ref. No sin ref.
Esencialmente, si se tratara de un paquete de claves (que, de hecho, lo es), incluirá todos los accesorios de componentes relevantes según el tipo de elemento, además del objeto ref.
Entonces, si sabemos que este tipo de objeto contiene la ref
clave, también podemos obtener ese tipo de referencia haciendo lo siguiente:
// before
type PolymorphicRef<C extends React.ElementType> = unknown;
// after
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>["ref"];
Esencialmente, React.ComponentPropsWithRef<C>
devuelve un tipo de objeto, por ejemplo,
{
ref: SomeRefDefinition,
// ... other keys,
color: string
href: string
// ... etc
}
Para seleccionar solo el ref
tipo, podemos hacer esto:
React.ComponentPropsWithRef<C>["ref"];
Tenga en cuenta que la sintaxis es similar a la sintaxis del acceso a la propiedad en JavaScript, es decir, ["ref"]
. Ahora que tenemos el ref
accesorio escrito, podemos continuar y pasarlo al elemento renderizado:
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: PolymorphicComponentProp<C, TextProps>,
ref?: PolymorphicRef<C>
) => {
//...
return (
<Component {...style} ref={ref}> // look here
{children}
</Component>
);
}
);
¡Hemos hecho un progreso decente! De hecho, si continúa y verifica el uso de Text
como lo hicimos antes, no habrá más errores:
// create the ref object
const divRef = useRef<HTMLDivElement | null>(null);
...
// pass ref to the rendered Text component
<Text as="div" ref={divRef}>
Hello Text world
</Text>
Sin embargo, nuestra solución todavía no está tan fuertemente tipada como me gustaría. Avancemos y cambiemos la referencia pasada a Text
como se muestra a continuación:
// create a "button" ref object
const buttonRef = useRef<HTMLButtonElement | null>(null);
...
// pass a button ref to a "div". NB: as = "div"
<Text as="div" ref={buttonRef}>
Hello Text world
</Text>
TypeScript debería arrojar un error aquí, pero no lo hace. Estamos creando una button
referencia, pero pasándola a un div
elemento. Eso no está bien.
Si observa el tipo exacto de ref
, se ve así:
React.RefAttributes<unknown>.ref?: React.Ref<unknown>
¿Ves el unknown
de ahí? Esa es una señal de tipeo débil. Idealmente, deberíamos tener HTMLDivElement
allí para definir explícitamente el objeto ref como un div
elemento ref.
Tenemos trabajo que hacer. Primero veamos los tipos de los otros accesorios del Text
componente, que aún hacen referencia al PolymorphicComponentProp
tipo. Cambie esto a un nuevo tipo llamado PolymorphicComponentPropWithRef
. Esto será solo una unión de PolymorphicComponentProp
y el ref prop. (Lo adivinaste.)
Aquí está:
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
> = PolymorphicComponentProp<C, Props> &
{ ref?: PolymorphicRef<C> };
Esta es solo una unión de la anterior PolymorphicComponentProp
y { ref?: PolymorphicRef<C> }
.
Ahora necesitamos cambiar los accesorios del componente para hacer referencia al nuevo PolymorphicComponentPropWithRef
tipo:
// before
type TextProps = { color?: Rainbow | "black" };
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: PolymorphicComponentProp<C, TextProps>,
ref?: PolymorphicRef<C>
) => {
...
}
);
// now
type TextProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<
C,
{ color?: Rainbow | "black" }
>;
export const Text = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: TextProps<C>, // look here
ref?: PolymorphicRef<C>
) => {
...
}
);
Hemos actualizado TextProps
a la referencia PolymorphicComponentPropWithRef
y eso ahora se pasa como accesorios para el Text
componente. ¡Hermoso!
Queda una última cosa por hacer: proporcione una anotación de tipo para el Text
componente. Se parece a:
export const Text : TextComponent = ...
TextComponent
es el tipo de anotación que escribiremos. Aquí está completamente escrito:
type TextComponent = <C extends React.ElementType = "span">(
props: TextProps<C>
) => React.ReactElement | null;
Este es esencialmente un componente funcional que toma TextProps
y devuelve React.ReactElement | null
, donde TextProps
es como se definió anteriormente:
type TextProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<
C,
{ color?: Rainbow | "black" }
>;
¡Con esto, ahora tenemos una solución completa!
Voy a compartir la solución completa ahora. Puede parecer abrumador al principio, pero recuerde que hemos trabajado línea por línea en todo lo que ve aquí. Léalo con esa confianza.
import React from "react";
type Rainbow =
| "red"
| "orange"
| "yellow"
| "green"
| "blue"
| "indigo"
| "violet";
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
// This is the first reusable type utility we built
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
// This is a new type utitlity with ref!
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
// This is the type for the "ref" only
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>["ref"];
/**
* This is the updated component props using PolymorphicComponentPropWithRef
*/
type TextProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<
C,
{ color?: Rainbow | "black" }
>;
/**
* This is the type used in the type annotation for the component
*/
type TextComponent = <C extends React.ElementType = "span">(
props: TextProps<C>
) => React.ReactElement | null;
export const Text: TextComponent = React.forwardRef(
<C extends React.ElementType = "span">(
{ as, color, children }: TextProps<C>,
ref?: PolymorphicRef<C>
) => {
const Component = as || "span";
const style = color ? { style: { color } } : {};
return (
<Component {...style} ref={ref}>
{children}
</Component>
);
}
);
¡Y ahí tienes!
Ha creado con éxito una solución robusta para manejar componentes polimórficos en React con TypeScript. Sé que no fue un viaje fácil, pero lo lograste.
Gracias por seguirnos.
Esta historia se publicó originalmente en https://blog.logrocket.com/build-strongly-typed-polymorphic-components-react-typescript/
1636236360
In this lesson we look at how to add #cypress with code coverage support for a Create #React App application with #TypeScript.
In the end you will have a developer flow that can save you a bunch of time in testing effort
1638074460
This quick lesson demonstrates how to ignore errors in a JSX / #React file with #TypeScript
1615544450
Since March 2020 reached 556 million monthly downloads have increased, It shows that React JS has been steadily growing. React.js also provides a desirable amount of pliancy and efficiency for developing innovative solutions with interactive user interfaces. It’s no surprise that an increasing number of businesses are adopting this technology. How do you select and recruit React.js developers who will propel your project forward? How much does a React developer make? We’ll bring you here all the details you need.
Facebook built and maintains React.js, an open-source JavaScript library for designing development tools. React.js is used to create single-page applications (SPAs) that can be used in conjunction with React Native to develop native cross-platform apps.
In the United States, the average React developer salary is $94,205 a year, or $30-$48 per hour, This is one of the highest among JavaScript developers. The starting salary for junior React.js developers is $60,510 per year, rising to $112,480 for senior roles.
In context of software developer wage rates, the United States continues to lead. In high-tech cities like San Francisco and New York, average React developer salaries will hit $98K and $114per year, overall.
However, the need for React.js and React Native developer is outpacing local labour markets. As a result, many businesses have difficulty locating and recruiting them locally.
It’s no surprise that for US and European companies looking for professional and budget engineers, offshore regions like India are becoming especially interesting. This area has a large number of app development companies, a good rate with quality, and a good pool of React.js front-end developers.
As per Linkedin, the country’s IT industry employs over a million React specialists. Furthermore, for the same or less money than hiring a React.js programmer locally, you may recruit someone with much expertise and a broader technical stack.
React is a very strong framework. React.js makes use of a powerful synchronization method known as Virtual DOM, which compares the current page architecture to the expected page architecture and updates the appropriate components as long as the user input.
React is scalable. it utilises a single language, For server-client side, and mobile platform.
React is steady.React.js is completely adaptable, which means it seldom, if ever, updates the user interface. This enables legacy projects to be updated to the most new edition of React.js without having to change the codebase or make a few small changes.
React is adaptable. It can be conveniently paired with various state administrators (e.g., Redux, Flux, Alt or Reflux) and can be used to implement a number of architectural patterns.
Is there a market for React.js programmers?
The need for React.js developers is rising at an unparalleled rate. React.js is currently used by over one million websites around the world. React is used by Fortune 400+ businesses and popular companies such as Facebook, Twitter, Glassdoor and Cloudflare.
As you’ve seen, locating and Hire React js Developer and Hire React Native developer is a difficult challenge. You will have less challenges selecting the correct fit for your projects if you identify growing offshore locations (e.g. India) and take into consideration the details above.
If you want to make this process easier, You can visit our website for more, or else to write a email, we’ll help you to finding top rated React.js and React Native developers easier and with strives to create this operation
#hire-react-js-developer #hire-react-native-developer #react #react-native #react-js #hire-react-js-programmer