Como gerenciar estado sem usar Assunto ou manipulação imperativa em um exemplo simples de RxJS?

Estou experimentando o RxJS há duas semanas e, embora eu o ame em princípio, não consigo encontrar e implementar o padrão correto para gerenciar o estado. Todos os artigos e perguntas parecem concordar:

Subject sempre que possível, deve ser evitado em favor de apenas empurrar o estado através de transformações;.getValue() deve ser totalmente reprovado; e.do talvez deva ser evitado, exceto pela manipulação do DOM?

O problema com todas essas sugestões é que nenhuma literatura parece dizer diretamente o que você deveria usar, além de "você aprenderá o modo Rx e deixará de usar o Assunto".

Mas não consigo encontrar um exemplo direto em nenhum lugar que indique especificamente a maneira correta de executar adições e remoções em um único fluxo / objeto, como conseqüência de várias outras entradas de fluxo, de maneira sem estado e funcional.

Antes de ser apontado novamente nas mesmas direções, os problemas com a literatura descoberta são:

A introdução à programação reativa Você está ausente: excelente texto inicial, mas não aborda especificamente essas questões.O exemplo TODO para RxJS vem com React e envolve manipulação explícita deSubjects como proxies para React Stores.http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/ : usa explicitamente umstate objeto para adição e remoção de itens.

Minha talvez décima reescrita do TODO padrão segue - Minhas iterações anteriores abordadas incluem:

começando com uma matriz mutável de 'itens' - ruim porque o estado é explícito e é imperativamente gerenciadousandoscan concatenar novos itens para umaddedItems$ e, em seguida, ramifique outro fluxo em que os itens removidos foram excluídos - ruim como oaddedItems$ O fluxo aumentaria indefinidamente.descobrindoBehaviorSubjecte usando isso - parecia ruim, pois para cada novoupdatedList$.next() emissão, requer o valor anterior para iterar, o que significa queSubject.getValue() é essencial.tentando transmitir o resultado doinputEnter$ eventos de adição em eventos de remoção filtrada - mas cada novo fluxo cria uma nova lista e, em seguida, alimenta-a notoggleItem$ etoggleAll$ fluxos significa que cada novo fluxo depende do anterior e, portanto, causar uma das 4 ações (adicionar, remover, alternar item ou alternar tudo) exige que toda a cadeia seja executada desnecessariamente novamente.

Agora cheguei ao círculo completo, onde voltei a usar os doisSubject (e como ele deve ser repetidamente repetido de qualquer maneira sem usargetValue()?) edo, como mostrado abaixo. Eu e meu colega concordamos que esta é a maneira mais clara, mas é claro que parece a menos reativa e mais imperativa. Qualquer sugestão clara sobre o caminho correto para isso seria muito 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

Em relação à resposta muito útil do @ user3743222, posso ver como representar o estado como uma entrada adicional pode tornar uma função pura e, portanto,scan é a melhor maneira de representar uma coleção evoluindo ao longo do tempo, com uma captura instantânea de seu estado anterior até esse ponto como um parâmetro de função adicional.

No entanto, já era assim que abordava minha segunda tentativa, comaddedItems$ sendo um fluxo de entradas digitalizado:

// 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...

A solução óbvia seria apenas teritems = []e manipule-o diretamente ouconst items = new BehaviorSubject([]) - mas a única maneira de iterar parece estar usandogetValue para expor o estado anterior, que Andre Stalz (CycleJS) comentou nas questões do RxJS como algo que realmente não deveria ser exposto (mas, se não, então como é utilizável?).

Acho que tive uma idéia de que, com fluxos, você não deveria usar Assuntos ou representar qualquer coisa através de um estado 'almôndega', e na primeira resposta não tenho certeza de como isso não introduz fluxos de cadeia em massa que são órfãos / crescem infinitamente / têm que construir um sobre o outro na seqüência exata.

questionAnswers(1)

yourAnswerToTheQuestion