How to update array object within reducer - reactjs

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.

Related

Re render component React table

I am trying to re render a component. I have a refresh button and I want to clean all filters and sorting values when clicked.
The thing is that I can not make a re render, not even with forceUpdate(), it is doing NOTHING and I don't know why. Also, I tried with setState(), and nothing. What I want to happen is what happens when I change the page, it re renders the component. Please can anybody could help me? What am I doing wrong?
import React, { Component } from "react";
import DeleteComponent from "../components/DeleteComponent"
import ReactTable from 'react-table';
import { Link, withRouter } from 'react-router-dom';
import axios from "axios";
import { getJwt } from '../helpers/jwt'
import eye from '../img/eye.png'
import bin from '../img/bin.png'
import writing from '../img/writing.png'
class CustomReactTable extends Component {
constructor(props) {
super(props)
this.state = {
data: [],
showDelete: false,
item: null,
pages: null,
totalItems: null,
loading: false,
state: {},
}
}
fetchData = (state) => {
this.setState({ state: state })
const jwt = getJwt()
if (!jwt) {
this.props.history.push('/login')
}
let config = {
headers: { 'Authorization': `Bearer ${jwt}` },
params: {
page: state.page,
pageSize: state.pageSize,
sorted: state.sorted,
filtered: state.filtered
}
}
this.setState({ loading: true })
axios.get(`http://localhost:3001/api/v1${this.props.location.pathname}`, config)
.then(response => {
console.log(response)
this.setState({
data: response.data.result,
loading: false
})
})
axios.get(`http://localhost:3001/api/v1${this.props.location.pathname}/count-documents`, config)
.then(response => {
this.setState({
totalItems: response.data.result,
pages: Math.ceil(response.data.result / state.pageSize)
})
})
}
loadOptions = () => {
this.props.columns.push({
Header: "",
Cell: (row) => [
// Find a better way to add unique key
<Link to={`${this.props.location.pathname}/${row.original._id}/show`} key={row.original._id} params={{ id: row.original._id }}><button className="btn-xs btn-outline-light"><img style={{ width: '1em' }} src={eye} /></button></Link>,
<Link to={`${this.props.location.pathname}/${row.original._id}/edit`} key={row.original._id + 'a'}><button className="btn-xs btn-outline-light"><img style={{ width: '1em' }} src={writing} /></button></Link>,
<button key={row.original._id + 'b'} className="btn-xs btn-outline-light" onClick={() => { this.onClickDeleteButton(row.original._id) }}><img style={{ width: '1em' }} src={bin} /></button>
]
})
}
loadFunctionalities = () => {
return (
<div className='functionalities-react-table'>
<span className='functionalities-add-item-table'>
<Link to={`${this.props.location.pathname}/add`}><button className="btn-sm btn-outline-success">Add new {this.props.modelName}</button></Link>
</span>
<span className='functionalities-refresh-table'>
<button className="btn-sm btn-outline-dark">Refresh table</button>
</span>
</div>
)
}
onClickDeleteButton = (id) => {
this.setState({ showDelete: true, item: id })
}
onCancelDeleteClick = () => {
this.setState({ showDelete: false })
}
componentDidMount() {
this.loadOptions()
}
reloadData = () => {
this.fetchData(this.state.state)
}
render() {
return (
<div className='main-content'>
{this.state.showDelete && (
<DeleteComponent reloadData={this.reloadData} onCancelDeleteClick={this.onCancelDeleteClick} item={this.state.item} />
)}
<h3>{`${this.props.modelName} (${this.state.totalItems})`}</h3>
{this.loadFunctionalities()}
<ReactTable
data={this.state.data}
columns={this.props.columns}
manual
onFetchData={this.fetchData}
defaultPageSize={10}
pages={this.state.pages}
style={{ fontSize: '0.9em' }}
>
</ReactTable>
<div className="total-records-tag">{this.props.modelName}: {this.state.totalItems}</div>
</div >
)
}
}
export default withRouter(CustomReactTable);

