Pila web reactiva:3RES:React, Redux, RethinkDB, Express, Socket.io

Esta publicación ha sido escrita por Scott Hasbrouck. Puedes encontrarlo en Twitter o en su sitio web.

No ha sido más que maravilloso ver que JavaScript realmente se incendió en los últimos años en la tecnología web, y finalmente se convirtió en el lenguaje más utilizado en 2016, según los datos de StackOverflow. Mi historia con JavaScript comenzó hace unos 15 años, no mucho después de que se lanzó por primera vez como parte de Netscape Navigator 2, en 1996. Mi recurso de aprendizaje más utilizado fue DynamicDrive, y sus tutoriales y fragmentos de código de "HTML dinámico" o DHTML – un término acuñado por Internet Explorer 4. En realidad, DHTML era un conjunto de características del navegador implementadas con JavaScript, CSS y HTML que podían brindarle elementos ingeniosos como botones de rollover y tableros de cotizaciones.

Avanzando rápidamente hasta hoy, ahora vivimos en un mundo donde JavaScript ha crecido para hacerse cargo de la tecnología web. ¡No solo en el navegador, sino que ahora es el lenguaje de back-end más popular según el mismo informe de StackOverflow! Naturalmente, siempre hay quienes no les gusta el lenguaje citando cosas como la facilidad de crear una variable global, o nulo por ser un objeto e indefinido por ser su propio tipo de datos. Pero descubrí que cada idioma que aprendo tiene peculiaridades que son fácilmente evitables una vez que aprendes a usarlo correctamente. Y queremos convertirnos en expertos en nuestro oficio y realmente aprender a dominar nuestras herramientas, ¿no es así?

Estos son los factores principales (buenos o no), que creo que son por qué JavaScript se ha apoderado tan rápidamente de Internet:

  1. JavaScript es el único lenguaje universal del lado del cliente.
  2. JavaScript es relativamente fácil de aprender, especialmente viniendo de cualquier otro lenguaje similar a C.
  3. Con la llegada de Node.js, JavaScript ahora puede ejecutarse en servidores (y Node/V8 es extremadamente eficiente en recursos al hacerlo).
  4. ES6 apareció en el momento justo y "arregló" muchos de los problemas con la sintaxis de JavaScript y la falta de funciones.
  5. Marcos front-end maduros. Seamos realistas, la creación de una aplicación front-end en JavaScript estándar requiere mucha disciplina para evitar que se convierta en código espagueti. React/Redux/Reflux y Angular proporcionan el marco para mantenerlo organizado.
  6. La amplitud y calidad de los proyectos de código abierto y la facilidad de instalar esos módulos con npm.

En particular, la llegada de Node.js ha llevado la adopción de JavaScript a un máximo histórico. ¡Con él, solo debemos aprender un idioma para una pila completa, y somos capaces de construir cosas como trabajadores en segundo plano y servidores HTTP con él! Incluso recientemente terminé mi primer libro sobre el cobro de tarjetas de crédito con stripe usando JavaScript y Node.js, algo que nunca pensé que sería capaz de hacer cuando aprendí el idioma por primera vez hace más de una década. Entonces, te guste o no, aquí estamos, viviendo en un mundo de Internet JavaScript. Pero aquí estás . Mi conjetura es que probablemente te guste. Que genial, bienvenido! Porque ahora quiero compartir con ustedes cómo me las arreglé para capitalizar este nuevo mundo expansivo de JavaScript para construir una pila de aplicaciones web verdaderamente reactiva, todo en un idioma de arriba a abajo.

La pila 3RES

Sí, tampoco sé cómo pronunciar eso... ¿tres? Por supuesto. Empecemos por arriba con React.

Bibliotecas de solo interfaz

Reaccionar

React es una forma declarativa de crear interfaces de usuario, que se apoya en gran medida en su extensión de sintaxis similar a XML, llamada JSX. Su aplicación se crea a partir de "componentes", cada uno de los cuales encapsula partes pequeñas, a menudo reutilizables, de su interfaz de usuario. Cada uno de estos componentes tiene su propio estado inmutable, que contiene información sobre cómo deben renderizarse los componentes. El estado tiene una función de setter pura (sin efectos secundarios) y no debe cambiarse directamente. Esta descripción general de la pila 3RES propuesta solo requerirá un conocimiento básico de React. ¡Por supuesto que quieres convertirte en un maestro de React! Asegúrese de obtener más información sobre React en SurviveJS, uno de los mejores libros completos de React con una versión gratuita.

