简体   繁体   中英

Problems in displaying redux state changes in react components

Update to the original post: I have realized that the issue is that the updates in the Redux state (props to the WaitingRoom component) somehow set the entire state of the component to its initial state. Thus, "showmatchingwindow" was set to false as well. Initially, after MATCHING_REQUEST was dispatched and "loading" was set to true, the state of WaitingRoom component still remained unchanged. However, after MATCHING_SUCCESS was dispatched and "loading" turned false and "buddy" and "chatid" in the Redux state got updated, all the fields in the state of WaitingRoom were somehow reset to their initial values. This is weird and did not happen with my other Redux actions. I am wondering whether anyone can help me explain what caused this. Really appreciate your patience and any help you can offer!

Link to the Github repo : https://github.com/liu550/salon_project (WaitingRoom component is in src/components/waitingroom.js

///Below is the original post///

I am building a random matching feature for my website. Here is a brief description : I want a chat window to pop up once 2 users are successfully matched up. Inside my Redux state, I have (1) "loading," (2) "buddy" representing the id of the user that our current user is matched with, and (3) "chatid" representing the id of the firestore document that stores the chat history between 2 users. The startMatching Redux action is called in the WaitingRoom component which is a web page that displays other users' profiles in addition to having the matching button. The chat window is the MatchingWindow component, which needs a chatid as its props to render the corresponding chat history between 2 users.

I am sure that my Redux actions work fine but got problems when passing the updated Redux state to my React components. Below are 2 approaches that I tried:

Approach 1 : Display the chat window inside a modal returned by the WaitingRoom component. As you can see, what I am doing here is just conditional rendering. However, with this approach, the modal somehow quickly showed up for less than a second and then disappeared along with the previous modal where users select their gender preferences. Checking the props to the WaitingRoom component, I found that "buddy" and "chatid" were indeed updated. Shouldn't MatchingWindow be rendered? I also tried just putting success> instead of the MatchingWindow component and it was the same outcome.

Approach 1 (WaitingRoom)

class WaitingRoom extends Component {

    state = {
      ......
    }

    showMatching = (e) => {
      e.preventDefault();
      this.setState({
        showmatching: true
      })
    }

    handleMatching = (e) => {
      e.preventDefault();
      this.props.startMatching(this.props.auth.uid, this.props.profile, this.props.profile.gender, this.state.genderpreference);
      this.setState({
        showmatchingwindow: true
      })
    }

    closeMatching = () => {
      this.setState({
        showmatching: false
      })
    }


      render() {

        const { auth, users, profile, loading, sent, buddy, chatid } = this.props;


        return (

            <div>
            <Button variant="success" onClick={this.showMatching} style={{ fontSize: "large", fontFamily: "Comic Sans MS, cursive, sans-serif"}}>Randomly find a study/work buddy instantly!</Button>
            </div>


            <Modal show={this.state.showmatching} onHide={this.closeMatching}>
              <Modal.Header closeButton></Modal.Header>
              <Modal.Body>
                <form onSubmit={this.handleMatching}>
                  Match me with:
                  <div className="form-inline">
                        <div className="radio-container">
                            <label>Male only</label><input type="radio" id="genderpreference" value="Male" onChange={this.handleChange} checked={this.state.genderpreference === "Male"} />   
                        </div>

                  ......

                  </div>

                  <button>Start matching</button>
                </form>
              </Modal.Body>
            </Modal>

            <Modal show={this.state.showmatchingwindow} onHide={this.closeMatching}>
              <Modal.Header closeButton></Modal.Header>
              <Modal.Body>
                { loading ?  <div class="loader"></div> : null}
                { buddy !== "" && loading === false ? <MatchingWindow chatid=chatid /> : null }
                { buddy === "" && loading === false ? <span>Sorry we cannot help you find a study/work buddy currently</span> : null }
              </Modal.Body>
            </Modal>

        );
      }
    }

const mapStateToProps = (state) => {
  return {
    users: state.firestore.ordered.users,
    auth: state.firebase.auth,
    profile: state.firebase.profile,
    loading: state.auth.loading,
    sent: state.auth.sent,
    buddy: state.auth.buddy,
    chatid: state.auth.chatid
  }
}

export default compose(
connect(mapStateToProps, {submitTicket, startMatching, groupMessaging}),
firestoreConnect([
{ collection: 'users' }
])
)(WaitingRoom);

