Ejemplo de Implementación de React Recoil

Ejemplo de Implementación de React Recoil Créditos de imagen: Unsplash

Recoil es una librería JavaScript para la gestión del estado en React.js. Es relativamente novedosa (a fecha de la redacción de este documento todavía está en fase alpha), y ha sido desarrollada por Facebook, creador también de React.

A diferencia de Redux, que es la librería hasta ahora más usada para la gestión del estado, Recoil es mucho más sencilla de implementar. Esta sencillez se debe a que Recoil utiliza la misma filosofía de componentes y hooks que usa React, y que mostraremos en este tutorial.

Conceptos

Al usar Recoil se crea un grafo de flujo de datos que va desde los atoms (estado compartido), a través de los selectors (funciones puras), y hacia los componentes de React.

  • Atoms: Son unidades de estado a los que los componentes se pueden subscribir. Cuando un atom es actualizado, cada componente suscrito es re-renderizado con el nuevo valor. También pueden ser creados en tiempo de ejecución, y usados en lugar del estado local de uno o varios componentes, permitiendo así que éstos compartan su estado.

  • Selectors: Transforman ese estado tanto de forma síncrona como asíncrona. Son funciones puras que aceptan como entrada tanto atoms como otros selectors. Cuando estos atoms o selectors son actualizados, la función selector será reevaludada. Los componentes pueden subscribirse a los selectors al igual que a los atoms, y serán re-renderizados cuando los selectors cambien.

Los selectors son utilizados para calcular datos derivados, que están basados en el estado. Esto permite evitar estados redundantes, pues unos pocos datos del estado son guardados en atoms, mientras que todo lo demás es calculado basado en ese estado básico.

La Aplicación de ejemplo

Con el objetivo de mostrar el funcionamiento y la implementación de esta bibliotecta he desarrollado una pequeña aplicación, cuyo código describiré en este artículo. A continuación dejo los enlaces para acceder a la App.

La App también contiene un ejemplo de uso de react-router, el paquete para la gestión del enrutado de páginas en React. La descripción de este paquete, que no interfiere con Recoil, la puede consultar en mi artículo Ejemplo de Implementación de React Router v6.


app ejemplo react recoil

Instalación de la Aplicación

Si desea instalar en su equipo la aplicación para probarla, modificarla o consultar su código, lo puede hacer mediante las siguientes instrucciones.

$ git clone https://github.com/frames75/react-router-recoil-demo
$ cd react-router-recoil-demo
$ npm install
$ npm start

Instalación del paquete en un proyecto

Para poder usar Recoil en otro proyecto es necesario instalarlo. Se puede hacer con la siguiente instrucción:

$ cd carpeta-del-proyecto/

$ npm install recoil

El código de la Aplicación

Para la realización de la App me he basado en el tutorial oficial de Recoil (en inglés). Si nunca has utilizado esta biblioteca recomiendo su lectura y realización. Si sólo quieres ojear la forma en que se implementa para poder adaptarla a un proyecto propio, seguir este artículo te puede ser de utilidad.

Declarar RecoilRoot

Todos los componentes que vayan a utilizar el estado Recoil deben estar englobados dentro del componente <RecoilRoot>. En nuestro ejemplo insertaremos este componente en la función raíz App().

import {
  RecoilRoot,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
    <div className="App">
      <header className="App-header">
      </header>
      <div className="App-body">
        <nav className="App-nav">
        </nav>
        <main className="App-content">
        </main>
      </div>
    </div>
    </RecoilRoot>
  );
}

export default App;

Atoms

La declaración del atom contendrá el estado de la aplicación. En nuestro ejemplo será simplemente un array de objetos, representando cada uno un ítem de la lista to-do. En el atom indicaremos dos campos: una clave única key y un valor por defecto default.

Para leer el contenido de este atom usaremos el hook useRecoilValue() dentro de nuestro componente TodoList.

import {
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
  useSetRecoilState,
} from 'recoil';

const todoListState = atom({
  key: 'todoListState',
  default: [],
});

function TodoList() {
  const todoList = useRecoilValue(todoListState);

  return (
    <>
      {/* <TodoListStats /> */}
      {/* <TodoListFilters /> */}
      <TodoItemCreator />

      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
}

Para crear nuevos ítems to-do usaremos el hook useSetRecoilState(), con el que obtendremos una función setter en nuestro componente TodoItemCreator. Esta función nos servirá para actualizar la lista de ítems, añadiendo uno en este caso.

Se recomienda usar este hook cuando un componente necesita escribir en el estado sin previamente leerlo. Si un componente usó el hook useRecoilState() para obtener el setter, también se subscribiría a actualizaciones y re-renderizaría cuando el atom o el selector se actualizase. Usando useSetRecoilState() se permite al componente definir el valor del estado sin que re-renderice cuando el valor cambie.

function TodoItemCreator() {
  const [inputValue, setInputValue] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false,
      },
    ]);
    setInputValue('');
  };

  const onChange = ({target: {value}}) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

