Search code examples
reactjsreduxredux-thunksemantic-ui-react

Not sure why my redux state is reverting?


I am using a Modal component from Semantic UI React; And because the way my app is structured for responsiveness, I found the modal not being active across different breakpoints — essentially I have two different components handling the different break points or experiences i.e. mobile and desktop. Because of this I decided to add the state of the modal to my redux store.

However I noticed a behavior upon clicking the button to close, the state moves to false for a micro-second and then back to true. Thus giving the appearance the close button is stuck or not working.

This is my modal component:

import React, { Component } from 'react'
import { Button, Modal, Transition } from 'semantic-ui-react'

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { logOutUser, modalStateOn, modalStateOff  } from '../../store/index'

class MyModal extends Component {

 close = () => {
  const { modalStateOff } = this.props
  modalStateOff();
 }

 logOutUser = () => {
  const { logOutUser } = this.props
  logOutUser()
 }

 render() {
  const { modalActive } = this.props

  console.log("this.props in Modal ", this.props);

   return (
    <>
      <Modal dimmer={'blurring'} size={'mini'} open={modalActive} onClose={this.close}>
        <Modal.Header>
         <p>Are you sure you want to log out of your account?</p>
        </Modal.Header>
        <Modal.Actions>
         <Button
          color='black'
         onClick={this.close}
         >
          No
       </Button>
         <Button
          positive
          icon='checkmark'
          labelPosition='right'
          content='Yes'
         onClick={() => { this.close; this.logOutUser() }}
         />
        </Modal.Actions>
      </Modal>
    </>
   )
 }
}