Redux doesn't fetch array after action has dispatched

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

having trouble getting redux components to re-render

I have 2 components (a form for inputing values and a react-table component to display the inputed values) that I am putting on a dashboard. When I enter the values into the form to update the redux store, I can see the changes from redux tools however, the table component doesn't update until I go to a different page and come back. Anyone know what I am doing wrong?
Here is my reducer. I don't believe I am mutating the state here.
Reducer:
const keysReducerDefaultState = [];
export default (state = keysReducerDefaultState, action) => {
switch (action.type) {
case 'ADD_KEYS':
return [
...state,
action.keyPair
];
case 'REMOVE_KEYS':
return state.filter(({ name }) => {
return name !== action.name;
});
default:
return state;
}
}
Component 1
class KeysImportForm extends React.Component {
constructor(props) {
super(props);
this.state = {
// type validation
name: "",
publicKey: "",
privateKey: "",
};
this.typeClick = this.typeClick.bind(this);
}
render() {
const { classes } = this.props;
return (
// a form that takes in 3 fields, name, publickey and privatekey
);
}
}
const mapDispatchToProps = (dispatch) => ({
addKeys: (keyPair) => dispatch(addKeys(keyPair))
});
export default withStyles(validationFormsStyle)(connect(undefined, mapDispatchToProps)(KeysImportForm));
Component 2
class KeysTable extends React.Component {
constructor(props) {
super(props);
const data = props.keys.map((prop, key) => {
return {
id: key,
name: prop.name,
publicKey: prop.publicKey,
privateKey: prop.privateKey,
};
});
this.state = {
data
};
}
render() {
const { classes } = this.props;
return (
<GridContainer>
<GridItem xs={12}>
<Card>
<CardHeader color="primary" icon>
<CardIcon color="primary">
<Assignment />
</CardIcon>
<h4 className={classes.cardIconTitle}>Key Pairs</h4>
</CardHeader>
<CardBody>
<ReactTable
data={this.state.data}
filterable
columns={[
{
Header: "Name",
accessor: "name",
minWidth: 10
},
{
Header: "Public Key",
accessor: "publicKey",
minWidth: 50
},
{
Header: "Private Key",
accessor: "privateKey",
minWidth: 50
},
{
Header: "Action",
accessor: "action",
minWidth: 10,
sortable: false,
filterable: false
}
]}
defaultPageSize={10}
showPaginationTop
showPaginationBottom={false}
className="-striped -highlight"
/>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
}
}
const mapDispathToProps = (dispatch, props) => ({
removeKeys: (id) => dispatch(removeKeys(id))
});
const mapStateToProps = (state) => {
return {
keys: state.keys
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispathToProps)(KeysTable));
Dashboard
class Dashboard extends React.Component {
state = {
value: 0
};
handleChange = (event, value) => {
this.setState({ value });
};
handleChangeIndex = index => {
this.setState({ value: index });
};
render() {
const { classes } = this.props;
return (
<div>
<KeysImportForm/>
<KeysTable/>
</div>
);
}
}
Dashboard.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(dashboardStyle)(Dashboard);
I'm not 100% sure, but it looks like you are having the following error:
In your constructor you do a (unnecessary) copy of you props to your state, which introduces the error and defeats the purpose of Redux:
const data = props.keys.map((prop, key) => {
return {
id: key,
name: prop.name,
publicKey: prop.publicKey,
privateKey: prop.privateKey,
};
});
This causes your data to only update when your constructor is called, which is when your component mounts (a.k.a. you reload your page).
Instead use your props directly as your data. Redux will cause your component to re-render every time the state changes.

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))
}
};

How to pass argument to function in reactjs?

