mxdi9i7
mxdi9i7

Reputation: 697

Redux-Observable and Rxjs not capturing the first event but captures the second event

I've been working on a chat-bot app that's written in Typescript and uses Redux-Observable with rxjs. I tried to send a click event from the Shell.tsx component to my redux store which first gets intercepted by rxjs Epic and then gets sent off to redux store. But my redux store's return state does not effect the change that's supposed to be made on a click event, however it does return the expected result on a second click.

This is part of my Shell.tsx that contains the relevant component methods that fires off the click event as an action to the store:

    import * as React from 'react';
    import { ChatState, FormatState } from './Store';
    import { User } from 'botframework-directlinejs';
    import { classList } from './Chat';
    import { Dispatch, connect } from 'react-redux';
    import { Strings } from './Strings';
    import { createStore, ChatActions, sendMessage } from './Store';

    import { Subscription } from 'rxjs/Subscription';
    import { Observable } from 'rxjs/Observable';
    import 'rxjs/add/observable/fromEvent';
    import 'rxjs/add/observable/merge';

interface Props {
    inputText: string,
    strings: Strings,
    isActive: boolean,

    onChangeText: (inputText: string) => void,

    sendMessage: (inputText: string) => void
    checkActive: (isChatActive: boolean) => void
}

private handleChatClick(isChatActive) {
        this.store.dispatch({type: 'Chat_Activate', isChatActive: true})
        setTimeout(() => {
            this.store.subscribe(() => {
                this.isActive = this.store.getState().shell.isChatActive
            })
            console.log(this.isActive)
        }, 3000)
        if (this.isActive) {
            this.forceUpdate()
        }
        // this.props.checkActive(true)
    }

render() {

        //other code

        return (
            <div className={ className }>
                <div className="wc-textbox">
                {
                    console.log('chat rendered')
                }
                    <input
                        type="text"
                        className="wc-shellinput"
                        ref={ input => this.textInput = input }
                        value={ this.props.inputText }
                        onChange={ _ => this.props.onChangeText(this.textInput.value) }
                        onKeyPress={ e => this.onKeyPress(e) }
                        placeholder={ placeholder }
                        aria-label={ this.props.inputText ? null : placeholder }
                        aria-live="polite"
                        // onFocus={ this.props.handleChatClick}
                        onClick={() => {
                            this.handleChatClick(true)
                        }}
                    />
                </div>
            </div>
        );
    }


export const Shell = connect(
    (state, ownProps) => {
        return {
            inputText: state.shell.input,
            strings: state.format.strings,
            isActive: state.shell.isChatActive,

            // only used to create helper functions below
            locale: state.format.locale,
            user: state.connection.user
        }
    }
    , {
        // passed down to ShellContainer
        onChangeText: (input: string) => ({ type: 'Update_Input', input, source: "text" } as ChatActions),
        // only used to create helper functions below
        sendMessage
    }, (stateProps: any, dispatchProps: any, ownProps: any): Props => ({
        // from stateProps
        inputText: stateProps.inputText,
        strings: stateProps.strings,
        isActive: stateProps.isActive,
        // from dispatchProps
        onChangeText: dispatchProps.onChangeText,
        checkActive: dispatchProps.checkActive,
        // helper functions
        sendMessage: (text: string) => dispatchProps.sendMessage(text, stateProps.user, stateProps.locale),
    }), {
        withRef: true
    }
)(ShellContainer);

This is my the part of my Store.ts code:

export interface ShellState {
    sendTyping: boolean
    input: string,
    isChatActive: boolean,
    isPinging: boolean
}

export const setChatToActive = (isChatActive: boolean) => ({
    type: 'Chat_Activate',
    isChatActive: isChatActive,
    } as ChatActions);

export const ping = (isPinging: boolean) => ({
    type: 'Is_Pinging',
    isPinging: isPinging
} as ChatActions)

export type ShellAction = {
    type: 'Update_Input',
    input: string
    source: "text"
} |  {
    type: 'Card_Action_Clicked'
} | {
    type: 'Set_Send_Typing',
    sendTyping: boolean
} | {
    type: 'Send_Message', 
    activity: Activity
} | {
    type: 'Chat_Activate',
    isChatActive: boolean
} | {
    type: 'Is_Pinging',
    isPinging: boolean
}