Reducción

Si React encapsula todos sus componentes de interfaz de usuario, Redux encapsula todos sus datos representados como un objeto de JavaScript. Este objeto de estado es inmutable y no debe modificarse directamente, sino solo enviando una acción. De esta manera, React/Redux combinados pueden reaccionar automáticamente para indicar los cambios y actualizar los elementos DOM relevantes para reflejar los nuevos valores. Redux tiene una documentación increíble, probablemente una de las mejores para cualquier biblioteca de código abierto que haya usado. Para colmo, Redux también tiene 30 videos gratuitos en egghead.

Bibliotecas frontend y backend

Socket.IO

Lo más probable es que sus aplicaciones web hasta la fecha se hayan basado en AJAX para comunicarse con el servidor, que se basa en una API de JavaScript introducida por Microsoft llamada XMLHttpRequest. Para muchas acciones únicas inducidas por el usuario, como iniciar sesión, AJAX tiene mucho sentido. Sin embargo, es extremadamente derrochador confiar en él para datos que se actualizan continuamente y para múltiples clientes. La única forma real de manejar esto es sondeando regularmente el backend en intervalos cortos, solicitando nuevos datos. Los WebSockets son una tecnología relativamente nueva que ni siquiera se estandarizó hasta 2011. Un WebSocket abre una conexión TCP continuamente pendiente y permite marcos de datos que debe enviar el servidor o el cliente. Se inicia con un "apretón de manos" HTTP como una solicitud de actualización. Sin embargo, al igual que a menudo no usamos la API vanilla XMLHttpRequest (confíe en mí, tuve que hacerlo, no quiere implementar esto usted mismo y admitir todos los navegadores), normalmente tampoco usamos el JavaScript WebSocket API directamente. Socket.io es la biblioteca más ampliamente aceptada para las comunicaciones WebSocket del lado del cliente y del servidor, y también implementa un XMLHttpRequest/polling fallback para cuando fallan los WebSockets. ¡Usaremos esta biblioteca junto con las fuentes de cambios de RethinkDB (descritas a continuación) y Redux, para mantener continuamente actualizados todos los estados de nuestros clientes con nuestra base de datos!

Tecnologías y bibliotecas back-end

Repensar DB

RethinkDB es un almacén de datos NoSQL de código abierto que almacena documentos JSON. A menudo se compara con MongoDB, pero es muy superior en muchos aspectos clave que son relevantes para que nuestra pila 3RES funcione. Principalmente, RethinkDB viene listo para usar con la consulta changefeeds – ¡la capacidad de adjuntar un detector de eventos a una consulta que recibirá actualizaciones en tiempo real cada vez que se agregue, actualice o elimine un documento seleccionado por esa consulta! Como se mencionó anteriormente, emitiremos eventos de Socket.io desde nuestros feeds de cambios de RethinkDB. Además, RethinkDB es increíblemente fácil de escalar mediante fragmentación e implementa redundancia con replicación. Tiene un increíble programa de divulgación para desarrolladores y una documentación nítida, y mejora constantemente con los comentarios de ingenieros como nosotros.

Exprés

Por último, nuestra aplicación aún deberá aceptar solicitudes HTTP como rutas. Express es el marco minimalista aceptado de Node.js para crear rutas HTTP. Usaremos esto para todo lo que requiera un evento único que esté fuera del alcance de Socket.io:carga de la página inicial, inicio de sesión, registro, cierre de sesión, etc.

Construyendo el código del servidor

Nuestra aplicación de muestra será una simple lista de tareas pendientes sin autenticación. Una de mis quejas comunes es cuando la aplicación de muestra para un tutorial simple tiene una gran base de código, lo que hace que sea demasiado lento seleccionar las partes relevantes de la aplicación. Por lo tanto, esta aplicación de muestra será mínima, pero mostrará exactamente un ejemplo de cada pieza requerida de esta pila para una reactividad de extremo a extremo. La única carpeta es un /public carpeta con todo nuestro JavaScript integrado. Un punto importante que esta aplicación omite en ese espíritu es la autenticación y las sesiones:¡cualquiera en Internet puede leer y editar Todo! Si está interesado en agregar autenticación a esta aplicación con Socket.io y Express, ¡tengo un tutorial completo sobre cómo hacerlo en mi sitio web!