// utility for creating unique Id
let id = 0;
function getId() {
  return id++;
}

El componente TodoItem mostrará el valor del ítem to-do. A demás, permitirá cambiar su texto y eliminarlo. Usaremos el hook useRecoilState() para leer el atom, así como obtener una función setter que usaremos para actualizar el texto del ítem, marcarlo como completado, y eliminarlo.

function TodoItem({item}) {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const index = todoList.findIndex((listItem) => listItem === item);

  const editItemText = ({target: {value}}) => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      text: value,
    });

    setTodoList(newList);
  };

  const toggleItemCompletion = () => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      isComplete: !item.isComplete,
    });

    setTodoList(newList);
  };

  const deleteItem = () => {
    const newList = removeItemAtIndex(todoList, index);

    setTodoList(newList);
  };

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  );
}

function replaceItemAtIndex(arr, index, newValue) {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}

function removeItemAtIndex(arr, index) {
  return [...arr.slice(0, index), ...arr.slice(index + 1)];
}

Selectors

En nuestra aplicación de ejemplo vamos a utilizar los siguientes selectors, que representarán los distintos datos derivados del estado:

  • Lista filtrada to-do: Lista de ítems clasificados según estén completados o no.
  • Estadísticas de la lista to-do: La cantidad total de ítem así como el porcentaje de los que estén completados.

Para almacenar el valor del filtro seleccionado crearemos otro atom.

const todoListFilterState = atom({
  key: 'TodoListFilter',
  default: 'Show All',
});

Así, usando los dos atoms que hemos creado (la lista de ítems y el valor del filtro), definiremos el selector que genera la lista filtrada. Si se actualiza cualquiera de los atoms, la lista filtrada que contiene el selector se regenerará.

const filteredTodoListState = selector({
  key: 'FilteredTodoList',
  get: ({get}) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

Adaptaremos el componente TodoList al nuevo selector modificando sólamente el parámetro que recibe el hook useRecoilValue().

function TodoList() {
  // changed from todoListState to filteredTodoListState
  const todoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />

      {todoList.map((todoItem) => (
        <TodoItem item={todoItem} key={todoItem.id} />
      ))}
    </>
  );
}

Para cambiar el valor del filtro, crearemos el siguiente componente.

function TodoListFilters() {
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  const updateFilter = ({target: {value}}) => {
    setFilter(value);
  };

  return (
    <>
      Filter:
      <select value={filter} onChange={updateFilter}>
        <option value="Show All">All</option>
        <option value="Show Completed">Completed</option>
        <option value="Show Uncompleted">Uncompleted</option>
      </select>
    </>
  );
}

Ahora crearemos el selector que contendrá las siguientes variables estadísticas:

  • Número total de ítems.
  • Número de ítems completados.
  • Número de ítems no completados.
  • Porcentaje de ítems completados.

Y también el componente que las mostrará.

const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const list = get(todoListState);
    const values = new Map();

    values.set('total', list.length);
    values.set('total_Completed', list.filter(item => item.isComplete).length);
    values.set('total_Uncompleted', values.get('total') - values.get('total_Completed'));
    values.set('percent_Completed', (values.get('total')>0) ? values.get('total_Completed')*100/values.get('total') : 0);

    return values;
  },
});

function TodoListStats() {
  const todoStats = useRecoilValue(todoListStatsState);
  const arrKeys = todoStats ? [...todoStats.keys()] : null;

  return (
    <ul>
    { arrKeys && arrKeys.map((item) => {
        let formatedStat = todoStats.get(item);
        if (item==='percent_Completed') 
          formatedStat = new Intl.NumberFormat("de-DE", {maximumFractionDigits: 2}).format(formatedStat) + '%';

        return (
          <li key={item}>
            {item}: {formatedStat}
          </li>
      );})
    }
    </ul>
  );
}

De esta forma hemos creado cuatro componentes que están relacionados a través de su estado, compartiéndolo. Cuando un componente modifique su estado, el resto de componentes que contenga alguno de los atoms o selectors modificados, se re-renderizará instantáneamente.

Recursos

Artículos relacionados

Comentarios