¿Cómo administrar el estado sin usar el sujeto o la manipulación imperativa en un simple ejemplo de RxJS?

He estado experimentando con RxJS durante dos semanas, y aunque en principio me encanta, parece que no puedo encontrar e implementar el patrón correcto para administrar el estado. Todos los artículos y preguntas parecen estar de acuerdo:

Subject debe evitarse siempre que sea posible a favor de simplemente impulsar el estado a través de transformaciones;.getValue() debe quedar en desuso por completo; y.do ¿Debería evitarse a excepción de la manipulación DOM?

El problema con todas estas sugerencias es que ninguna de la literatura parece decir directamente lo que debería usar, además de "aprenderá la forma Rx y dejará de usar el Asunto".

Pero no puedo encontrar un ejemplo directo en ninguna parte que indique específicamente la forma correcta de realizar adiciones y eliminaciones a una sola secuencia / objeto, como consecuencia de múltiples entradas de secuencia, de una manera funcional y sin estado.

Antes de que apunte nuevamente en las mismas direcciones, los problemas con la literatura descubierta son:

La Introducción a la Programación Reactiva Te has estado perdiendo: excelente texto de inicio, pero no aborda específicamente estas preguntas.El ejemplo TODO para RxJS viene con React e implica la manipulación explícita deSubjects como representantes de React Stores.http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/ : utiliza explícitamente unstate objeto para agregar y quitar artículos.

Mi quizás 10a reescritura de TODO estándar sigue: Mis iteraciones anteriores cubiertas incluyen:

comenzando con una matriz de "elementos" mutable - malo ya que el estado es explícito y se maneja imperativamenteutilizandoscan para concatenar nuevos elementos a unaddedItems$ transmisión, luego bifurca otra transmisión donde se eliminaron los elementos eliminados, malo como eladdedItems$ la corriente crecería indefinidamente.descubriendoBehaviorSubjecty usar eso - parecía malo ya que para cada nuevoupdatedList$.next() emisión, requiere el valor anterior para iterar, lo que significa queSubject.getValue() es esencial.tratando de transmitir el resultado de lainputEnter$ eventos de adición en eventos de eliminación filtrados, pero luego cada nueva secuencia crea una nueva lista y luego la introduce en eltoggleItem$ ytoggleAll$ streams significa que cada nueva secuencia depende de la anterior, por lo que causar una de las 4 acciones (agregar, eliminar, alternar elemento o alternar todo) requiere que toda la cadena se ejecute innecesariamente de nuevo.

Ahora he cerrado el círculo, donde he vuelto a usar ambosSubject (y cómo se supone que debe repetirse sucesivamente de cualquier manera sin usargetValue()?) ydo, como se muestra a continuación. Mi colega y yo coincidimos en que esta es la forma más clara, pero, por supuesto, parece la menos reactiva y más imperativa. ¡Cualquier sugerencia clara sobre la forma correcta para esto sería muy apreciada!

import Rx from 'rxjs/Rx';
import h from 'virtual-dom/h';
import diff from 'virtual-dom/diff';
import patch from 'virtual-dom/patch';

const todoListContainer = document.querySelector('#todo-items-container');
const newTodoInput = document.querySelector('#new-todo');
const todoMain = document.querySelector('#main');
const todoFooter = document.querySelector('#footer');
const inputToggleAll = document.querySelector('#toggle-all');
const ENTER_KEY = 13;

// INTENTS
const inputEnter$ = Rx.Observable.fromEvent(newTodoInput, 'keyup')
    .filter(event => event.keyCode === ENTER_KEY)
    .map(event => event.target.value)
    .filter(value => value.trim().length)
    .map(value => {
        return { label: value, completed: false };
    });

const inputItemClick$ = Rx.Observable.fromEvent(todoListContainer, 'click');

const inputToggleAll$ = Rx.Observable.fromEvent(inputToggleAll, 'click')
    .map(event => event.target.checked);

const inputToggleItem$ = inputItemClick$
    .filter(event => event.target.classList.contains('toggle'))
    .map((event) => {
        return {
            label: event.target.nextElementSibling.innerText.trim(),
            completed: event.target.checked,
        };
    })

const inputDoubleClick$ = Rx.Observable.fromEvent(todoListContainer, 'dblclick')
    .filter(event => event.target.tagName === 'LABEL')
    .do((event) => {
        event.target.parentElement.classList.toggle('editing');
    })
    .map(event => event.target.innerText.trim());

const inputClickDelete$ = inputItemClick$
    .filter(event => event.target.classList.contains('destroy'))
    .map((event) => {
        return { label: event.target.previousElementSibling.innerText.trim(), completed: false };
    });

const list$ = new Rx.BehaviorSubject([]);