Approach 2 : Create a separate MatchingBox component, which will render different elements/components based on different states. However, when I went with this approach, componentWillReceiveProps() was not fired after the Redux state changes, as the console.log()s I placed inside the method were not executed. It was weird because, as I mentioned in approach 1, the Redux state was indeed changed when I checked the props to the WaitingRoom component, which means that my Redux action and reducer code should be fine and it should work the same for the MatchingBox component as well. As a result of componentWillReceiveProps() not being fired, the chatid I passed to the MatchingWindow component was an empty string, which is problematic. I tried componentDidUpdate() and it did not work either.

Approach 2 (WaitingRoom)

class WaitingRoom extends Component {

      render() {

        return (

            ......All the same except for replacing the second Modal with:
            { this.state.showmatchingwindow ? <MatchingBox /> : null }

        );
      }
    }

Approach 2 (MatchingBox)

class MatchingBox extends Component {

    state = {
        loading: true,
        chatid: "",
        buddy: []
    }

    componentDidMount() {
        console.log(this.props.loading);
        console.log(this.props.chatid);
        console.log(this.props.buddy);
        this.setState({
            loading: this.props.loading,
            chatid: this.props.chatid,
            buddy: this.props.buddy
        })
    }


    componentWillReceiveProps() {
        console.log(this.props.chatid);
        console.log(this.props.buddy);
        this.setState({
            loading: this.props.loading,
            chatid: this.props.chatid,
            buddy: this.props.buddy
            })
        }

    render() {
        let box;
        if (this.props.loading === true) {
            box = <div class="loader"></div>
        }
        else {
            if (this.props.buddy !== "") {
                box = <div>
                <MatchingWindow chatid={this.state.chatid} />
                </div>    
            }
            else {
                box = <span>Sorry</span>
            }
        }
        return (
            <div>
                {box}
            </div>

        );
    }

}

const mapStateToProps = (state) => {
    return {
        auth: state.firebase.auth,
        loading: state.auth.loading,
        chatid: state.auth.chatid,
        buddy: state.auth.buddy,
        profile: state.firebase.profile
    };
}

export default connect(mapStateToProps)(MatchingBox);

As stated above, I am pretty sure my redux actions and reducer are fine but just want to make my description more understandable.

Redux actions

export const startMatching = (userid, userprofile, usergender, genderpreference) => (dispatch) => {

    dispatch({ type: MATCHING_REQUEST });

    ......

    dispatch(matchingConversation(userspool[number].id, doc.data(), userprofile, {time: "", from: "", content: ""}, number, userspool));      
}

export const matchingConversation = (user2id, user2profile, user1profile, message, number, userspool) => (dispatch, getState) => {

      .....
                .then(() => {
                    dispatch({ type: CHAT_SUCCESS, payload: doc.id });
                })
                .then(() => {
                    dispatch({ type: MATCHING_SUCCESS, payload: user2id });
                })
}

Redux reducer

const initialState = {
    loading: false,
    chatid: "",
    buddy: "",
}

const authReducer = (state = initialState, action) => {
    const {type, payload} = action;

    switch(type) {
        ......

        case CHAT_SUCCESS:
            return {
                ...state,
                chatid: action.payload
            }
        case MATCHING_REQUEST:
            return {
                ...state,
                loading: true
            }
        case MATCHING_SUCCESS:
            return {
                ...state,
                buddy: action.payload,
                loading: false
            }
        case MATCHING_FAIL:
            return {
                ...state,
                buddy: action.payload,
                loading: false
            }
        default: return state;
    }
}

Firstly, I don't think you need to use any internal state here. When you know multiple components have to share same variables, and they are not hierarchial, just use a store and use application state.

Secondly, I think in your reducer, you might be pointing to the same initial state and directly mutating it (against redux rules). In short, due to shallow copy, your state might be pointing to the same old initial state object and thus your react component is not detecting a change.

Try creating a deep copy (not shallow copy) of your initial state in your redux reducer before you return a new state. These links talk about these concepts:

  1. https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns
  2. React component not updating when store state has changed

Lastly, should not your buddy in the initial state be an empty array instead of a string?

You seem to be using a function to return the component:

// main.js
const WaitingRoomPage = () => {
  return <WaitingRoom />;
};

...

<Route path='/waitingroom' component={WaitingRoomPage} />

I don't think you should be doing that because every re-render of Main will cause your WaitingRoom to keep remounting (and thus re-initializing) . Your WaitingRoom is not a functional component, and even if it was, you should still just use the simpler way of declaring a Route with a component attribute:

<Route path='/waitingroom' component={WaitingRoom} />

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM