React + Typescript: tipando funcion

Pasar propiedades del tipo cadena de texto o un objeto está chupado con React, pero... ¿Y si queremos pasar una propiedad de tipo función? Esto es muy normal si por ejemplo queremos que un componente padre pueda capturar el evento onChange o el onClick de un componente hijo.

Manos a la obra

En vez de utilizar constantes para el nombre y apellidos, vamos a guardarlo como parte del estado:

export default function App() {
+ const [nombre, setNombre] = React.useState('Jose');
+ const [apellidos, setApellidos]  = React.useState('García Pérez')

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <MuestraNombre
      <MuestraNombre
-       nombre="Jose"
+       nombre={nombre}
-       apellidos="García Pérez"
+       apellidos={apellidos}
      />
    </div>
  );
}

Vamos a empezar por añadir un botón a nuestro componente MuestraNombre que al clickarlo permita al padre realizar una operación, en este caso lo que haremos será borrar el campo apellido.

Lo primero que se nos puede ocurrir para implementar ésto es usar el tipo any, esto es un cajón de sastre y debemos de evitarlo en la medida de lo posible:

interface Props {
  nombre: string;
  apellidos: string;
+ onResetApellidos : any;
}

const MuestraNombre: React.FC<Props> = (props) => {
  return (
+   <>
      <h1>
        {props.nombre} {props.apellidos}
      </h1>
+     <button onClick={props.onResetApellidos}>Borrar apellidos</button>
+   </>
  );
};

En el padre podemos consumirlo:

export default function App() {
  const [nombre, setNombre] = React.useState('Jose');
  const [apellidos, setApellidos] = React.useState('García Pérez')

+ const handleResetApellidos = () => {
+   setApellidos('');
+ }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <MuestraNombre
        nombre={nombre}
        apellidos={apellidos}
+       onResetApellidos={handleResetApellidos}
      />
    </div>
  );
}

Esto de usar any es un hack sucio, de hecho una medida para ver la calidad de código TypeScript, es ver cuantos any se están usando en el proyecto, ¿Qué limitaciones tengo aquí?

  • Le puedo pasar cualquier valor, con lo que podría equivocarme y tener un error de ejecución.
  • No conozco que firma tiene el callback que estoy definiendo, tendría que ponerme a mirar el código del componente para averiguarlo.

Si nos ponemos encima del onClick del button que hemos definido en MuestraNombre, podemos ver la firma que espera. En nuestro caso no vamos a hacer uso del parámetro que nos informa ese evento, sólo queremos saber cuándo se dispara el click del botón, lo que hacemos, es definir el tipo para onResetApellidos de la siguiente manera:

interface Props {
  nombre: string;
  apellidos: string;
-  onResetApellidos: any;
+  onResetApellidos: () => void;
}

Aquí le estamos diciendo que onResetApellidos es de tipo función, que no recibirá parámetros y no se espera que devuelva nada (es de tipo void), si nos vamos ahora al componente App e intentamos cambiar el valor de la propiedad onResetApellidos por algo que no sea una función veremos que el editor nos resalta un error.

Para terminar, vamos a ver como manejarnos con el evento de un campo de tipo input. En este caso, sí que tenemos un valor que queremos pasar para arriba, lo que se esté tecleando en ese momento.

Vamos a reemplazar el campo texto que muestra los apellidos por un input

const MuestraNombre: React.FC<Props> = (props) => {
  return (
    <>
      <h1>
-      {props.nombre} {props.apellidos}
+      {props.nombre}
      </h1>
+     <input value={props.apellidos}/>
      <button onClick={props.onResetApellidos}>Borrar Apellidos</button>
    </>
  );
};

Si queremos modificar el campo apellidos tenemos que enviar la nueva propuesta de valor al componente padre y que éste haga el setState de turno.

Una opción para implementar ésto, sería ponernos encima del evento onChange del input y copiar la firma del mismo para añadirlo en las propiedades:

interface Props {
  nombre: string;
  apellidos: string;
  onResetApellidos: () => void;
+  onSetApellidos: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const MuestraNombre: React.FC<Props> = (props) => {
  return (
    <>
      <h1>{props.nombre}</h1>
      <input
        value={props.apellidos}
+       onChange={props.onSetApellidos}
      />
      <button onClick={props.onResetApellidos}>Borrar Apellidos</button>
    </>
  );
};

Y vamos a consumirlo en App:

export default function App() {
  const [nombre, setNombre] = React.useState("Jose");
  const [apellidos, setApellidos] = React.useState("García Pérez");

  const handleResetApellidos = () => {
    setApellidos("");
  };

+ const handleSetApellidos = (event: React.ChangeEvent<HTMLInputElement>) => {
+   setApellidos(event.target.value)
+ }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <MuestraNombre
        nombre={nombre}
        apellidos={apellidos}
        onResetApellidos={handleResetApellidos}
+       onSetApellidos={handleSetApellidos}
      />
    </div>
  );
}

Esta aproximación en muchos casos no es óptima, ya que le estamos exponiendo al componente padre el detalle de implementación de nuestro componente, le forzamos saber qué es eso de event.target.value, ¿Y si en un futuro decidiéramos no usar un input? Lo ideal es abstraer el contrato, y usar algo común para los dos componentes, en este caso el tipo string, veamos cómo:

interface Props {
  nombre: string;
  apellidos: string;
  onResetApellidos: () => void;
-  onSetApellidos: (event: React.ChangeEvent<HTMLInputElement>) => void;
+  onSetApellidos: (value : string) => void;
}

const MuestraNombre: React.FC<Props> = (props) => {
  return (
    <>
      <h1>{props.nombre}</h1>
-     <input value={props.apellidos} onChange={props.onSetApellidos} />
+     <input value={props.apellidos} onChange={e => props.onSetApellidos(e.target.value)} />
      <button onClick={props.onResetApellidos}>Borrar Apellidos</button>
    </>
  );
};

Y en el componente padre:

export default function App() {
  const [nombre, setNombre] = React.useState("Jose");
  const [apellidos, setApellidos] = React.useState("García Pérez");

  const handleResetApellidos = () => {
    setApellidos("");
  };

-  const handleSetApellidos = (event: React.ChangeEvent<HTMLInputElement>) => {
-    setApellidos(event.target.value);
-  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <MuestraNombre
        nombre={nombre}
        apellidos={apellidos}
        onResetApellidos={handleResetApellidos}
-       onSetApellidos={handleSetApellidos}
+       onSetApellidos={setApellidos}
      />
    </div>
  );
}

¿Te apuntas a nuestro máster?

Si te ha gustado este ejemplo y tienes ganas de aprender Front End guiado por un grupo de profesionales ¿Por qué no te apuntas a nuestro Máster Front End Online Lemoncode? Tenemos tanto edición de convocatoria con clases en vivo, como edición continua con mentorización, para que puedas ir a tu ritmo y aprender mucho.