Comencemos con el backend. Primero, debe obtener una copia de RethinkDB y luego iniciarla con:

[Nota al margen]

Leer publicaciones de blog es bueno, pero ver cursos en video es aún mejor porque son más atractivos.

Muchos desarrolladores se quejaron de la falta de material de video de calidad asequible en Node. Es una distracción ver videos de YouTube y una locura pagar $ 500 por un curso de video de Node.

Visite Node University, que tiene cursos de video GRATUITOS en Node:node.university.

[Fin de la nota al margen]

$ rethinkdb

Una vez que inicie RethinkDB, navegue a la interfaz web súper práctica en http://localhost:8080. Haga clic en la pestaña 'Tablas' en la parte superior, luego agregue una base de datos llamada '3RES_Todo', luego, una vez creada, agregue una tabla llamada 'Todo'.

El código completo para esta muestra está en Github, por lo que solo analizaremos los puntos clave aquí, suponiendo que esté familiarizado con los conceptos básicos de Node.js. El repositorio incluye todos los módulos requeridos en package.json , pero si desea instalar manualmente los módulos necesarios para la parte de back-end de la aplicación, ejecute:

$ npm install --save rethinkdb express socket.io

Ahora que tenemos los paquetes necesarios, configuremos una aplicación de nodo básica que sirve index.html .

// index.js

// Express
var express = require('express');
var app = express();
var server = require('http').Server(app);
var path = require('path');

// Socket.io
var io = require('socket.io')(server);

// Rethinkdb
var r = require('rethinkdb');

// Socket.io changefeed events
var changefeedSocketEvents = require('./socket-events.js');

app.use(express.static('public'));

app.get('*', function(req, res) {
  res.sendFile(path.join(__dirname + '/index.html'));
});

r.connect({ db: '3RES_Todo' })
.then(function(connection) {
    io.on('connection', function (socket) {

        // insert new todos
        socket.on('todo:client:insert', function(todo) {
            r.table('Todo').insert(todo).run(connection);
        });

        // update todo
        socket.on('todo:client:update', function(todo) {
            var id = todo.id;
            delete todo.id;
            r.table('Todo').get(id).update(todo).run(connection);
        });

        // delete todo
        socket.on('todo:client:delete', function(todo) {
            var id = todo.id;
            delete todo.id;
            r.table('Todo').get(id).delete().run(connection);
        });

        // emit events for changes to todos
        r.table('Todo').changes({ includeInitial: true, squash: true }).run(connection)
        .then(changefeedSocketEvents(socket, 'todo'));
    });
    server.listen(9000);
})
.error(function(error) {
    console.log('Error connecting to RethinkDB!');
    console.log(error);
});

Más allá de las pocas líneas repetitivas Express/Node.js que probablemente haya visto cientos de veces, lo primero que notará es la conexión a RethinkDB. El connect() El método especifica la base de datos '3RES_Todo' que configuramos anteriormente. Una vez que se establece una conexión, escuchamos las conexiones de Socket.io de los clientes y luego le decimos a Express que escuche cualquier puerto que deseemos. El evento de conexión, a su vez, proporciona el socket desde el que emitimos eventos.

Ahora que tenemos una conexión RethinkDB y un Socket a un cliente, ¡configuremos la consulta de alimentación de cambios en la tabla 'Todo' de RethinkDB! Los changes() acepta un objeto literal de propiedades, del cual haremos uso de dos:El includeInitial La propiedad le dice a RethinkDB que envíe la tabla completa como el primer evento, luego escucha los cambios. El squash La propiedad asegurará que los cambios simultáneos se combinen en un solo evento, en caso de que dos usuarios cambien un Todo en el mismo instante.
Escuchar los eventos de Socket.io antes de iniciar el feed de cambios de RehtinkDB, nos permite modificar la consulta por usuario . Por ejemplo, en una aplicación del mundo real, probablemente desee transmitir todos para esa sesión de usuario específica, por lo que agregaría el ID de usuario en su consulta de RethinkDB. Como se mencionó anteriormente, si desea alguna orientación sobre cómo usar las sesiones con Socket.io, tengo un artículo completo en mi blog.