How can I send sport_id form getSport to getEvents to show each sports events?
Can I put getSport function to other component, call and use it in this component?
events json:
[
{
"id": "912653",
"time": "1536471082",
"time_status": "1",
"league": {
"id": "900",
"name": "Hong Kong 2nd Division",
"cc": "hk"
},
"home": {
"id": "13767",
"name": "Yau Tsim Mong",
"image_id": "193606",
"cc": "hk"
},
"away": {
"id": "63770",
"name": "Tuen Mun SA",
"image_id": "56045",
"cc": "hk"
},
"timer": {
"tm": 74,
"ts": 25,
"tt": "1",
"ta": 0
},
"scores": {}
}
]
sports json:
[
{
"id": 8,
"name": "Rugby Union",
"is_active": null,
"slug": "rugby-union"
}
]
Here is my code:
import React, { Component } from "react";
import axios from "axios";
import moment from "moment";
export default class Feutred extends Component {
state = {
sports: [],
events: [],
isLoading: true,
errors: null
};
getSports() {
axios
.get("/api/v1/sports.json")
.then(response =>
response.data.map(sport => ({
id: sport.id,
name: sport.name,
slug: sport.slug
}))
)
.then(sports => {
this.setState({
sports,
isLoading: false
});
})
.catch(error => this.setState({ error, isLoading: false }));
}
getEvents() {
axios
.get("/api/v1/events?sport_id=${sport_id}")
.then(response =>
response.data.map(event => ({
id: event.id,
time: event.time,
league: event.league,
time_status: event.time_status,
homeTeam: event.home,
awayTeam: event.away
}))
)
.then(events => {
this.setState({
events,
isLoading: false
});
})
.catch(error => this.setState({ error, isLoading: false }));
}
componentDidMount() {
this.getSports();
(this.interval = setInterval(
() => this.getEvents({ time: Date.now() }),
12000
));
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { sports, isLoading } = this.state;
return (
<React.Fragment>
{!isLoading ? (
sports.map(sport => {
const { id, name } = sport;
return (
<div key={sport.id}>
<div className="text">
<p className="meta">
<span className="matchinfo">
<span className="block">time</span>
<span className="block">timestatus</span>
</span>
</p>
<h3>
home-team vs aya tream
</h3>
<p className="league">
<a className="watchlive" href="">
<span className="icon" />
<span>Watch live</span>
</a>
<span>{sport.name} - league cc - league name</span>
</p>
</div>
</div>
);
})
) : (
<p>Loading...</p>
)}
</React.Fragment>
);
}
}
Just destructure it - load sports in one component then render some <EventsLoadingComponent /> passing sport id as prop ...
HINT: Use if(isLoading) return <p>Loading...</p> in render before 'main return' - no need to use ternary operator in return JSX.
UPDATE:
render() {
const { sports, isLoading } = this.state;
if(isLoading) return <p>Loading...</p>
return (
<React.Fragment>
{sports.map(sport => <EventsLoadingComponent sport={sport}/>}
</React.Fragment>
);
}
Move getEvents into <EventsLoadingComponent/> - you'll be fething for events related to this.props.sport.id and render them. This way each of them can be separately updated.
Remember to use key in the topmost html element.
UPDATE #2:
can you please give your code comparison with my code ?
Your code - linear, procedural, 'flat template-driven', forcing async to be sync, all-in-one-component ... while html is a (flatten view of) tree structure.
React thinking (generally, not my code only) - more OO, building tree of objects closer related to data and view structure, giving them own responsibility (data handling, view). Easier to read, expand (destructure further details to components - even one-liners), suitable to decorating, easy to manage ... and reuse.
Often object in structure renders only passed children (or nothing) only providing functionality. Available level of complexity is greater, communication within this structure is far easier (and less dependent) than (it could be done) in html.
Something like this:
getEvents({ id }) {
axios
.get(`/api/v1/events?sport_id=${id}`)
...
}
componentDidMount() {
this.getSports()
.then(() => {
return Promise
.all(this.state.sports.map(this.getEvents))
});
...
}
Note:
You need to refine the way you save the data because you need to know which events are for which sport.

Resources