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' **
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;