A continuación, registramos tres detectores de eventos de socket para eventos inducidos por el cliente:insertar, actualizar y eliminar. Estos eventos, a su vez, realizan las consultas RethinkDB necesarias.

Por último, verá que el feed de cambios invoca una función que estamos importando. Esta función acepta dos argumentos:la referencia del socket y una cadena de lo que queremos llamar a estas filas individuales en nuestros sockets ("todo" en este caso). Aquí está la función del controlador de alimentación de cambios que emite eventos de Socket.io:

// socket-events.js

module.exports = function(socket, entityName) {
    return function(rows) {
        rows.each(function(err, row) {
            if (err) { return console.log(err); }
            else if (row.new_val && !row.old_val) {
                socket.emit(entityName + ":insert", row.new_val);
            }
            else if (row.new_val && row.old_val) {
                socket.emit(entityName + ":update", row.new_val);
            }
            else if (row.old_val && !row.new_val) {
                socket.emit(entityName + ":delete", { id: row.old_val.id });
            }
        });
    };
};

Como puede ver, pasando el socket referencia y el entityName , devuelve una función que acepta el cursor de filas de RethinkDB. Todos los cursores RethinkDB tienen un each() método, que se puede utilizar para recorrer el cursor fila por fila. Esto nos permite analizar el new_val y el old_val de cada fila, y luego por alguna lógica simple, determinamos si cada cambio es un insert , update o delete evento. Estos tipos de eventos luego se agregan al entityName cadena, para producir eventos que se asignan a objetos de la propia entidad como:

'todo:new' => { name: "Make Bed", completed: false, id: ''48fcfafa-a2fa-454c-9ab4-8a5540d07ee0'' }

'todo:update' => { name: "Make Bed", completed: true, id: ''48fcfafa-a2fa-454c-9ab4-8a5540d07ee0'' }

'todo:delete' => { id: ''48fcfafa-a2fa-454c-9ab4-8a5540d07ee0'' }

Finalmente, para probar esto, hagamos un archivo index.html con un JavaScript simple capaz de escuchar estos eventos:

<html>
    <head>
        <script src="/socket.io/socket.io.js"></script>
        <script>
            var socket = io.connect('/');
            socket.on('todo:insert', function (data) {
                console.log("NEW");
                console.log(data);
            });
            socket.on('todo:update', function (data) {
                console.log("UPDATE");
                console.log(data);
            });
            socket.on('todo:delete', function (data) {
                console.log("DELETE");
                console.log(data);
            });
        </script>
    </head>
    <body>Checkout the Console!</body>
<html>

¡Démosle una vuelta! Vaya a su terminal (suponiendo que todavía tenga RethinkDB ejecutándose en otra pestaña) y ejecute:

$ node index.js

Abra dos pestañas en Chrome:http://localhost:9000 y http://localhost:8080. En la pestaña con nuestra aplicación de nodo simple, abra su consola de JavaScript, notará que no hay nada allí, ¡porque aún no hemos agregado ningún Todo! Ahora abra la consola RethinkDB en la pestaña del puerto 8080 en Chrome, vaya a la pestaña Explorador de datos y ejecute esta consulta:

r.db("3RES_Todo").table("Todo").insert({ name: "Make coffee", completed: false })

Ahora regrese a su otra pestaña de Chrome con la aplicación Node. ¡Viola! Está el todo que acabamos de agregar a la base de datos, claramente identificado como un nuevo registro. Ahora intente actualizar el todo, usando la identificación que RethinkDB asignó a su todo:

r.db("3RES_Todo").table("Todo").get("YOUR_TODO_ID").update({ completed: true })

Una vez más, el evento de cambio se reconoció como una actualización y el nuevo objeto pendiente se envió a nuestro cliente. Finalmente, eliminemos la tarea pendiente:

r.db("3RES_Todo").table("Todo").get("YOUR_TODO_ID").delete()

Nuestro controlador de fuente de cambios reconoció esto como un evento de eliminación y devolvió un objeto con solo la identificación (¡para que podamos eliminarlo de la matriz de todos en nuestro estado Redux!).