// MODEL / OPERATIONS
const addItem$ = inputEnter$
    .do((item) => {
        inputToggleAll.checked = false;
        list$.next(list$.getValue().concat(item));
    });

const removeItem$ = inputClickDelete$
    .do((removeItem) => {
        list$.next(list$.getValue().filter(item => item.label !== removeItem.label));
    });

const toggleAll$ = inputToggleAll$
    .do((allComplete) => {
        list$.next(toggleAllComplete(list$.getValue(), allComplete));
    });

function toggleAllComplete(arr, allComplete) {
    inputToggleAll.checked = allComplete;
    return arr.map((item) =>
        ({ label: item.label, completed: allComplete }));
}

const toggleItem$ = inputToggleItem$
    .do((toggleItem) => {
        let allComplete = toggleItem.completed;
        let noneComplete = !toggleItem.completed;
        const list = list$.getValue().map(item => {
            if (item.label === toggleItem.label) {
                item.completed = toggleItem.completed;
            }
            if (allComplete && !item.completed) {
                allComplete = false;
            }
            if (noneComplete && item.completed) {
                noneComplete = false;
            }
            return item;
        });
        if (allComplete) {
            list$.next(toggleAllComplete(list, true));
            return;
        }
        if (noneComplete) {
            list$.next(toggleAllComplete(list, false));
            return;
        }
        list$.next(list);
    });

// subscribe to all the events that cause the proxy list$ subject array to be updated
Rx.Observable.merge(addItem$, removeItem$, toggleAll$, toggleItem$).subscribe();

list$.subscribe((list) => {
    // DOM side-effects based on list size
    todoFooter.style.visibility = todoMain.style.visibility =
        (list.length) ? 'visible' : 'hidden';
    newTodoInput.value = '';
});

// RENDERING
const tree$ = list$
    .map(newList => renderList(newList));

const patches$ = tree$
    .bufferCount(2, 1)
    .map(([oldTree, newTree]) => diff(oldTree, newTree));

const todoList$ = patches$.startWith(document.querySelector('#todo-list'))
    .scan((rootNode, patches) => patch(rootNode, patches));

todoList$.subscribe();


function renderList(arr, allComplete) {
    return h('ul#todo-list', arr.map(val =>
        h('li', {
            className: (val.completed) ? 'completed' : null,
        }, [h('input', {
                className: 'toggle',
                type: 'checkbox',
                checked: val.completed,
            }), h('label', val.label),
            h('button', { className: 'destroy' }),
        ])));
}
Editar

En relación con la respuesta muy útil de @ user3743222, puedo ver cómo representar el estado como una entrada adicional puede hacer que una función sea pura y, por lo tanto,scan es la mejor manera de representar una colección que evoluciona con el tiempo, con una instantánea de su estado anterior hasta ese punto como un parámetro de función adicional.

Sin embargo, así fue como me acerqué a mi segundo intento, conaddedItems$ siendo un flujo escaneado de entradas:

// this list will now grow infinitely, because nothing is ever removed from it at the same time as concatenation?
const listWithItemsAdded$ = inputEnter$
    .startWith([])
    .scan((list, addItem) => list.concat(addItem));

const listWithItemsAddedAndRemoved$ = inputClickDelete$.withLatestFrom(listWithItemsAdded$)
    .scan((list, removeItem) => list.filter(item => item !== removeItem));

// Now I have to always work from the previous list, to get the incorporated amendments...
const listWithItemsAddedAndRemovedAndToggled$ = inputToggleItem$.withLatestFrom(listWithItemsAddedAndRemoved$)
    .map((item, list) => {
        if (item.checked === true) {
        //etc
        }
    })
    // ... and have the event triggering a bunch of previous inputs it may have nothing to do with.


// and so if I have 400 inputs it appears at this stage to still run all the previous functions every time -any- input
// changes, even if I just want to change one small part of state
const n$ = nminus1$.scan...

La solución obvia sería simplemente teneritems = []y manipularlo directamente, oconst items = new BehaviorSubject([]) - pero entonces la única forma de iterar parece estar usandogetValue para exponer el estado anterior, que Andre Stalz (CycleJS) ha comentado en los problemas de RxJS como algo que realmente no debería exponerse (pero, de nuevo, si no, ¿cómo se puede usar?).

Supongo que acabo de tener una idea de que con las transmisiones, no se suponía que debías usar Sujetos o representar nada a través de una 'albóndiga' estatal, y en la primera respuesta no estoy seguro de cómo esto no introduce transmisiones encadenadas en masa que son huérfanos / crecen infinitamente / tienen que construirse unos sobre otros en secuencia exacta.

Respuestas a la pregunta(1)

Su respuesta a la pregunta