Search code examples
javascriptreactjsreact-reduxreact-thunk

In presentational component, Redux Store state is undefined


I want to access Redux Store State in presentational component, but the state is undefined. I made Container Component, Presentational Component, and Redux Store.

So I tried to access user in presentational component but user is undefined.

I use Redux-Thunk when create Redux Store for asynchronous API request.

The doubful part is that Rdux Store State has a value but i can't access the state user in presentaional component

// headerState.js

import { handleActions } from 'redux-actions';
import * as api from '../lib/api';

const USER_STATE = 'headerState/USER_STATE';
const USER_STATE_SUCCESS = 'headerState/USER_STATE_SUCCESS';
const USER_STATE_FAILURE = 'headerState/USER_STATE_FAILURE';

const USER_LOGOUT = 'headerState/USER_LOGOUT';
const USER_LOGOUT_SUCCESS = 'headerState/USER_LOGOUT_SUCCESS';
const USER_LOGOUT_FAILURE = 'headerState/USER_LOGOUT_FAILURE';

export const getUserInfo = () => async (dispatch) => {
  dispatch({ type: USER_STATE });
  try {
    const response = await api.getUser();
    dispatch({
      type: USER_STATE_SUCCESS,
      payload: response.data,
    });
  } catch (error) {
    dispatch({
      type: USER_STATE_FAILURE,
      payload: error,
      error: true,
    });
    throw error;
  }
};

export const userLogout = () => async (dispatch) => {
  dispatch({ type: USER_LOGOUT });
  try {
    const response = await api.logout();
    dispatch({
      type: USER_LOGOUT_SUCCESS,
      payload: response.data,
    });
  } catch (error) {
    dispatch({
      type: USER_LOGOUT_FAILURE,
      payload: error,
      error: true,
    });
    throw error;
  }
};

const initialState = {
  user: null,
  loading: {
    isProcessing: false,
  },
};

const headerState = handleActions(
  {
    [USER_STATE]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        isProcessing: true,
      },
    }),
    [USER_STATE_SUCCESS]: (state, action) => ({
      ...state,
      user: action.payload,
      loading: {
        ...state.loading,
        isProcessing: false,
      },
    }),
    [USER_STATE_FAILURE]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        isProcessing: false,
      },
    }),
    [USER_LOGOUT]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        isProcessing: true,
      },
    }),
    [USER_LOGOUT_SUCCESS]: (state, action) => ({
      ...state,
      user: action.payload,
      loading: {
        ...state.loading,
        isProcessing: false,
      },
    }),
    [USER_LOGOUT_FAILURE]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        isProcessing: false,
      },
    }),
  },
  initialState,
);

export default headerState;

// HeaderContainer.js

import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import Header from './common/Header';
import { getUserInfo, userLogout } from '../modules/headerState';

const HeaderContainer = ({ user, getUserInfo, userLogout }) => {
  useEffect(() => {
    getUserInfo();
  }, [getUserInfo]);

  return <Header user={user} logout={userLogout} />;
};

const mapStateToProps = (state) => {
  return {
    user: state.user,
    loading: state.loading,
  };
};

export default connect(mapStateToProps, {
  getUserInfo,
  userLogout,
})(HeaderContainer);

// Header.js

(...)

const Header = ({ user, logout }) => {  ** // user is undefined **
  return (
    <>
      <TopNav>
        <Link to="/" style={TitleLinkStyle}>
          <NavTitle>
            <img
              src={logo}
              width="50"
              height="50"
              color="white"
              style={{ paddingRight: '25px', alignSelf: 'center' }}
              alt="logo"
            />
            Service Title
          </NavTitle>
        </Link>
        <div style={{ display: 'flex', flex: 4 }} />
        <SubNav>
          <Link to="/home" style={{ textDecoration: 'none', color: 'white' }}>
            <HomeDiv>Home</HomeDiv>
          </Link>
          {!user.snsId && (
            <Link
              to="/login"
              style={{ textDecoration: 'none', color: 'white' }}
            >
              <LoginDiv>Login</LoginDiv>
            </Link>
          )}
          {user.snsId && (
            <LoginDiv onClick={logout} style={{ cursor: 'pointer' }}>
              Logout
            </LoginDiv>
          )}
        </SubNav>
      </TopNav>
    </>
  );
};

export default Header;


Solution

  • There are two potential problems.

    First, the initial value of user is null until USER_STATE_SUCCESS is dispatched. So trying to access user.snsId in your presentation component will cause a type error:

    Cannot read property 'snsId' of undefined

    You can use optional chaining to attempt to access snsId and simplify your logic.

    // Header.js
    const Header = ({ user }) => {
      return user?.snsId ? <div>Logout</div> : <div>Login</div>;
    };
    

    The second issue potentially lies in how you are creating and accessing your store. I have assumed that you are doing something like this:

    const reducers = combineReducers({
      header: headerReducer
    });
    const store = createStore(reducers);
    

    When using mapStateToProps, your container component needs to access that state slice by the same key with which it was created - header.

    // HeaderContainer.js
    const mapStateToProps = (state) => {
      return {
        user: state.header.user, // state.user is undefined
        loading: state.header.loading // state.loading is undefined
      };
    };
    

    Edit serene-booth-z4x4u