Aprende Javascript con MentoringJS - Step 10

Esta es la tercera parte de mi revisión del tutorial de react-express.

La primera parte se puede ver en mi blog

La segunda parte se puede ver en mi blog

Índice de ejercicios

Ejercicios del 1 al 6

Ejercicios del 7 al 14


15. Component State (ToDo-List)

Almacenar información en el state de los componentes está bien para aplicaciones pequeñas y también para porciones de aplicaciones independientes del resto de la aplicación. La mejora manera de crear componentes es agruparlos en dos categorías: containers y componentes.

Los containers son conscientes de la información y de la lógica de la aplicación. También pueden pasar información y callbacks como props a los componentes de presentación y también controlar la actualización de información cuando el usuario interactúa con la aplicación.

Componentes simples/presentacionales. La mayoría de componentes no contienen nada de lógica. Estos componentes de presentación se puede utilizar fácilmente en otra aplicación diferente ya que son completamente genéricos y sus únicas entradas son sus propios props. Los componentes de presentación normalmente se llaman componentes.

Para aclarar estos conceptos se va a mostrar una aplicación (una ToDo-List) con varios de los componentes mencionados.

Esta aplicación tiene 1 container y 3 componentes. Normalmente, estos containers y componentes suelen ir cada uno en su propio fichero y se debe exportar cada uno posteriormente.

Ficheros:

  • index.js El índex es la entrada del proyecto. Es la entrada para el bundle de Javascript y será el que renderizará el componente root en el DOM.

  • App.js App es el componente container “inteligente”, el que contendrá la información y lógica de la ToDo-List para añadir y eliminar ítems. App renderizará los otros componentes, List, Input y Title, pasando la información y callbacks de la ToDo-List para modificar la lista.

  • List.js Este componente renderiza una lista de strings. Se dispara con el callback onClickItem cuando un ítem es apretado.

  • Input.js Este componente renderiza un campo de entrada. Mantiene el input actual en su estado y entonces lanza un callback, onSubmitEditing cuando el usuario aprieta Enter.

  • Title.js Un componente de título sencillo. Puro componente de presentación


index.js

import { render } from "react-dom";
import React from "react";

import App from "./App";

render(<App />, document.querySelector("#app"));

Aquí se hacen varios imports de las librerías de React y también el componente App del fichero App.js del mismo directorio de trabajo.

Aquí vemos una novedad, el import del componente App se hace sin {}. Esto es debido a que el componente (clase) App del fichero App.js se ha exportado de ese fichero como default. En la explicación del archivo App.js se verá más en profundidad.

Al final renderiza este componente App al DOM.

Volver a los ficheros

App.js

import React, { Component } from 'react'

import List from './List'
import Input from './Input'
import Title from './Title'

export default class App extends Component {

  state = {
    todos: ['Click to remove', 'Learn React', 'Write Code', 'Ship App'],
  }

  onAddTodo = (text) => {
    const {todos} = this.state

    this.setState({
      todos: [text, ...todos],
    })
  }

  onRemoveTodo = (index) => {
    const {todos} = this.state

    this.setState({
      todos: todos.filter((todo, i) => i !== index),
    })
  }

  render() {
    const {todos} = this.state

    return (
      <div style={styles.container}>
        <Title>
          To-Do List
        </Title>
        <Input
          placeholder={'Type a todo, then hit enter!'}
          onSubmitEditing={this.onAddTodo}
        />
        <List
          list={todos}
          onClickItem={this.onRemoveTodo}
        />
      </div>
    )
  }
}

const styles = {
  container: {
    display: 'flex',
    flexDirection: 'column',
  }
}

La primera y más llamativa diferencia con el resto de ejemplos que se han ido viendo es la siguiente:

import React, { Component } from 'react'

import List from './List'
import Input from './Input'
import Title from './Title'

