Dynalon / reactive-state

Redux-clone build with strict typing and RxJS down to its core. Wrist-friendly, no boilerplate or endless switch statements
MIT License
138 stars 7 forks source link

Collaborate on getting redux-dev-tools working #4

Closed Ronsku closed 6 years ago

Ronsku commented 7 years ago

I thought I would share the code I'm trying to get redux-dev-tools to work. There seems to be some async issues.

Check this out, it's ugly but for now it's just for testing purposes:

import { Store, Reducer, Action } from 'reactive-state';
import { createStore, compose } from 'redux'

interface CounterState {
    value: number;
}

interface AppState {
    counter: CounterState;
}

const counterInitialState: CounterState = {
    value: 0
}

const appInitialState = {
    counter: counterInitialState
}

export const rootState = Store.create(appInitialState);

export const counterAction = new Action<number>();
const counterReducer: Reducer<CounterState, number> = (state, payload = 1) => {
    return { ...state, value: state.value + payload };
}

const counterStore: Store<CounterState> = rootState.createSlice("counter");
counterStore.addReducer(counterAction, counterReducer);

const reducer = (state = { counter: { value: 0 } }, action) => {
    console.log('reducer state: ', state.counter.value);
    if (action.payload !== undefined) {
        console.log('reducer payload: ', action.payload.counter.value);
        return action.payload;
    } else
        return state;
}

const reduxStore = createStore(
    reducer,
    { counter: { value: 0 } },
    window['devToolsExtension'] ? window['devToolsExtension']() : f => f
)

rootState.select(s => s).subscribe(state => {
    console.log(`state: ${state.counter.value}`);
    reduxStore.dispatch({ type: 'ACTION', payload: state })
});

counterAction.next();
counterAction.next();
counterAction.next();

Result: redux-dev-tools

Values come in right, but redux-dev-tools seems too slow. This works also by binding some action to a mouse action and spamming actions fast. If you do it slowly everything is fine. Does redux-dev-tools have some kind of debounceTime? If so, why? seems stupid?

Dynalon commented 7 years ago

I am not sure if redux-dev-tools has a debounce time; will need to investigate that.

To test things if its really a delay, you could slow down the "sync" to redux using .timer() from RxJS:


rootState
    .select(s => s)
    .zip(Observable.timer(0, 1000))
    .subscribe(([state, timer]) => {
    console.log(`state: ${state.counter.value}`);
    reduxStore.dispatch({ type: 'ACTION', payload: state })
    });
Dynalon commented 7 years ago

I looked a little into how redux-devtools work internally, and played a little. I could get it to work so that I can see actions in the devtools using your approach (every action has type 'ACTION'). Of course, deleting/skipping actions had no effect ( as there is yet no sync back from rexu store to the reactive state store).

I am now convinced that with a little work, it is possible to fully integrate with redux devtools using this approach:

  1. Create a custom "enhancer" function, which is a middle ware for redux that can modify the store created by redux. devtools does this already, overriding the default store with a custom dispatch function that keeps a journal in localstorage of what actions got dispatched, and the state change that this action triggered. It should be possible to create a ReactiveState middleware that hooks into the devtools middleware (read: executes after the devtools middleware executed) and would feed state changes from the reactive-state store into the redux store, and back.
  2. Make changes to the reactive-state core and add a strictly-for-developer-tools-only API that returns an Observable that will emit a single dataset consisting of (1) the action name (string), (2) the state before the reducer triggered by that action became active and (3) the state after the reducer has worked and modified it and possibly (4) a custom cloneDeep/deserializer function passed in as argument from the devtools, so the states that are emitted are cloned/serialized (not sure if (4) is really needed).

Right now I want to focus on the API and the functionality of ReactiveState, I am building a complex application that I just migrated to ReactiveState to prove it is feasible. I have also some API additions in mind (i.e. provide a connect() like function as Redux has) and might not work on the first task (devtools enhancer) in the next time. I am, however, willing to complete (2) within a few days as this is something that could be useful in more scenarios (i.e. just logging to console what actions got dispatced and how they changed the state) and support development.

Ronsku commented 7 years ago

Sounds like a very good plan. It would really strengthen this library by having the dev-tools working with easy separation from production.

Ronsku commented 7 years ago

I would love to see your solutions in a more complex application using reactive-state :)

Dynalon commented 7 years ago

I just added an simple devtool API that lets you register a function to be notified whenever an action updates the state. For examples, look at the devtool unit tests. This can be used to console.log the state change, or (what we need) to map it to redux/redux devtool middleware.

Ronsku commented 7 years ago

Sweet, looks very good. I will test it out tomorrow! :) Thank you!

Dynalon commented 7 years ago

I played around a little and got a bit closer. The devtools now show at least the correct action names (if you provide a name as argument in the constrcutor of the Action) and payloads that were dispatched. The state changes is however not picked correctly for whatever reason.

Here is my current approach:

import { createStore, combineReducers, StoreCreator, StoreEnhancer, compose } from "redux"

const change = new Subject<any>();
export const serverStore = Store.create<ServerState>();

const devtoolExtension: StoreEnhancer<any> = window["devToolsExtension"]();
let currentState: any;
const reduxToReactiveSync = new Subject<any>();

serverStore.select().take(1).subscribe(initialState => {
    const enhancer = (next) => {
        currentState = initialState;

        return (reducer, initialState) => {
            // this returns the store instance modifid by the devtool enhancer
            const store = next(reducer, initialState);

            // write back the state from DevTools/Redux to our ReactiveState
            store.subscribe(() => {
                const reduxState = store.getState();
                reduxToReactiveSync.next(reduxState);
            })

            // forward the actions from ReactiveState devtool notification to the underlying Redux store
            change.subscribe(p => {
                currentState = p.state;
                store.dispatch({ type: p.actionName, payload: p.payload });
            });
            return store;
        };
    };

    const redux = createStore(
        f => f, // reducer
        initialState,
        compose(enhancer, devtoolExtension)
    );
});

serverStore.devTool = {
    notifyStateChange: (actionName, payload, state) => {
        if (actionName !== "INTERNAL_SYNC")
            change.next({ actionName, payload, state: state });
    }
}

const syncReducer = (state, payload) => {
    return { ...payload };
};
serverStore.addReducer(reduxToReactiveSync, syncReducer, "INTERNAL_SYNC");
Dynalon commented 6 years ago

I'm closing this as I think the current devtool support is all we can get. Here is what works:

Here is what not works:

Other features not tested, if its possible to integrate them we should open a separate issue for that.