Search code examples
reactjsreduxreact-reduxmaterial-ui

React/Redux Doesn't Update State Properly and Can't Be Used in Component


I am new to React and Redux and have been following the official Redux docs Todo List example https://redux.js.org/basics/example and recently changed my code to follow the presentational/container components structure. I have tried many different ways of moving the API call, but often times the weights object in the state is showing as undefined (likely because it's trying to be used before the actual API call is made, but I can't figure out how to change the API call from where it is now).

Here are the files:

actions/index.js

import axios from 'axios';

export const getWeights = () => dispatch => {
  axios.get('/api/weights')
  .then(res => 
    dispatch({
      type: 'GET_WEIGHTS',
      payload: res.data
    })
  )
}

export const setShownCategory = category => ({
  type: 'SET_SHOWN_CATEGORY',
  category
})

export const Categories = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_ACCESSORIES: 'SHOW_ACCESSORIES',
  SHOW_BARBELLS: 'SHOW_BARBELLS',
  SHOW_DUMBBELLS: 'SHOW_DUMBBELLS',
  SHOW_PLATES: 'SHOW_PLATES',
  SHOW_RACKS: 'SHOW_RACKS',
  SHOW_OTHER: 'SHOW_OTHER'
}

reducers/weights.js

const initialState = {weights: []}

export default function(state = initialState, action) {
  switch (action.type) {
    case 'GET_WEIGHTS':
      return {
        ...state,
        weights: action.payload
      }
    default:
      return state;
  }
}

components/Items/index.js

import React from 'react';
import CategoryBar from './CategoryBar'

import Typography from '@material-ui/core/Typography';
import { connect } from 'react-redux';
import { getWeights } from '../../actions'
import WeightItems from './WeightItems';

class Items extends React.Component {
  componentDidMount() {
    console.log(this.props)
    this.props.getWeights();
  }

  render() {
    const { weights } = this.props.weights;
    return (
      <div>
         <Typography variant="h4">Browse</Typography>
         <div>
           <CategoryBar />
         </div>
         <WeightItems weights={weights} />
       </div>
    )
  }
}

const mapStateToProps = state => ({
  weights: state.weights
});

export default connect(
  mapStateToProps,
  { getWeights }
)(Items)

components/Items/WeightItems.js

import React from 'react'

import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';
import Item from './Item'

const useStyles = makeStyles({
  items: {
    display: 'flex',
    margin: 'auto',
  }
})

const WeightItems = ({ weights }) => {
  const classes = useStyles()

  return (
    <Grid className={classes.items} container direction="row" justify="center" alignItems="center" spacing={3}>
      {weights.map(weight => <Item key={weight.id} weight={weight} />)}
    </Grid>
  )
}

export default WeightItems

components/Items/Item.js

import React from 'react';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';

const Item = ({ weight }) => {
  return (
    <Grid item xs={12} sm={6} md={4}>
      <Paper>{weight.title}</Paper>
    </Grid>
  )
}

export default Item

store.js

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const initialState = {};

const middleWare = [thunk];

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  rootReducer,
  initialState,
  composeEnhancers(applyMiddleware(...middleWare))
);

export default store;

Currently, I call the API in the components/Items/index.js component by importing the corresponding dispatch action and using connect(), however, instead of doing this and passing the weights in, I need to be able to call the API somewhere else and render the VisibleWeights component, based on the filter selected from a different component (I'm not showing as it's working properly and updating the state properly, but can definitely add if it needs to be shown for context).

containers/VisibleWeights.js

import { connect } from 'react-redux'
import { Categories } from '../actions'
import WeightItems from '../components/Items/WeightItems'

const getVisibleWeights = (weights, category) => {
  console.log(weights, category)
  switch(category) {
    case Categories.SHOW_ACCESSORIES:
      return weights.filter(weight => weight.category === 'Accessories')
    case Categories.SHOW_BARBELLS:
      return weights.filter(weight => weight.category === 'Barbells')
    case Categories.SHOW_DUMBBELLS:
      return weights.filter(weight => weight.category === 'Dumbbells')
    case Categories.SHOW_PLATES:
      return weights.filter(weight => weight.category === 'Plates')
    case Categories.SHOW_RACKS:
      return weights.filter(weight => weight.category === 'Racks')
    case Categories.SHOW_OTHER:
      return weights.filter(weight => weight.category === 'Other')
  }
}

const mapStateToProps = state => ({
  weights: getVisibleWeights(state.weights, state.shownCategory)
})

export default connect(
  mapStateToProps,
)(WeightItems)

Please let me know if I can add any other code or comments. I've been working on getting this little thing right for the past two days, and can not figure out what needs to be changed to make this work properly. I know the API is being called correctly, but don't know how the components need to be changed to be able to display it properly. **When I try to call the component instead of the component, I get the error: 'TypeError: Cannot read property 'map' of undefined' **


Solution

  • As you are declaring weights as an empty array [] in initial state in reducers/weights.js

    And you are destructuring it as as below

    const { weights } = this.props.weights;
    

    as weights as per your declaration is undefined you are getting the error

    TypeError: Cannot read property 'map' of undefined
    

    so try changing it as per your exact requirement

    Make sure that all your code works even with no data beacause the whole component gets rendered before the API call, so either handle the components to render with empty data and make sure your code dont beak before you get the API data

    As per your code it think wights is Array of Objects so making the following change wont break your application i think

    File: components/Items/index.js

    In render function change the following line

    const { weights } = this.props.weights;
    

    To

    const { weights = [] } = this.props;