export default class App extends Component {

Aquí no solo salen algunos imports diferentes (como en el index.js) sino también el componente (clase) App es algo diferente.

En primer lugar, el módulo App.js, como todos los demás excepto index.js, no tienen al principio del archivo el import de react-dom. Esto es debido a que no les hace falta importar de React el componente react-dom ya que no van a renderizar nada al DOM. El único fichero que renderiza es el index.js que hace una importación del fichero App.js que será el único que haga una renderización al DOM. Este fichero App.js sí que tiene el import de react-dom tal y como hemos visto.

En segundo lugar, nos encontramos con export default.

Cuando se trabajo con módulos, es decir, ficheros separados donde en cada uno hay un componente, las clases creadas en cada módulo son privadas. Para hacerlas accesibles desde otros módulos (ficheros) se debe hacer pública. Esto es lo que se hace con export. Además, como se ve por ejemplo en List.js, el componente List se hace de la siguiente manera:

export default class List extends Component {}

Aquí vemos que se exporta el componente como export default. Este default se añade para indicar a React que este componente será el por defecto a exportar. Cuando se hace así, desde el módulo o fichero que se llame a este componente se puede llamar sin necesidad de añadir {}.

Esto es lo que se conoce como Export name/default clases.

Respecto al código:

En primer lugar, se establece un state con un array:

state = {
  todos: ['Click to remove', 'Learn React', 'Write Code', 'Ship App'],
}

Posteriormente, se añaden dos funciones en formato arrow_functions:

onAddTodo = (text) => {
  const {todos} = this.state

  this.setState({
    todos: [text, ...todos],
  })
}

Esta función sirve para añadir un nuevo elemento al array todos. La función tiene un parámetro text que será lo que se añada al array. Esto se hace con this.setState y modificando el array todos añadiendo text. Esto vemos que se hace con el spread operator, característica de ES6.

La siguiente función, onRemoveTodo es la encargada de borrar un elemento de la lista del ToDo.

onRemoveTodo = (index) => {
  const {todos} = this.state

  this.setState({
    todos: todos.filter((todo, i) => i !== index),
  })
}

Aquí vemos que se le pasa un parámetro index a dicha función. Dentro de this.setState se añade una función nueva y se le asigna el resultado a todos. En esta función se usa el método filter que sirve para que revise (itere) por todos los elementos del array todos y le pase el predicado de la función a cada elemento del array. La función en arrow_functions tiene dos parámetros, todo e i, mientras que el contenido es que i sea diferente de index, que es el parámetro de la función principal, onRemoveTodo.

Básicamente, esta función recibe un elemento (index) a borrar (que se llamará más tarde con un evento) y lo que hace es revisar todo el array para devolver el mismo array pero sin ese elemento que se ha seleccionado.

Después llegamos a la parte del método render donde se renderiza el contenido final del componente App.

render() {
  const {todos} = this.state

  return (
    <div style={styles.container}>
      <Title>
        To-Do List
      </Title>
      <Input
        placeholder={'Type a todo, then hit enter!'}
        onSubmitEditing={this.onAddTodo}
      />
      <List
        list={todos}
        onClickItem={this.onRemoveTodo}
      />
    </div>
  )
}
}

En esta parte del código se devuelve un div con unos estilos determinados posteriormente y con diverso contenido JSX y llamando a los otros componentes exportados de la aplicación, como Title, Input y List.

En cada parte de este componente se añade contenido específico de ese módulo.

En Title se añade simplemente el título de la ToDo-List.

En Input se añade un placeholder y un evento onSubmitEditing llamando a la función onAddTodo que sirve para añadir un nuevo elemento.

En List se añade contenido dinámico con el contenido del array todos y también un evento onClickItem llamando a la función de eliminar elementos onRemoveTodo.

Al final de todo se añade el contenido CSS que se utiliza para darle estilos a la lista To-Do.

const styles = {
  container: {
    display: 'flex',
    flexDirection: 'column',
  }
}

Volver a los ficheros

List.js

import React, { Component } from "react";

export default class List extends Component {
  renderItem = (text, i) => {
    const { onClickItem } = this.props;

    return (
      <div style={styles.item} onClick={() => onClickItem(i)}>
        {text}
      </div>
    );
  };

  render() {
    const { list } = this.props;

    return (
      <div style={styles.container}>
        {list.map(this.renderItem)}
      </div>
    );
  }
}

const styles = {
  container: {
    display: "flex",
    flexDirection: "column"
  },
  item: {
    backgroundColor: "whitesmoke",
    marginBottom: 5,
    padding: 15
  }
};