export const shell: Reducer<ShellState> = (
    state: ShellState = {
        input: '',
        sendTyping: false,
        isChatActive: false,
        isPinging: false
    },
    action: ShellAction
) => {
    console.log(state)
    switch (action.type) {
        case 'Update_Input':
            return {
                ... state,
                input: action.input
            };
        case 'Send_Message':
            return {
                ... state,
                input: ''
            };
        case 'Chat_Activate':
            const newState = {
                ...state,
                isChatActive: action.isChatActive
            }
            return newState
        case 'Set_Send_Typing':
            return {
                ... state,
                sendTyping: action.sendTyping
            };

        case 'Card_Action_Clicked':
           return {
               ... state
           };
        case 'Is_Pinging':
            const newPing = {
                ... state,
                isPinging: action.isPinging
            }
            return newPing;

        default:
        return state;
    }
}




// 2. Epics 

//************************************************************************
//Import modules
import { applyMiddleware } from 'redux';
import { Epic } from 'redux-observable';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/mapTo';

import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/operator/takeUntil';

import 'rxjs/add/observable/bindCallback';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/of';

//************************************************************************
//Asynchronously send messages
const sendMessageEpic: Epic<ChatActions, ChatState> = (action$, store) =>
    action$.ofType('Send_Message')
    .map(action => {
        const state = store.getState();
        const clientActivityId = state.history.clientActivityBase + (state.history.clientActivityCounter - 1);
        return ({ type: 'Send_Message_Try', clientActivityId } as HistoryAction);
    });

const setChatToActiveEpic: Epic<ChatActions, ChatState> = (action$, store) =>
    action$.ofType('Chat_Activate')
    .mapTo({type: 'Chat_Activate', isChatActive: true} as ChatActions)
    .takeUntil(
        action$.ofType('Chat_Activate')
    )

const pingEpic: Epic<ChatActions, ChatState> = (action$, store) => 
    action$.ofType('Is_Pinging')
    .mapTo({type: 'Is_Pinging', isPinging: true} as ChatActions)
    .takeUntil(
        action$.ofType('Is_Pinging')
    )


// 3. Now we put it all together into a store with middleware

import { Store, createStore as reduxCreateStore, combineReducers } from 'redux';
import { combineEpics, createEpicMiddleware } from 'redux-observable';

export const createStore = () =>
    reduxCreateStore(
        combineReducers<ChatState>({
            shell,
            format,
            size,
            connection,
            history      
        }),
        applyMiddleware(createEpicMiddleware(combineEpics(
            updateSelectedActivityEpic,
            sendMessageEpic,
            trySendMessageEpic,
            retrySendMessageEpic,
            showTypingEpic,
            sendTypingEpic,
            setChatToActiveEpic,
            pingEpic
        )))
    );

export type ChatStore = Store<ChatState>;

In a nutshell I want to produce a console log of true when I click on the input element in my Shell.tsx. But the output is always false when I click on the input for the first time, works when I click it again.

console log results

Upvotes: 1

Views: 1018

Answers (1)

Cory Danielson
Cory Danielson

Reputation: 14501

I don't see anything immediately wrong in your code that would cause state to not be changed the first time that the action is dispatched. The logging is a little confusing though, so it might be causing you to think the code is behaving differently than it is?

From your screenshot, I can see that the first console.log statement is from store.ts line 84 (if you look all the way to the right, you can see that), and the second console.log is coming from the component.

enter image description here

In your store.ts file, you have a console.log statement at the top of the reducer. Because this logging is at the top of the reducer, it will always display the previous state, not the updated state.

export const shell: Reducer<ShellState> = (
    state: ShellState = {
        input: '',
        sendTyping: false,
        isChatActive: false,
        isPinging: false
    },
    action: ShellAction
) => {
    console.log(state)

Another thing that may be confusing you is that you are listening to store changes AFTER you've changed the store.

// this updates the store
this.store.dispatch({type: 'Chat_Activate', isChatActive: true})
// 3 seconds later, you're listening for store changes, but it's already changed
setTimeout(() => {
    this.store.subscribe(() => {
        this.isActive = this.store.getState().shell.isChatActive
    })
    // then you console.log this.isActive which might be false because that's the initial state in the reducer
    console.log(this.isActive)
}, 3000)

You should subscribe to the store BEFORE you dispatch the action, if you want to see it change.

this.store.subscribe(() => {
    this.isActive = this.store.getState().shell.isChatActive;
});
this.store.dispatch({type: 'Chat_Activate', isChatActive: true});

Alternatively, you can use Connected components from React-Redux. They will listen for store changes and update components automatically for you, so that you don't have to subscribe to the store for changes yourself.


If you want to see the stores value update immediately with console.log, you could do something like

private handleChatClick(isChatActive) {
    console.log(`before click/chat active event: ${this.store.getState().shell.isChatActive}`);
    this.store.dispatch({type: 'Chat_Activate', isChatActive: true})
    console.log(`after click/chat active event: ${this.store.getState().shell.isChatActive}`);
}

Upvotes: 1

Related Questions