Redux doesn't fetch array after action has dispatched - reactjs

I'm looking to append the data in the UPLOAD_IMAGE to GET_IMAGES. Without having to re-rerendering the component. Or in other words, without having to refresh the page.
I get type errors whenever img is followed
<Typography className={classes.imageTypographyTitle} variant="h4" align="center">{img.image_title}</Typography>
<Divider className={classes.imageDivider} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
........
TypeError: Cannot read property 'image_title' of undefined
On refresh i see the new data, and i can add data, and i can see the updated array. The type error only happens if the images array is empty.
I would like to append the data to the empty array, and show the data without re render/refresh or any type errors errors.
Should i use another lifecycle method ? because componentWillMount Cannot be called twice, just once. So given that array is empty, should i use something like shouldComponentUpdate to fetch the initial data ?
data structure given that their is existing data in the array.
0:{
"id": 71,
"image_title": "ii",
"img_url": "https://*********",
"created_at": "2019-06-24T02:36:48.359Z",
"updated_at": "2019-06-24T02:36:48.359Z",
"user_id": 1,
"user": {
"id": 1,
"googleId": null,
"username": "a******",
"password": "**********",
"email": "a********",
"created_at": "2019-06-23T18:57:17.253Z",
"updated_at": "2019-06-23T18:57:17.253Z"
},
"comments": []
}
reducer
import { GET_IMAGES, POST_COMMENT, DELETE_IMAGE, UPLOAD_IMAGE } from '../actions/types';
const initialState = {
images:[],
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_IMAGES:
console.log(action.data);
return{
...state,
images:action.data
}
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images
// console.log(myImages); // empty array
const newImage = action.newImage
console.log(newImage[0]); // gets the new uploaded image.
return {
images:[
{
id: newImage[0].id,
user:{
username:newImage[0].user.username
},
comments:{
comment_body: newImage[0].comments.comment_body
},
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
},
myImages[0] // pass the previous images if array
/// isn't empty
]
}
default:
return state;
}
}
action
// upload image
export const uploadImage = data => {
return (dispatch) => {
Axios.post('/images/upload', data).then((response) => {
const newImage = {...response.data}
console.log(newImage);
dispatch({type:UPLOAD_IMAGE, newImage})
// history.push("/dashboard");
});
}
}
// get images
export const getImages = () => {
return async (dispatch) => {
const url = await Axios.get('/images/uploads')
const data = url.data;
dispatch({
type: GET_IMAGES,
data
})
}
}
Dashboard.js
import React, { Component } from "react";
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
import ImageUploader from 'react-images-upload';
import ImageContainer from "./ImageContainer"
import {connect} from 'react-redux';
import {getImages, deleteImage, uploadImage} from '../actions/imageActions';
import dashboardStyles from '../styles/dashboardStyles';
import {withStyles} from '#material-ui/core/styles';
import {compose} from 'redux';
class Dashboard extends Component{
constructor(props){
super(props);
this.state = {
image_url: '',
description:'',
upload:false,
isComment:false,
comment_body:''
}
}
handleUpload = file => {
const data = new FormData()
const image = file[0]
// console.log(this.state.description)
// data.append('ourImage', this.state.description)
data.append('ourImage',image, this.state.description )
this.props.uploadImage(data);
this.setState({
description: ''
})
}
handleChange = (e) => {
// e.preventDefault();
this.setState({
[e.target.name]: e.target.value
})
// console.log(this.state.description)
}
componentDidMount(){
this.props.getImages();
console.log(this.props.image.images);
}
.........
{image.images.length > 0 ? (
image.images.map( (img, i) => (
<div key={i}>
<ImageContainer img={img} deleteImg={() => this.deleteImg(img.id)}/>
</div>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}
const mapStateToProps = (state) => ({
image: state.image
})
const mapDispatchToProps = (dispatch) => ({
getImages: () => dispatch(getImages()),
uploadImage: (data) => dispatch(uploadImage(data))
})
export default compose(connect(mapStateToProps, mapDispatchToProps), withStyles(dashboardStyles))(Dashboard)
image container
render(){
const { img, deleteImg, classes } = this.props
return(
<Grid item sm={12} md={12} className={classes.imageGridItem}>
<Paper className={classes.imageContainerPaper}>
{/* // empty image_title */}
<Typography className={classes.imageTypographyTitle} variant="h4" align="center">{img.image_title}</Typography>
<Divider className={classes.imageDivider} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
........
</Grid>
)
}
}

You need to spread the existing images array inside your new state.
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images
// console.log(myImages); // empty array
const newImage = action.newImage
console.log(newImage[0]); // gets the new uploaded image.
return {
images:[
{
id: newImage[0].id,
user:{
username:newImage[0].user.username
},
comments:{
comment_body: newImage[0].comments.comment_body
},
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
},
...state.images
]
}
So with that you have a new state, with your new image first, followed by the initial images.

Fix. remove this line.
myImages[0] // pass

Related

How can I make an API call on clicking a button in React

I want to fetch data from a currency API when I click the Button component. I have placed the code to request data in a useEffect hook but when I attempt to place the useEffect in the handleClickOpen function, it returns an error. Should I leave out useEEffect? Where should I place the API call in the code below?
import * as React from 'react';
import { useEffect } from "react"
import axios from "axios"
import PropTypes from 'prop-types';
import Button from '#mui/material/Button';
import Avatar from '#mui/material/Avatar';
import List from '#mui/material/List';
import ListItem from '#mui/material/ListItem';
import ListItemAvatar from '#mui/material/ListItemAvatar';
import ListItemText from '#mui/material/ListItemText';
import DialogTitle from '#mui/material/DialogTitle';
import Dialog from '#mui/material/Dialog';
import { blue } from '#mui/material/colors';
const emails = [
{title: "Pound sterling", symbol: "£", id: 1, acronym: "GBP"},
{title: "Euro", symbol: "€", id: 2, acronym: "EUR"},
{title: "Nigerian Naira", symbol: "₦", id: 3, acronym: "NGN"},
{title: "Saudi Arabian riyal", symbol: "SR", id: 4, acronym: "SAR"},
{title: "Indian rupee", symbol: "₹", id: 5, acronym: "INR"},
{title: "United States dollar", symbol: "$", id: 6, acronym: "USD"},
]
function SimpleDialog(props) {
const { onClose, selectedValue, open } = props;
const handleClose = () => {
onClose(selectedValue);
};
const handleListItemClick = (value) => {
onClose(value);
};
useEffect(() => {
const options = {
method: 'GET',
url: 'https://exchangerate-api.p.rapidapi.com/rapid/latest/USD',
headers: {
'X-RapidAPI-Key': 'c0d2e70417msh3bde3b7dbe9e25ap12748ejsncd1fe394742c',
'X-RapidAPI-Host': 'exchangerate-api.p.rapidapi.com'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
},[])
return (
<Dialog onClose={handleClose} open={open}>
<DialogTitle>Choose a currency</DialogTitle>
<List sx={{ pt: 0 }}>
{emails.map((email) => (
<ListItem button onClick={() => handleListItemClick(email.symbol)} key={email.id}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: blue[100], color: blue[600] }}>
{email.symbol}
</Avatar>
</ListItemAvatar>
<ListItemText primary={email.title} />
</ListItem>
))}
</List>
</Dialog>
);
}
SimpleDialog.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
selectedValue: PropTypes.string.isRequired,
};
export default function SimpleDialogDemo({selectedValue, setSelectedValue}) {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = (value) => {
setOpen(false);
setSelectedValue(value);
};
return (
<div>
<Button variant="text" onClick={handleClickOpen} sx={{
ml: 30,
mb: 0,
p: 0,
}}>
{selectedValue} {emails.find(email => email.symbol===selectedValue).acronym}
</Button>
<SimpleDialog
selectedValue={selectedValue}
open={open}
onClose={handleClose}
/>
</div>
);
}
Just move the API call to a separate function, say getAPIData or makeAPICall. Either use a normal function or use useCallback to avoid multiple creations of the function.
You can now call this function in useEffect on initial load as well as anywhere else you want to make the same API call.
use a state variable to store the data obtained from the API call and use this variable to render the JSX Element.
const makeAPICall = () => {
//api call and response
//store response in a state variable
}
useEffect(()=>{
makeAPICall();
}, [])
const handleClickEvent = () => {
makeAPICall();
}
I recommending you using reducer for handling such action, so when you click a button it will dispatch a state to do API fetch in reducer and dispatch a state when page load inside useEffect.
First thing is react does not recomment or allow using of hooks like useEffect in other functions, thats why you are getting error. Now since you need your data on click, place the fetch code you have written in useEffect in ClickHandler ( you donot need useEffect for the scenario defined above ).
const handleListItemClick = () => {
const options = {
method: 'GET',
url: 'https://exchangerate-api.p.rapidapi.com/rapid/latest/USD',
headers: {
'X-RapidAPI-Key': 'c0d2e70417msh3bde3b7dbe9e25ap12748ejsncd1fe394742c',
'X-RapidAPI-Host': 'exchangerate-api.p.rapidapi.com'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
}
And if you want to make use of this data fetched, add it in a state variable and use it. :)

How can I access state inside of my Redux cartReucer?

I need to access my current cart state which is just a list of products that have
been added to the cart, so that I check ids in order to calculate quantity for duplicate products. I realize that one of my issues here is that i've initialized itemsInCart with an empty array but i'm not sure what else to do here since, state can't be destructured without it.
cartReducer.js
const itemsInCart = []
export const cartReducer = (state = itemsInCart, action) => {
const { type, payload } = action;
switch (type) {
case "ADD_TO_CART":
return [
...state,
{
imgUrl: payload.imgUrl,
name: payload.name,
price: payload.price,
quantity: payload.quantity
},
];
default:
}
return state;
};
Product.js
Clicking the button dispatches the 'ADD_TO_CART' action, adds new products to our cart in state.
import React from 'react';
import {useDispatch} from 'react-redux';
const Product = ({imgUrl, name, price, id }) => {
const dispatch = useDispatch()
const addToCart = () => {
dispatch({
type: "ADD_TO_CART",
payload: {
imgUrl: imgUrl,
name: name,
price: price,
id: id
}
})
}
return (
<div
key={id}
style={{
textAlign: "center",
display: "flex",
border: "1px solid",
marginBottom: "2rem",
flexDirection: 'column'
}}
>
<img
src={imgUrl}
style={{ height: "5rem", width: "5rem" }}
alt="The product"
/>
<h3>{name}</h3>
<p>${price}.00</p>
{id}
<button onClick={()=>addToCart()}>Add To Cart</button>
</div>
);
}
export default Product;
InCartList.js
Renders list of items in my cart inside CartContainer
import React from "react";
import { useSelector } from "react-redux";
import CartItem from "./CartItem";
const ProductList = () => {
const allState = useSelector((state) => state.cart);
const renderProducts = () => {
return allState.map((product) => {
return (
<CartItem id={product.id} quantity={product.quantity}key={product.id} name={product.name} price={product.price}/>
);
});
};
return <>{renderProducts()}</>;
};
export default ProductList;
You shouldn't place any logic inside reducer (reducer should only pure function)
You can try to get state you want before dispatch action ADD_TO_CART
use getStateToProps function
use store which should be exported when initialized inside App component (I guess)
export const store = configureAppStore(history);

Value in countUp and even in typography not updating

I'm trying to modify the proyect of { https://www.youtube.com/watch?v=khJlrj3Y6Ls&t=3311s } to my country, so, instead of having countries displayed in the option box and selecting one for updating the value of the cards, I'm trying to achieve to update the numbers when selecting a region, one by one... The options and region names are done.
But, when selecting a region the number is not updating and just displays zero, even is receiving the correct data in JSON (and even passing a number it just desplays zero).
I'm sorry if I'm a bit ambiguous but I'm newbie in React. How I can achieve this?, it just displays zero... And I have a lot of hours trying to resolve this.
Thanks and also, the 99% of the work is from the tutorial of JavaScript Mastery.
There's the code.
import axios from 'axios';
const url = 'https://covid19.mathdro.id/api/countries/CHILE';
export const fetchData = async (provincia) => {
let changeableUrl = url;
if (provincia) {
changeableUrl = `${url}/confirmed`;
try {
const {
data: [
confirmed,
recovered,
deaths,
lastUpdate,
]
} = await axios.get(changeableUrl);
return {
confirmed,
recovered,
deaths,
lastUpdate,
};
} catch (error) {
return error;
}
} else {
try {
const {
data: {
confirmed,
recovered,
deaths,
lastUpdate,
}
} = await axios.get(changeableUrl);
return {
confirmed,
recovered,
deaths,
lastUpdate,
};
} catch (error) {
return error;
}
}
};
export const fetchDailyData = async () => {
try {
const {
data
} = await axios.get(`${url}`);
return data.map(({
confirmed,
deaths,
reportDate: date
}) => ({
confirmed: confirmed.total,
deaths: deaths.total,
date
}));
} catch (error) {
return error;
}
};
export const fetchCountries = async () => {
try {
const {data: provincias } = await axios.get(`${url}/confirmed`);
return provincias.map((provincia) => provincia.provinceState);
} catch (error) {
return error;
}
};
There's the APP.js
import React from 'react';
// import Cards from './components/Cards/Cards.jsx';
// import Chart from './components/Chart/Chart.jsx';
// import CountryPicker from './components/CountryPicker/CountryPicker.jsx';
import {Cards, Chart, CountryPicker} from './components';
import styles from './App.module.css';
import { fetchData} from './api';
// Acá se agrega el componente
class App extends React.Component {
state = {
data: {},
provincia: '',
}
async componentDidMount(){
const fetchedData = await fetchData();
this.setState({ data: fetchedData })
}
handleCountryChange = async (provincia) => {
const fetchedData = await fetchData(provincia);
console.log(fetchedData)
this.setState({ data: fetchedData, provincia: provincia })
}
render() {
const { data, provincia} = this.state;
return (
<div className={styles.container}>
<Cards data={data} />
<CountryPicker handleCountryChange = {
this.handleCountryChange
}
/>
<Chart data={data} provincia={provincia} />
</div>
)
}
}
export default App;
CARDS.js
import React from 'react';
import { Card, CardContent, Typography, Grid } from '#material-ui/core';
import CountUp from 'react-countup';
import cx from 'classnames';
import styles from './Cards.module.css';
const Cards = ({ data :{confirmed, recovered, deaths, lastUpdate}}) => {
if(!confirmed) {
return 'Cargando...';
}
return(
<div className={styles.container}>
<Grid container spacing={3} justify="center">
<Grid item component={Card} xs={12} md={3} className={cx(styles.card, styles.infectados)}>
<CardContent>
<Typography color="textSecondary" gutterBottom>Casos Totales</Typography>
<Typography variant="h5">
<CountUp start={0} end={confirmed.value} duration={2.5} separator=","
/>
</Typography>
<Typography color="TextSecondary">{new Date(lastUpdate).toLocaleDateString()}</Typography>
<Typography variant="body2">Número de Casos Totales de COVID-19</Typography>
</CardContent>
</Grid>
<Grid item component={Card} xs={12} md={3} className={cx(styles.card, styles.recuperados)}>
<CardContent>
<Typography color="textSecondary" gutterBottom>Recuperados</Typography>
<Typography variant="h5">
<CountUp start={0} end={recovered.value} duration={2.5} separator=","
/>
</Typography>
<Typography color="TextSecondary">{new Date(lastUpdate).toLocaleDateString()}</Typography>
<Typography variant="body2">Número de Casos Recuperados Totales de COVID-19</Typography>
</CardContent>
</Grid>
<Grid item component={Card} xs={12} md={3} className={cx(styles.card, styles.muertes)}>
<CardContent>
<Typography color="textSecondary" gutterBottom>Muertes</Typography>
<Typography variant="h5">
<CountUp start={0} end={deaths.value} duration={2.5} separator=","
/>
</Typography>
<Typography color="TextSecondary">{new Date(lastUpdate).toLocaleDateString()}</Typography>
<Typography variant="body2">Número de muertes totales causadas por COVID-19</Typography>
</CardContent>
</Grid>
</Grid>
</div>
)
}
export default Cards;
And the COUNTRYPICKER.js (regionpicker in my case)
import React, { useState, useEffect} from 'react';
import { NativeSelect, FormControl } from '#material-ui/core';
import styles from './CountryPicker.module.css';
import { fetchCountries } from '../../api';
const CountryPicker = ({
handleCountryChange
}) => {
const [fetchedCountries, setFetchedCountries] = useState([]);
useEffect(() => {
const fetchAPI = async () => {
setFetchedCountries(await fetchCountries());
}
fetchAPI();
}, [setFetchedCountries]);
return (
<FormControl className={styles.formControl}>
<NativeSelect defaultValue = ""
onChange = {
(e) => {
handleCountryChange(e.target.value)
}
} >
{/* Lo siguiente es para el selector de países */}
<option value="Chile">Todo Chile</option>
{fetchedCountries.map((provincia, i) => <option key={i} value={provincia}>{provincia}</option>)}
</NativeSelect>
</FormControl>
)
}
export default CountryPicker;
At first it is updating correct
JSON when it is updating at correct
Then, when selecting a region it is not updating and just displays zero
JSON when it is not displaying correctly
the problem is the API response format for the provincia data.
I fixed it doing this in the api/index.js
export const fetchData = async (provincia) => {
let changeableUrl = url;
if (provincia) {
changeableUrl = `${url}/confirmed`;
try {
const data = await axios.get(changeableUrl);
const filteredData = data.data.find((province) => {
if (province.provinceState == provincia){
return province;
};
})
return {confirmed: {value: filteredData.confirmed} , recovered: {value: filteredData.recovered} , deaths: {value: filteredData.deaths},
lastUpdate: new Date(filteredData.lastUpdate).toISOString()};
} catch (error) {
return error;
}
} else {
try {
const {
data: {
confirmed,
recovered,
deaths,
lastUpdate,
}
} = await axios.get(changeableUrl);
console.log("fetchdata else: ", confirmed,
recovered,
deaths,
lastUpdate);
return {
confirmed,
recovered,
deaths,
lastUpdate,
};
} catch (error) {
return error;
}
}
};
Let me know if that works for you.

How to update array object within reducer

TLDR: How to update array object within the reducer
I would need some help understanding how to update the like count value of my post data once the action has been fired, and possibly a working logic.
Posts are being fetched from an action, being passed and mapped as a posts prop. Ideally it should make a new likes object on upvote
A user is able to click like, and on the backend its adds a like. Which is good.
The front end needs to upvote the current value to plus +1, however the current logic is not working.
Getting this error with current logic
there seem to be an error TypeError: Invalid attempt to spread
non-iterable instance
console.log(index) renders the like count for whatever post the user clicked on.
for example like
20
I would not be able to use state, i would need to do this in redux.
https://i.stack.imgur.com/1N0Nh.png <- idea of what the front end looks like
Here is the Posts Structure
{
"id": 5,
"title": "React Interview Questiossssnsdd",
"post_content": "ssss",
"username": "blueowl",
"createdAt": "2019-04-26T09:38:10.324Z",
"updatedAt": "2019-04-26T18:55:39.319Z",
"userId": 1,
"Likes": [
{
"id": 131,
"like": true,
"createdAt": "2019-04-26T12:20:58.251Z",
"updatedAt": "2019-04-26T12:20:58.251Z",
"userId": 1,
"postId": 5
},
{
"id": 152,
"like": true,
"createdAt": "2019-04-26T14:01:13.347Z",
"updatedAt": "2019-04-26T14:01:13.347Z",
"userId": 1,
"postId": 5
},
{
"id": 153,
"like": true,
"createdAt": "2019-04-26T14:01:46.739Z",
"updatedAt": "2019-04-26T14:01:46.739Z",
"userId": 1,
"postId": 5
},...
Example Likes Structure
[
{
"id": 182,
"like": true,
"createdAt": "2019-04-27T11:05:05.612Z",
"updatedAt": "2019-04-27T11:05:05.612Z",
"userId": 1,
"postId": 5
},
{
"id": 178,
"like": true,
"createdAt": "2019-04-27T10:44:49.311Z",
"updatedAt": "2019-04-27T10:44:49.311Z",
"userId": 1,
"postId": 5
},
{
"id": 179,
"like": true,
"createdAt": "2019-04-27T10:45:27.380Z",
"updatedAt": "2019-04-27T10:45:27.380Z",
"userId": 1,
"postId": 5
},
{
"id": 180,
"like": true,
"createdAt": "2019-04-27T10:46:44.260Z",
"updatedAt": "2019-04-27T10:46:44.260Z",
"userId": 1,
"postId": 5
},
reducer
const initialState = {
post: [],
postError: null,
posts:[],
isEditing:false,
isEditingId:null,
likes:[],
someLike:[],
postId:null
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
console.log(action.data)
return {
...state,
posts: action.data, // maps posts fine,
}
case ADD_LIKE:
console.log(action.id) // renders post id
// console.log(state.posts) // logs posts array
console.log(state.posts)
const index = state.posts.find((post) => post.id === action.id).Likes.length
console.log(index); // gets likes length for the corresponding id to whatever post that has been clickd
// renders 5 or 3 (their is currently 2 posts)
// honestly don't what im doing below this line of code but should make a new like object
return [
{
Likes: [
...state.posts.find((post) => post.id === action.id).Likes.length + 1,
action.newLikeObject
]
}
]
show update count below here
myLikes={post.Likes.length} // right here
render(){
const {posts} = this.props; // from reducer
return (
<div>
{posts.map(post => (
<Paper key={post.id} style={Styles.myPaper}>
<PostItem
myLikes={post.Likes.length} // right here
myTitle={this.state.title}
editChange={this.onChange}
editForm={this.formEditing}
isEditing={this.props.isEditingId === post.id}
removePost={this.removePost}
{...post}
/>
</Paper>
))}
</div>
);
}
}
extra info
actions.js
export const postLike = (id) => {
return (dispatch) => {
// console.log(userId);
return Axios.post('/api/posts/like', {
postId: id
}).then( (like) => {
dispatch({type: ADD_LIKE, id})
// console.log('you have liked this', like)
}).catch( (err)=> {
console.log('there seem to be an error', err);
})
}
}
Edit
console.log(newState)
{
"post": [],
"postError": null,
"posts": [
{
"id": 5,
"title": "React Interview Questiossssnsdd",
"post_content": "ssss",
"username": "EliHood",
"createdAt": "2019-04-26T09:38:10.324Z",
"updatedAt": "2019-04-26T18:55:39.319Z",
"userId": 1,
"Likes": [
{
"id": 219,
"like": true,
"createdAt": "2019-04-27T15:54:03.841Z",
"updatedAt": "2019-04-27T15:54:03.841Z",
"userId": 1,
"postId": 5
},
{
"id": 189,
"like": true,
"createdAt": "2019-04-27T11:11:07.558Z",
"updatedAt": "2019-04-27T11:11:07.558Z",
"userId": 1,
"postId": 5
},
{
"id": 190,
"like": true,
"createdAt": "2019-04-27T11:12:09.599Z",
"updatedAt": "2019-04-27T11:12:09.599Z",
"userId": 1,
"postId": 5
},
....,
"isEditing": false,
"isEditingId": null,
"likes": [
77,
24
],
"someLike": [],
"postId": null
}
Like Component
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faCoffee, faAdjust } from '#fortawesome/free-solid-svg-icons';
import {connect} from 'react-redux';
import { postLike} from '../actions/';
class Like extends Component{
constructor(props){
super(props);
this.state = {
likes: null,
heart: false
}
}
// passes post id thats stored in PostItem.js
clickLike = (id) => {
this.props.postLike(id);
// toggles between css class
this.setState({
heart: !this.state.heart
})
}
render(){
return(
<div style={{float:'right', fontSize: '1.5em', color:'tomato'}} >
<i style={{ marginRight: '140px'}} className={this.state.heart ? 'fa fa-heart':'fa fa-heart-o' }>
<span style={{ marginLeft: '6px'}}>
<a href="#" onClick={() =>this.clickLike(this.props.like)}>Like</a>
</span>
{/* gets the like counts */}
<span style={{ marginLeft: '7px'}} >{this.props.likes} </span>
</i>
</div>
)
}
}
const mapStateToProps = (state) => ({
isEditingId: state.post.isEditingId,
})
const mapDispatchToProps = (dispatch) => ({
postLike: (id) => dispatch( postLike(id))
// Pass id to the DeletePost functions.
});
export default connect(mapStateToProps, mapDispatchToProps)(Like);
Like component being passed here as <Like like={id} likes={myLikes} />
PostItem.js
import React, { Component } from 'react';
import Paper from '#material-ui/core/Paper';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import moment from 'moment';
import Editable from './Editable';
import {connect} from 'react-redux';
import {UpdatePost, postLike, getCount} from '../actions/';
import Like from './Like';
import Axios from '../Axios';
const Styles = {
myPaper: {
margin: '20px 0px',
padding: '20px'
},
button:{
marginRight:'30px'
}
}
class PostItem extends Component{
constructor(props){
super(props);
this.state = {
disabled: false,
myId: 0,
likes:0
}
}
componentWillMount(){
}
onUpdate = (id, title) => () => {
// we need the id so expres knows what post to update, and the title being that only editing the title.
if(this.props.myTitle !== null){
const creds = {
id, title
}
this.props.UpdatePost(creds);
}
}
render(){
const {title, id, userId, removePost, createdAt, post_content, username, editForm, isEditing, editChange, myTitle, postUpdate, Likes, clickLike, myLikes} = this.props
return(
<div>
<Typography variant="h6" component="h3">
{/* if else teneray operator */}
{isEditing ? (
<Editable editField={myTitle ? myTitle : title} editChange={editChange}/>
): (
<div>
{title}
</div>
)}
</Typography>
<Typography component={'span'} variant={'body2'}>
{post_content}
<h5>by: {username} </h5>
{/* component span cancels out the cant be a decedent of error */}
<Typography component={'span'} variant={'body2'} color="textSecondary">{moment(createdAt).calendar()}</Typography>
{/* gets like counts */}
<Like like={id} likes={myLikes} />
</Typography>
{!isEditing ? (
<Button variant="outlined" type="submit" onClick={editForm(id)}>
Edit
</Button>
):(
// pass id, and myTitle which as we remember myTitle is the new value when updating the title
<div>
<Button
disabled={myTitle.length <= 3}
variant="outlined"
onClick={this.onUpdate(id, myTitle)}>
Update
</Button>
<Button
variant="outlined"
style={{marginLeft: '0.7%'}}
onClick={editForm(null)}>
Close
</Button>
</div>
)}
{!isEditing && (
<Button
style={{marginLeft: '0.7%'}}
variant="outlined"
color="primary"
type="submit"
onClick={removePost(id)}>
Remove
</Button>
)}
</div>
)
}
}
const mapStateToProps = (state) => ({
isEditingId: state.post.isEditingId,
})
const mapDispatchToProps = (dispatch) => ({
// pass creds which can be called anything, but i just call it credentials but it should be called something more
// specific.
UpdatePost: (creds) => dispatch(UpdatePost(creds)),
postLike: (id) => dispatch( postLike(id)),
// Pass id to the DeletePost functions.
});
export default connect(mapStateToProps, mapDispatchToProps)(PostItem);
Posts.js (Master parent)
import React, { Component } from 'react';
import PostList from './PostList';
import {connect} from 'react-redux';
import { withRouter, Redirect} from 'react-router-dom';
import {GetPosts} from '../actions/';
const Styles = {
myPaper:{
margin: '20px 0px',
padding:'20px'
}
,
wrapper:{
padding:'0px 60px'
}
}
class Posts extends Component {
state = {
posts: [],
loading: true,
isEditing: false,
// likes:[]
}
async componentWillMount(){
await this.props.GetPosts();
const thesePosts = await this.props.myPosts
const myPosts2 = await thesePosts
// const filtered = myPosts2.map((post) => post.Likes )
// const likesCount = filtered.map( (like) => like.length)
this.setState({
posts: myPosts2,
loading:false
})
}
render() {
const {loading} = this.state;
const { myPosts} = this.props
if (!this.props.isAuthenticated) {
return (<Redirect to='/signIn' />);
}
if(loading){
return "loading..."
}
return (
<div className="App" style={Styles.wrapper}>
<h1> Posts </h1>
<PostList posts={this.state.posts}/>
</div>
);
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.user.isAuthenticated,
myPosts: state.post.posts,
})
const mapDispatchToProps = (dispatch, state) => ({
GetPosts: () => dispatch( GetPosts())
});
export default withRouter(connect(mapStateToProps,mapDispatchToProps)(Posts));
PostList.js
import React, { Component } from 'react';
import Paper from '#material-ui/core/Paper';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import moment from 'moment';
import {connect} from 'react-redux';
import {DeletePost, postLike, UpdatePost,EditChange, GetPosts, getCount, DisableButton} from '../actions/';
import PostItem from './PostItem';
import _ from 'lodash';
const Styles = {
myPaper: {
margin: '20px 0px',
padding: '20px'
}
}
class PostList extends Component{
constructor(props){
super(props);
this.state ={
title: '',
loading:true,
posts:[],
}
}
componentWillMount(){
this.props.GetPosts();
const ourPosts = this.props.myPosts
this.setState({
posts: ourPosts,
loading:false
})
console.log(this.state.posts)
}
componentWillReceiveProps(nextProps) {
const hasNewLike = false;
if(this.state.posts && this.state.posts.length) {
for(let index=0; index < nextProps.myPosts.length; index++) {
if(nextProps.myPosts[index].Likes.length !=
this.state.posts[index].Likes.length) {
hasNewLike = true;
}
}
}
if(hasNewLike) {
this.setState({posts: nextProps.myPosts}); // here we are updating the posts state if redux state has updated value of likes
}
console.log(nextProps.myPosts)
}
// Return a new function. Otherwise the DeletePost action will be dispatch each
// time the Component rerenders.
removePost = (id) => () => {
this.props.DeletePost(id);
}
onChange = (e) => {
e.preventDefault();
this.setState({
title: e.target.value
})
}
formEditing = (id) => ()=> {;
this.props.EditChange(id);
}
render(){
// const {posts, ourLikes, likes} = this.props;
// console.log(posts)
// console.log(this.props.ourLikes);
return (
<div>
{this.state.posts.map(post => (
<Paper key={post.id} style={Styles.myPaper}>
<PostItem
myLikes={post.Likes.length} // right here
myTitle={this.state.title}
editChange={this.onChange}
editForm={this.formEditing}
isEditing={this.props.isEditingId === post.id}
removePost={this.removePost}
{...post}
/>
</Paper>
))}
</div>
);
}
}
const mapStateToProps = (state) => ({
isEditingId: state.post.isEditingId,
myPosts: state.post.posts,
// ourLikes: state.post.likes // reducer likes
})
const mapDispatchToProps = (dispatch) => ({
// pass creds which can be called anything, but i just call it credentials but it should be called something more
// specific.
EditChange: (id) => dispatch(EditChange(id)),
UpdatePost: (creds) => dispatch(UpdatePost(creds)),
postLike: (id) => dispatch( postLike(id)),
GetPosts: () => dispatch( GetPosts()),
// Pass id to the DeletePost functions.
DeletePost: (id) => dispatch(DeletePost(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
The error seems be occurring due to the code below
...state.posts.find((post) => post.id === action.id).Likes.length + 1
so here, we are finding the length of likes whose result will be a number and then we are trying to spread a number type variable, but spread operator (...) works for iterables like object, array.
From what I understand we want to update the likes array in posts collection.
case ADD_LIKE:
const newState = {...state}; // here I am trying to shallow copy the existing state
newState.posts.find(post => post.id == action.id).Likes.push(action.newLikeObject); // here we are trying to append the new like object to already existing **likes** array in the **posts** which should now make the count increase by 1
return newState;
if we want to use spread operator to update the array, we can use as below:
case ADD_LIKE:
const newState = {...state}; // here I am trying to shallow copy the existing state;
const existingLikesOfPost = newState.posts.find(post => post.id == action.id).Likes;
newState.posts.find(post => post.id == action.id).Likes = [...existingLikesOfPost, action.newLikeObject]; // using this approach I got some code duplication so I suggested the first approach of using **push** method of array.
return newState;
In Posts.js we can add another lifecycle method, like below:
componentWillReceiveProps(nextProps) {
const hasNewLike = false;
if(this.state.posts && this.state.posts.length) {
for(let index=0; index < nextProps.myPosts.length; index++) {
if(nextProps.myPosts[index].Likes.length !=
this.state.posts[index].Likes.length) {
hasNewLike = true;
}
}
}
if(hasNewLike) {
this.setState({posts: nextProps.myPosts}); // here we are updating the posts state if redux state has updated value of likes
}
}
edited above solution to use componentWillrecieveProps instead of getDerivedStateFromProps
You're currently trying to spread an integer with the following line:
...state.posts.find((post) => post.id === action.id).Likes.length + 1,
(you shouldn't try and modify an array's length property directly like this, if that's what you were trying to do)
Modifying deeply nested objects like this is pretty annoying without a library like ramda, but I think you're looking for something like this in your return statement:
// copy your state's posts
const newPosts = [...state.posts]
// Find the post you're adding a like to
const idx = newPosts.findIndex((post) => post.id === action.id)
const postToReplace = newPosts[idx]
// Replace that post with a copy...
newPosts[idx] = {
...postToReplace,
// ... with the Likes object also copied, with the new Like appended.
Likes: [
...postToReplace.Likes,
action.newLikeObject
]
}
return {
...state,
posts: newPosts
}
Basically, you need to drill down into your object and start replacing the elements that you're affecting in an immutable way.

Why data from API are not shown in a table on ReactJS app

I'm building a ReactJS app which should show a table populated from data from an API. The API contains flights data divided by arrivals/departures.
I'm not getting any error at this moment but an empty screen without the rows. I'm not sure what I did wrong.
In the Network tab in dev tools, I see the JSON with all the data so I'm sure the API is processed. But on screen nothing and no errors from React.
I stuck with this. I shared the code if something I'm missing I will edit adding what you will require if something not clear.
The JSON I'm getting (just a sample):
{
"arrivals": [
{
"id": 118927,
"time": "11:05",
"date": "2018-10-20",
"expected": "15:00",
"airline": "Norwegian",
"arriving_from": "Prague, Czechia - Vaclav Havel Airport Prague",
"flight_no": "D83581",
"gate": "A20",
"terminal": "",
"status": "Baggage"
},
My Component:
import React from 'react';
import FilterableTable from 'react-filterable-table';
const FlightComponent = (props) => {
const renderTime = (props) => {
if (!props.value) {
return null;
}
const date = new Date(props.value);
const formatTime = ('0' + date.getUTCHours()).slice(-2) + ":" + ('0' + date.getUTCHours()).slice(-2);
return (
<span>{formatTime}</span>
);
};
const fields = [
{ name: 'time', displayName: "Time", inputFilterable: true, sortable: true, render: renderTime },
{ name: 'airline', displayName: "Airline", inputFilterable: true, sortable: true },
{ name: 'destination', displayName: "Destination", inputFilterable: true},
{ name: 'status', displayName: "Status", inputFilterable: true}
];
return (
<FilterableTable
namespace="Flights"
data={props.flights}
pagersVisible={false}
fields={fields}
noRecordsMessage="There are no flights to display"
noFilteredRecordsMessage="No flights match your filters!"
/>
)
};
export default FlightComponent;
My Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { TabContent, TabPane, Nav, NavItem, NavLink, Row, Col } from 'reactstrap';
import classnames from 'classnames';
import { loadFlights } from '../actions/action';
import FlightsComponent from '../components/FlightsComponent';
class FlightsContainer extends Component {
constructor(props) {
super(props);
this.state = {
activeTab: '1'
};
this.props.loadFlights('departure');
}
toggle(tab) {
const filterType = tab === '1' ? 'departure' : 'arrival';
if (this.state.activeTab !== tab) {
this.setState({
activeTab: tab
});
this.props.loadFlights(filterType);
}
}
render() {
return(
<div>
<h2 className='App'>Copenhagen Airport's Flights</h2>
<div sm="12" className="tab-section">
<Nav tabs className="m-3">
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === '1' })}
onClick={() => { this.toggle('1'); }}
>
Departures
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === '2' })}
onClick={() => { this.toggle('2'); }}
>
Arrivals
</NavLink>
</NavItem>
</Nav>
<TabContent activeTab={this.state.activeTab}>
<TabPane tabId="1">
<Row>
<Col sm="12">
<FlightsComponent {...this.props}/>
</Col>
</Row>
</TabPane>
<TabPane tabId="2">
<Row>
<Col sm="12">
<FlightsComponent {...this.props}/>
</Col>
</Row>
</TabPane>
</TabContent>
</div>
</div>
)
}
}
const mapDispatchToProps = {
loadFlights
};
const mapStateToProps = (state) => {
return {
flights: state.flightReducer.flights
}
};
export default connect(mapStateToProps, mapDispatchToProps)(FlightsContainer);
Reducer:
Index.js
import { combineReducers } from 'redux';
import flightReducer from './reducer';
export default combineReducers({
flightReducer
});
Reducer.js
import {
LOAD_FLIGHT_SUCCEED,
LOAD_FLIGHT_FAILED
} from '../constant';
const initialState = {
flights: [],
error: false
};
export default function(state = initialState, action) {
switch(action.type) {
case LOAD_FLIGHT_SUCCEED:
return {
...state,
error: false,
flights: action.flights
};
case LOAD_FLIGHT_FAILED:
return {
...state,
error: true
};
default:
return state;
}
}
LoadFlights
import { LOAD_FLIGHT } from '../constant';
export function loadFlights(filter) {
return {
type: LOAD_FLIGHT,
filter
}
}
You should use dispatch method.
const mapDispatchToProps = {
loadFlights
};
should be
const mapDispatchToProps = (dispatch) => {
return {
loadFlights: (p1) => dispatch(loadFlights(p1))
}
};

Resources