En este módulo List.js se exporta por defecto el componente List. En este componente devuelve una lista de arrays tal y como se ve en el return del método render.

return (
      <div style={styles.container}>
        {list.map(this.renderItem)}
      </div>
    );

Aquí se ve que se le pasa el método map a la constante list que antes se ha definido. La función que se llama para crear este nuevo array es renderItem.

renderItem = (text, i) => {
  const { onClickItem } = this.props;

  return (
    <div style={styles.item} onClick={() => onClickItem(i)}>
      {text}
    </div>
  );
};

Esta función tiene dos parámetros, text e i. Se declara dentro una constante onClickItem con contenido dinámico que será aceptada como prop. Esta función devuelve código JSX con unos estilos CSS definidos posteriormente y un evento onClick llamando al ítem concreto que se ha apretado y que se representa con onClickItem.

Al final se declara un objeto de estilos CSS con varias propiedades:

const styles = {
  container: {
    display: "flex",
    flexDirection: "column"
  },
  item: {
    backgroundColor: "whitesmoke",
    marginBottom: 5,
    padding: 15
  }
};

Volver a los ficheros

Input.js

import React, { Component } from "react";

export default class Input extends Component {
  state = {
    value: ""
  };

  handleChange = e => {
    this.setState({ value: e.target.value });
  };

  handleKeyPress = e => {
    if (e.key !== "Enter") return;

    const { onSubmitEditing } = this.props;
    const { value } = this.state;

    if (!value) return; // Don't submit if empty

    onSubmitEditing(value);
    this.setState({ value: "" });
  };

  render() {
    const { placeholder } = this.props;
    const { value } = this.state;

    return (
      <input
        style={styles.input}
        type={"text"}
        value={value}
        placeholder={placeholder}
        onChange={this.handleChange}
        onKeyPress={this.handleKeyPress}
      />
    );
  }
}

const styles = {
  input: {
    fontSize: "100%",
    padding: 15,
    borderWidth: 0
  }
};

Este módulo es casi equivalente al que vimos en la parte de Input Handling del propio tutorial de react.express. También lo revisé en mi segundo artículo sobre React.

Lo único que cambia es que se añade una nueva función(handleKeyPress) para comprobar si el usuario ha apretado la tecla Enter.

handleKeyPress = e => {
  if (e.key !== "Enter") return;

  const { onSubmitEditing } = this.props;
  const { value } = this.state;

  if (!value) return; // Don't submit if empty

  onSubmitEditing(value);
  this.setState({ value: "" });
};

En esta función se añade un condicional en el que se indica que si no se aprieta Enter no se devuelva nada. Si además el valor de value está vacío tampoco se devuelve nada. Tan solo se devuelve en los casos contrarios. Cuando se cambia el valor de value y se aprieta Enter se llama a la función onSubmitEditing.

Posteriormente se devuelve en return contenido JSX con un campo input con ciertos estilos CSS y algunas propiedades de dicho campo. Las más llamativas son onChange y onKeyPress que llaman a las dos funciones declaradas previamente, handleChange y handleKeyPress.

Finalmente se añade un objeto styles con ciertos estilos CSS.

Volver a los ficheros

Title.js

import React, { Component } from "react";

export default class Title extends Component {
  render() {
    const { children } = this.props;

    return (
      <div style={styles.header}>
        <div style={styles.title}>{children}</div>
      </div>
    );
  }
}

const styles = {
  header: {
    backgroundColor: "skyblue",
    padding: 15
  },
  title: {
    textAlign: "center",
    color: "white"
  }
};

Este módulo tiene un componente que exporta por defecto, Title. En este componente se le pasa como prop children que será lo que se devuelva en el return. Ahí se devuelve código JSX con estilos CSS y con el contenido dinámico pasado como prop.

Este contenido dinámico que se pasa como children es el que se pasa cuando se llama al componente Title desde el módulo app

return (
      <div style={styles.container}>
        <Title>
          To-Do List
        </Title>

Finalmente se añade un objeto styles con ciertos estilos CSS.


Con este artículo finalizo la revisión del tutorial de React.express. En siguientes posts seguiré con React a tope. Stay tuned!