function mapStateToProps(state) {
 const { modalActive } = state
 return { modalActive }
}
const mapDispatchToProps = dispatch =>
 bindActionCreators({ logOutUser, modalStateOn, modalStateOff }, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(MyModal)

This is my store:

import { createStore, applyMiddleware } from 'redux';

import { composeWithDevTools } from 'redux-devtools-extension';
import { persistStore } from 'redux-persist';

import { createLogger } from 'redux-logger'
import thunkMiddleware from 'redux-thunk';

/* initial state */
const startState = { isLoggedIn: false, modalActive: false }

/* action types */
export const actionTypes = {
    IS_LOGGED_IN: 'IS_LOGGED_IN',
    IS_LOGGED_OUT: 'IS_LOGGED_OUT',
    MODAL_ACTIVE: 'MODAL_ACTIVE',
    MODAL_INACTIVE: 'MODAL_INACTIVE'
}

/* reducer(s) */
export const reducer = (state = startState, action) => {
    switch (action.type) {
        case actionTypes.IS_LOGGED_IN:
          return Object.assign({}, state, {
            isLoggedIn: true,
          });
        case actionTypes.IS_LOGGED_OUT:
          return Object.assign({}, state, {
            isLoggedIn: false,
          });
        case actionTypes.MODAL_ACTIVE:
          return Object.assign({}, state, {
            modalActive: true
          });
        case actionTypes.MODAL_INACTIVE:
          return Object.assign({}, state, {
            modalActive: false
      });
        default:
            return state
    }
};

/* actions */
export const logInUser = () => {
    return { type: actionTypes.IS_LOGGED_IN }
}
export const logOutUser = () => {
    return { type: actionTypes.IS_LOGGED_OUT }
}
export const modalStateOn = () => {
 return { type: actionTypes.MODAL_ACTIVE, modalActive: true}
}

export const modalStateOff = () => {
 return { type: actionTypes.MODAL_INACTIVE, modalActive: false }
}

export default () => {
 let store;
 const isClient = typeof window !== 'undefined';
 if (isClient) {
  const { persistReducer } = require('redux-persist');
  const storage = require('redux-persist/lib/storage').default;
  const persistConfig = {
   key: 'primary',
   storage,
   whitelist: ['isLoggedIn', 'modalActive'], // place to select which state you want to persist

  }
  store = createStore(
   persistReducer(persistConfig, reducer),
   startState,
     composeWithDevTools(applyMiddleware(
            thunkMiddleware,
            createLogger({ collapsed: false })
        ))
  );
  store.__PERSISTOR = persistStore(store);
 } else {
  store = createStore(
   reducer,
   startState,
     composeWithDevTools(applyMiddleware(
            thunkMiddleware,
            createLogger({ collapsed: false })
        ))
  );
 }
 return store;
};

Thanks in advance!

UPDATE As per kkesley below, decided to console.log in my modalStateOn action function:

export const modalStateOn = () => {
 console.log('In modalStateOn action')
 return { type: actionTypes.MODAL_ACTIVE, modalActive: true}
}

This is a screenshot of what I got back:

enter image description here

And this is the component which calls the Modal:

import React, { Component } from 'react'
import { Link, NavLink, withRouter } from 'react-router-dom'
import Modal from '../components/Modal/MyModal.jsx'
import {
  Container,
  Menu,
  Responsive,
  Segment,
  Visibility,
  Sidebar,
  Icon,
  Button
} from 'semantic-ui-react'

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { modalStateOn, modalStateOff } from '../store/index'

const getWidth = () => {
 const isSSR = typeof window === 'undefined'

 return isSSR ? Responsive.onlyTablet.minWidth : window.innerWidth
}

const logOutMenuItemHelper = (isMobile, isLoggedIn, history, modalActive, nav, NavLink, modalStateOn, modalStateOff, handleSidebarHide) => {
 function mobilelogOutMenuItemHelper(history, modalActive, nav, NavLink, modalStateOn, modalStateOff, handleSidebarHide) {
  if (nav.name === 'Log in') {
   console.log("mobile nav.name ", nav.name);

   return (
    <Menu.Item
     key="/logout"
     name='Log out'
     onClick={(event) => { modalStateOn(); handleSidebarHide();}}>
     {modalActive ? <Modal history={history} isLoggedIn={isLoggedIn} modalActive={modalActive} modalStateOn={modalStateOn}  modalStateOff={modalStateOff} /> : 'Log Out'}
    </Menu.Item>
   )
  } else {
   return (
    <Menu.Item
     exact
     key={nav.name}
     as={NavLink}
     to={nav.path}
     name={nav.name}
     onClick={() => {
      handleSidebarHide()
     }}
    >
    </Menu.Item>
   )
  }

 }

 function desktoplogOutMenuItemHelper(history, modalActive, nav, NavLink, modalStateOn,  modalStateOff) {
  if (nav.name === 'Log in') {
 // console.log("desktop nav.name ", nav.name);

   return (
    <Menu.Item
     key="/logout"
     name='Log out'
     onClick={() => { modalStateOn(); }}>
     {(modalActive) ? <Modal history={history} isLoggedIn={isLoggedIn} modalActive={modalActive} modalStateOn={modalStateOn} modalStateOff={modalStateOff} /> : 'Log Out'}
    </Menu.Item>
   )
  } else {
   return (
    <Menu.Item
     exact
     key={nav.name}
     as={NavLink}
     to={nav.path}
     name={nav.name}
    >
    </Menu.Item>
   )
  }
 }

 if (isMobile && isLoggedIn) {
  return mobilelogOutMenuItemHelper(history, modalActive, nav, NavLink, modalStateOn, modalStateOff, handleSidebarHide)
 }
 return desktoplogOutMenuItemHelper(history, modalActive, nav, NavLink, modalStateOn, modalStateOff)
}

class DesktopContainer extends Component {
 state = {}

 hideFixedMenu = () => this.setState({ fixed: false })
 showFixedMenu = () => this.setState({ fixed: true })

 render() {
  const { fixed } = this.state;
  const { history, data, children, isLoggedIn, modalActive, modalStateOn, modalStateOff } = this.props
  console.log("this.props desktop in LinkNAV ", this.props);

  return (
   <Responsive getWidth={getWidth} minWidth={Responsive.onlyTablet.minWidth}>
    <Visibility
     once={false}
     onBottomPassed={this.showFixedMenu}
     onBottomPassedReverse={this.hideFixedMenu}
    >
     <Segment
      inverted
      textAlign='center'
      style={{ minHeight: 'auto', padding: '0' }}
      vertical
     >
      <Menu
       fixed={fixed ? 'top' : null}
       inverted={!fixed}
       pointing={!fixed}
       secondary={!fixed}
       size='large'
      >
       {/* {console.log("isLoggedIn in desktop homecomponent ", isLoggedIn)} */}
       {isLoggedIn ?
        data.filter(function (nav) {
         return (nav.name !== "Register")
        })
         .map(nav => {
          return (
           logOutMenuItemHelper(false, isLoggedIn, history, modalActive, nav, NavLink, modalStateOn, modalStateOff)
          )
         })
        :
        data.filter(function (nav) {
         return (nav.name != "Profile") && (nav.name != "Dashboard")
        })
         .map(nav => {
          return (
           <Menu.Item
            exact
            key={nav.path}
            as={NavLink}
            to={nav.path}
            name={nav.name}
           >
           </Menu.Item>
          )
         })}

      </Menu>
     </Segment>
    </Visibility>
    {children}

   </Responsive>
  );
 }
}

class MobileContainer extends Component {
 state = {}

 handleSidebarHide = () => this.setState({ sidebarOpened: false })

 handleToggle = () => this.setState({ sidebarOpened: true })

 render() {
  const { children, history, data, isLoggedIn, modalActive, modalStateOn, modalStateOff } = this.props
  const { sidebarOpened} = this.state

 console.log("this.props inMobile ", this.props);
  return (
   <Responsive
    as={Sidebar.Pushable}
    getWidth={getWidth}
    maxWidth={Responsive.onlyMobile.maxWidth}
   >
    <Sidebar
     as={Menu}
     animation='push'
     inverted
     onHide={this.handleSidebarHide}
     vertical
     visible={sidebarOpened}
    >
     {/* {console.log("isLoggedIn in desktop homecomponent ", isLoggedIn)} */}
     {isLoggedIn ?
      data.filter(function (nav) {
       return (nav.name !== "Register")
      })
       .map(nav => {
        return (
         logOutMenuItemHelper(false, isLoggedIn, history, modalActive, nav, NavLink, modalStateOn, modalStateOff, this.handleSidebarHide)
        )
       })
      :
      data.filter(function (nav) {
       return (nav.name != "Profile") && (nav.name != "Dashboard")
      })
       .map(nav => {
        return (
         <Menu.Item
          exact
          key={nav.name}
          as={NavLink}
          to={nav.path}
          name={nav.name}
          onClick={this.handleSidebarHide}
         >
         </Menu.Item>
        )
       })}

    </Sidebar>

    <Sidebar.Pusher dimmed={sidebarOpened}>
     <Segment
      inverted
      textAlign='center'
      style={{ minHeight: 'auto', padding: '1em 0em' }}
      vertical
     >
      <Container>
       <Menu inverted pointing secondary size='large'>
        <Menu.Item onClick={this.handleToggle}>
         <Icon name='sidebar' />
        </Menu.Item>
        <Menu.Item position='right'>

         <Button inverted>
          {isLoggedIn
           ? <Link to="/">Log out</Link>
           : <Link to="/login">Log in</Link>
          }
          </Button>
         {!isLoggedIn ? <Button inverted style={{ marginLeft: '0.5em' }}>
          <Link to="/register"><span>Register!</span></Link>
         </Button>: null}
        </Menu.Item>
       </Menu>
      </Container>
     </Segment>

     {children}

    </Sidebar.Pusher>
   </Responsive>
  );
 }
}

const LinkNavWithLayout = ({ GenericHeadingComponent, children, history, data, modalActive, modalStateOn, modalStateOff, isLoggedIn }) => (
 <React.Fragment>
  <DesktopContainer GenericHeadingComponent={GenericHeadingComponent} history={history} data={data} modalActive={modalActive} modalStateOn={modalStateOn} modalStateOff={modalStateOff} isLoggedIn={isLoggedIn}>
   {children}
  </DesktopContainer>
  <MobileContainer GenericHeadingComponent={GenericHeadingComponent} history={history} data={data} modalActive={modalActive} modalStateOn={modalStateOn} modalStateOff={modalStateOff} isLoggedIn={isLoggedIn}>
   {children}
  </MobileContainer>
 </React.Fragment>
)

function mapStateToProps(state) {
 const { isLoggedIn, modalActive } = state
 return { isLoggedIn, modalActive }
}

const mapDispatchToProps = dispatch =>
 bindActionCreators({ modalStateOn, modalStateOff }, dispatch)

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LinkNavWithLayout))

Upon adding throw new Error('test')

to...

export const modalStateOn = () => {
 throw new Error('test')
 return { type: actionTypes.MODAL_ACTIVE, modalActive: true}
}

it yielded,

enter image description here


Solution

  • I think you can return this

    <>
    {modalActive && <Modal history={history} isLoggedIn={isLoggedIn} modalActive={modalActive} modalStateOn={modalStateOn}  modalStateOff={modalStateOff} />}
    <Menu.Item
         key="/logout"
         name='Log out'
         onClick={(event) => { modalStateOn(); handleSidebarHide();}}>
         Log Out
    </Menu.Item>
    </>
    

    note that you have to do this for every <Menu.Item/> that renders a modal

    The bug is because when you click on the modal, the event propagates to the menu item as well..