Esto completa todo lo requerido en el backend para enviar todos y cambios en tiempo real a nuestro front-end. Pasemos al código de React/Redux y cómo integrar estos eventos de socket con los despachadores de Redux.

Aplicación básica React Todo

Para comenzar, configuremos nuestros requisitos de front-end y la agrupación con WebPack. Primero, instale los módulos necesarios (si desactivó el repositorio y ejecutó npm install no necesitas hacer esto):

$ npm install --save react react-dom material-ui react-tap-event-plugin redux react-redux
$ npm install --save-dev webpack babel-loader babel-core babel-preset-es2015 babel-preset-react babel-plugin-transform-class-properties

Ahora configuremos Webpack, nuestro webpack.config.js también debe incluir babel, y el babel transform-class-properties complemento:

var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './components/index.jsx',
    output: { path: __dirname + '/public', filename: 'bundle.js' },
    module: {
        loaders: [{
            test: /.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
                presets: ['es2015', 'react'],
                plugins: ['transform-class-properties']
            }
        }]
    }
}

¡Estamos listos para comenzar a construir la aplicación frontal de React/Redux! Si necesita repasar React y/o Redux, los recursos mencionados en la introducción le ayudarán. Quitemos el código que teníamos en index.html para demostrar cómo funciona Socket.IO, agregue algunas fuentes, coloque una identificación en un div vacío al que podemos adjuntar la aplicación React e importe el paquete webpack:

<html>
    <head>
        <link href='https://fonts.googleapis.com/css?family=Roboto:400,300,500' rel='stylesheet' type='text/css'>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
        <script src="/socket.io/socket.io.js"></script>
    </head>
    <body style="margin: 0px;">
        <div id="main"></div>
        <script src="bundle.js"></script>
    </body>
<html>

Pongamos todo nuestro renderizado de React y alguna otra configuración en components/index.js :

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from '../stores/todos.js';

import App from './app.jsx';

// Setup our socket events to dispatch
import TodoSocketListeners from '../socket-listeners/todos.js';
TodoSocketListeners(store);

// Needed for Material-UI
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();

// Render our react app!
ReactDOM.render(<Provider store={store} ><App /></Provider>, document.getElementById('main'));

Tenga en cuenta que tenemos que importar un detector de eventos de toque molesto para Material-UI (parece que están trabajando para eliminar este requisito). Después de importar el componente raíz de la aplicación, importamos un detector de eventos de socket que envía acciones de Redux, en /socket-listeners/todos.js :

// socket-listeners/todos.js
import io from 'socket.io-client';
const socket = io.connect('/');

export default function(store) {
    socket.on('todo:insert', (todo) => {
        store.dispatch({
            type: 'todo:insert',
            todo: todo
        });
    });

    socket.on('todo:update', function (todo) {
        store.dispatch({
            type: 'todo:update',
            todo: todo
        });
    });

    socket.on('todo:delete', function (todo) {
        store.dispatch({
            type: 'todo:delete',
            todo: todo
        });
    });
}

Esta función es bastante sencilla. Todo lo que estamos haciendo es escuchar los eventos de socket emitidos desde el backend socket-events.js . Luego, enviar las tareas pendientes insertadas, actualizadas o eliminadas, que a su vez son activadas por las fuentes de cambios de RethinkDB. ¡Esto une toda la magia de RehtinkDB/Socket!

Y ahora construyamos los componentes de React que conforman la aplicación. Como importado en components/index.jsx , hagamos components/app.jsx :

import React from 'react';
import AppBar from 'material-ui/lib/app-bar';
import TodoList from './todoList.jsx';
import AddTodo from './addTodo.jsx';

import { connect } from 'react-redux';

class Main extends React.Component {
    render() {
        return (<div>
            <AppBar title="3RES Todo" iconClassNameRight="muidocs-icon-navigation-expand-more" />
            <TodoList todos={this.props.todos} />
            <AddTodo />
        </div>);
    }
}

function mapStateToProps(todos) {
    return { todos };
}

export default connect(mapStateToProps)(Main);

Todo esto es repetitivo React y React-Redux. Importamos connect de react-redux y asigne el estado a las propiedades del componente TodoList, que es components/todoList.jsx :

import React from 'react';
import Table from 'material-ui/lib/table/table';
import TableBody from 'material-ui/lib/table/table-body';
import Todo from './todo.jsx';

export default class TodoList extends React.Component {
    render() {
        return (<Table>
            <TableBody>
                {this.props.todos.map(todo => <Todo key={todo.id} todo={todo} /> )}
            </TableBody>
        </Table>);
    }
}

La lista de tareas pendientes se compone de una tabla Material-UI, y simplemente estamos asignando las tareas pendientes de los accesorios a un componente Todo individual:

import React from 'react';
import TableRow from 'material-ui/lib/table/table-row';
import TableRowColumn from 'material-ui/lib/table/table-row-column';
import Checkbox from 'material-ui/lib/checkbox';
import IconButton from 'material-ui/lib/icon-button';

// Import socket and connect
import io from 'socket.io-client';
const socket = io.connect('/');

export default class Todo extends React.Component {
    handleCheck(todo) {
        socket.emit('todo:client:update', {
            completed: !todo.completed,
            id: todo.id
        });
    };

    handleDelete(todo) {
        socket.emit('todo:client:delete', todo);
    };

    render() {
        return (<TableRow>
            <TableRowColumn>
                <Checkbox label={this.props.todo.name} checked={this.props.todo.completed} onCheck={this.handleCheck.bind(this, this.props.todo)} />
            </TableRowColumn>
            <TableRowColumn>
                <IconButton iconClassName="fa fa-trash" onFocus={this.handleDelete.bind(this, this.props.todo)} />
            </TableRowColumn>
        </TableRow>)
    }
}

El componente Todo individual adjunta emisores para los eventos de Socket.IO a los eventos de IU adecuados para la casilla de verificación y el botón Eliminar. Esto emite el todo actualizado o eliminado a los detectores de eventos de Socket en el servidor.

¡El último componente de React que necesitamos es un botón para agregar todos! Adjuntaremos un botón de agregar flotante en la esquina inferior derecha de la aplicación:

import React from 'react';
import Popover from 'material-ui/lib/popover/popover';
import FloatingActionButton from 'material-ui/lib/floating-action-button';
import ContentAdd from 'material-ui/lib/svg-icons/content/add';
import RaisedButton from 'material-ui/lib/raised-button';
import TextField from 'material-ui/lib/text-field';

// Import socket and connect
import io from 'socket.io-client';
const socket = io.connect('/');

export default class AddTodo extends React.Component {
    constructor(props) {
        super(props);
        this.state = { open: false };
    };

    handlePopoverTap = (event) => {
        this.setState({
            open: true,
            anchor: event.currentTarget
        });
    };

    handlePopoverClose = () => {
        this.setState({ open: false });
    };

    handleNewTaskInput = (event) => {
        if (event.keyCode === 13) {
            if (event.target.value && event.target.value.length > 0) {

                // Emit socket event for new todo
                socket.emit('todo:client:insert', {
                    completed: false,
                    name: event.target.value
                });

                this.handlePopoverClose();
            }
            else {
                this.setState({ error: 'Tasks must have a name'});
            }
        }
    };

    render() {
        return (<div>
            <Popover
                open = { this.state.open }
                anchorEl = { this.state.anchor }
                anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
                targetOrigin={{ horizontal: 'left', vertical: 'bottom' }}
                onRequestClose={this.handlePopoverClose}>
                <TextField
                    style={{ margin: 20 }}
                    hintText="new task"
                    errorText={ this.state.error }
                    onKeyDown={this.handleNewTaskInput} />
            </Popover>
            <FloatingActionButton onTouchTap={this.handlePopoverTap} style={{ position: 'fixed', bottom: 20, right: 20 }}>
                <ContentAdd />
            </FloatingActionButton>
        </div>)
    };
}

El método de representación de este componente incluye el botón Agregar, que luego muestra una ventana emergente con un campo de entrada. El popover está oculto y se muestra según el booleano state.open . Con cada pulsación de tecla de la entrada, invocamos handleNewTaskInput , que escucha el código clave 13 (la tecla Intro). Si el campo de entrada está vacío, se muestra un error (nota de mejora:sería bueno validar esto en el backend). Si el campo de entrada no está vacío, emitimos la nueva tarea pendiente y cerramos la ventana emergente.

Ahora, solo necesitamos un poco más de Redux repetitivo para unir todo esto. Primero, un reductor para todos, y combínelos (planificando con anticipación para cuando desarrollemos esta aplicación y tengamos varios reductores):

// reducers/todos.js

// todos reducer
const todos = (state = [], action) => {
    // return index of action's todo within state
    const todoIndex = () => {
        return state.findIndex(thisTodo => {
            return thisTodo && thisTodo.id === action.todo.id;
        });
    };

    switch(action.type) {
        case 'todo:insert':
            // append todo at end if not already found in state
            return todoIndex() < 0 ? [...state, action.todo] : state;

        case 'todo:update':
            // Merge props to update todo if matching id
            var index = todoIndex();
            if (index > -1) {
                var updatedTodo = Object.assign({}, state[index], action.todo);
                return [...state.slice(0, index), updatedTodo, ...state.slice(index + 1)]
            }
            else {
                return state;
            }

        case 'todo:delete':
            // remove matching todo
            var index = todoIndex();
            if (index > -1) {
                return [...state.slice(0, index), ...state.slice(index + 1)];
            }
            else {
                return state;
            }

        default:
            return state;
    }
};

export default todos;

Y para combinar los reductores:

// reducers/index.js

import { combineReducers } from 'redux';
import todos from './todos.js';

const todoApp = combineReducers({ todos });

export default todoApp;

Los reductores tienen una función de utilidad para verificar si el todo ya existe en el estado (notará que si deja abierta la ventana del navegador y reinicia el servidor, socket.IO volverá a emitir todos los eventos al cliente). La actualización de una tarea pendiente utiliza Object.assign() para devolver un nuevo objeto con las propiedades actualizadas de todo. Por último, eliminar hace uso de slice() – que devuelve una nueva matriz, a diferencia de splice() .

Las acciones para estos reductores:

// actions/index.js

// Socket triggered actions
// These map to socket-events.js on the server
export const newTodo = (todo) => {
    return {
        type: 'todo:new',
        todo: todo
    }
}

export const updateTodo = (todo) => {
    return {
        type: 'todo:update',
        todo: todo
    }
}

export const deleteTodo = (todo) => {
    return {
        type: 'todo:delete',
        todo: todo
    }
}

¡Pongamos todo esto junto y construyamos con webpack!

$ webpack --progress --colors --watch

Nuestro producto final es una hermosa y simple aplicación de tareas que reacciona a todos los cambios de estado para todos los clientes. Abra dos ventanas del navegador una al lado de la otra y luego intente agregar, marcar y eliminar todos. Este es un ejemplo muy simple de cómo vinculé las fuentes de cambio de RethinkDB, Socket.IO y el estado de Redux, y en realidad solo está rascando la superficie de lo que es posible. La autenticación y las sesiones realmente harían de esta una aplicación web realmente increíble. Podría imaginar una lista de tareas pendientes que se pueda compartir para grupos de usuarios como hogares, socios, etc. completa con un feed de eventos de quién está completando cada tarea que se actualiza instantáneamente a todos los usuarios que están suscritos para recibir cada grupo específico de tareas pendientes.

En el futuro, planeo trabajar más para encontrar una forma más general de vincular cualquier conjunto de objetos dentro de un estado Redux que requiera menos repeticiones:una forma de conectar una matriz de estado a un punto final de Socket.IO similar a connect() de React-Redux . ¡Me encantaría escuchar los comentarios de cualquiera que haya hecho esto o que planee implementar estas increíbles tecnologías juntas en la misma pila!

Scott Hasbrouck

Bio:Scott es un ingeniero de software de toda la vida, le encanta compartir sus habilidades con otros a través de la escritura y la tutoría. Como emprendedor en serie, actualmente es el CTO de ConvoyNow, una de las tres empresas que ha iniciado como fundador técnico, impulsando de uno a más de un millón de usuarios. Siempre está buscando la próxima aventura a través de caminatas en lugares remotos, pilotando aviones pequeños y viajando.

¡Convoy es una solución de soporte técnico en el hogar! Ponemos en contacto a los clientes que tienen problemas para reparar o usar sus dispositivos con profesionales de soporte técnico amables y expertos.

Esta publicación ha sido escrita por Scott Hasbrouck. Puedes encontrarlo en Twitter o en su sitio web.