React updates state when clicked twice - reactjs

Having an issue with react not updating the state right away on the console.log, i have to click twice on the submit button in order for the console.log to show the updated state
i checked this, but i don't think that could be the issue
React: state not updating on first click
Working Demo, check the console out
https://codesandbox.io/s/l499j0p5vm?fontsize=14
Here is what i have
App.js
import React, {Component} from 'react';
import Navbar from './components/Navbar';
import {withStyles} from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Grid from '#material-ui/core/Grid';
import logo from './logo.svg';
import {Typography, Button} from '#material-ui/core';
import Footer from './components/Footer';
import Emoji from './components/Emoji';
import TextField from '#material-ui/core/TextField';
import EmojiPicker from 'emoji-picker-react';
import JSEMOJI from 'emoji-js';
import Icon from '#material-ui/core/Icon';
let jsemoji = new JSEMOJI();
// set the style to emojione (default - apple)
jsemoji.img_set = 'emojione';
// set the storage location for all emojis
jsemoji.img_sets.emojione.path = 'https://cdn.jsdelivr.net/emojione/assets/3.0/png/32/';
// some more settings...
jsemoji.supports_css = false;
jsemoji.allow_native = true;
jsemoji.replace_mode = 'unified'
const styles = theme => ({
shadows: ["none"],
spacing: 8,
root: {
flexGrow: 1,
minHeight: '800px',
width: '100%',
position: 'relative'
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'left',
width: '500px',
color: theme.palette.text.secondary
},
textField: {
width: '400px'
},
myitem: {
margin: '40px'
},
emoji: {
margin: '40px'
},
emojiButton: {
margin: '20px 0px'
},
cancel: {
margin: '20px 0px'
}
});
class App extends Component {
constructor(props) {
super(props);
this.state = {
emoji: '',
text: '',
items: [],
emojiToggle: false
}
}
onChange = (e) => {
e.preventDefault()
this.setState({text: e.target.value});
}
handleClick = (n, e) => {
let emoji = jsemoji.replace_colons(`:${e.name}:`);
this.setState({
text: this.state.text + emoji,
});
// console.log(this.state.items)
}
handleButton = (e) => {
e.preventDefault();
if(!this.state.emojiToggle){
this.setState({emojiToggle: true})
}
else{
this.setState({emojiToggle: false})
}
}
onSubmit = (e) => {
e.preventDefault();
this.setState({
text: this.state.text,
items: [this.state.text]
})
console.log(this.state.items) // have to click twice to see the updated state
}
render() {
const {classes} = this.props;
return (
<div className={classes.root}>
<Navbar/>
<Grid container spacing={12}>
<Grid item sm={6} className={classes.myitem}>
<Paper className={classes.paper}>
<Typography variant="h2" component="h2">
Insert An Emoji
</Typography>
{/* Begin Form */}
<form>
<TextField
id="standard-name"
label="Enter Something"
className={classes.textField}
value={this.state.text}
onChange={this.onChange}
margin="normal"
/>
{this.state.emojiToggle ? (
<div>
<EmojiPicker onEmojiClick={this.handleClick}/>
<Button
className={classes.cancel}
onClick={this.handleButton}
color="danger"
variant="outlined">
Close
</Button>
</div>
)
: (
<div>
<Button onClick={this.handleButton} color="primary" variant="outlined">
Show Emojis
</Button>
<Button onClick={this.onSubmit} style={{ marginLeft: '10px'}} color="primary" variant="outlined">
Submit
</Button>
</div>
)}
{/* End Form */}
</form>
</Paper>
</Grid>
</Grid>
<Footer/>
</div>
);
}
}
export default withStyles(styles)(App);

Console.log() exec before the state finish to update.
Because setState() is an asynchronous function
this.setState({
text: this.state.text,
items: [this.state.text]
}, () => console.log(this.state.items));

The problem is exactly what is mentioned in the post you linked. setState is an asynchronous function and doesn't necessarily set the state of your component before your console.log() is called. If you would like to see your new state after it is updated, you can add a callback function to setState to see what the results of the state update are.
this.setState({
text: this.state.text + emoji,
}, () => console.log(this.state.items));
EDIT
Here is a link to your demo with the console.log giving the correct result:
https://codesandbox.io/s/949xprn3xy

That is the expected behaviour. console.log() is synchronous, so it runs before setState() is finished. If you really want to see the current state, you should change your submit handler to this:
onSubmit = e => {
e.preventDefault();
this.setState(
{
text: this.state.text,
items: [this.state.text]
},
() => console.log(this.state.items)
);
};

Just use async before the function , use await before setState and your issue will be resolved

Related

ReactJS Antd Table Rendering Old Data

Summary: I have a component that contains an Antd Table component. The Antd Table component is created initially based on the original userData that is generated using a fetch request. The data is loaded initially and set using setState(fetchedData) calling render() to display appropriately. This works.
Using the NiceModal library, I present a new modal component and pass in a doCompelte closure that will then setstate again with the updated data. From here, the render() method is called again. I log the updated userData in the render() method before the data is loaded and it shows the data is updated and it confirms that render() is called with the new data. But, the visual display never actually loads on the screen.
At this point, I stopped taking user input from the Modal and tried giving it direct values, as shown in EditModal.jsx. This still doesn't work and I'm unsure of why.
Question: Why is my component render() method called, new data printed correctly, but the React Antd Table is not loading the new data?
Note: I did try creating a local variable inside the render() method that accesses userData at that given time but this also doesn't update the table.
UPDATE 1: You can see that the first name is "Mrs." then it gets hardcode changed to "GPRLEJAF" inside the showEditModal() function which now does NOT show a modal, it just changes the value and sets the state.
You'll see that the re-rendering shows the update in the console but the table is never updated.
UserList.jsx
import React, { useEffect } from 'react';
import NiceModal, { useModal } from '#ebay/nice-modal-react';
import { Button, Table } from 'antd';
import { EditOutlined } from '#ant-design/icons';
import AddCircle from '../../../node_modules/material-jsx-icons/dist/icons/add_circle';
import { useLocation } from 'react-router-dom';
import AddModal from './AddModal'
import EditModal from './EditModal'
import "./Table.css";
import 'bootstrap/dist/css/bootstrap.min.css';
import data from '../../mock';
function search() {
}
function withMyHook(Component) {
return function WrappedComponent(props) {
const myHookValue = useLocation();
return <Component {...props} myHookValue={myHookValue} />;
}
}
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
userData: [],
}
}
self = this;
columns = [
{
title: 'First Name',
dataIndex: 'First_Name',
width: '10%',
align: 'center',
},
{
title: 'Last Name',
dataIndex: 'Last_Name',
width: '10%',
align: 'center',
},
{
title: 'Address',
dataIndex: 'Address',
width: '20%',
align: 'center',
},
{
title: 'Billed',
dataIndex: 'Billed',
width: '5%',
align: 'center',
},
{
title: 'Work Date',
dataIndex: 'Work_Date',
width: '12%',
align: 'center',
},
{
title: 'Last Modified Date',
dataIndex: 'Table_Date',
width: '12%',
align: 'center',
},
{
title: 'Notes',
dataIndex: 'Notes',
width: '32%',
align: 'center',
},
{
title: 'Actions',
width: '5%',
align: 'center',
dataIndex: 'taco',
render: (_, value) => <Button type="link" icon={<EditOutlined />} onClick={() => this.showEditModal(value)}/>
},
];
showEditModal(value) {
/*
var index = this.userData.findIndex(item => item == value);
NiceModal.show(EditModal, { data: value, onEdit: (updatedUser) => {
this.userData[index] = updatedUser; // Update current user with updated data
this.setState([...this.userData]); // Re-Render with new data
}});
*/
this.userData[0].First_Name = "Random Name";
this.setState([this.userData]);
};
iconStyles = {
width: '25px',
fill: '#ffffff',
justifyContent: 'center',
alignItems: 'center',
float: 'left',
marginRight: '5px',
};
componentDidMount() {
console.log("Updating");
const token = this.props.myHookValue.state['token'];
console.log("Token", this.props.myHookValue.state['token']);
let response = fetch('https://x7n8z7pgo5.execute-api.us-east-1.amazonaws.com/dev', {
headers: {
"Authorization": token
}
})
.then(response => response.json())
.then(data => {
console.log("Retrieved Data", data);
this.userData = data["Items"];
console.log("User Data", this.userData);
this.setState([...data["Items"]]); //re-renders data
})
.catch(err => console.log(err));
}
showAddModal = () => {
NiceModal.show(AddModal, { component: this });;
};
render() {
const curData = this.userData;
console.log("Rerendering", curData);
return <div>
<div className="container-xl">
<div className="table-responsive">
<div className="table-wrapper">
<div className="table-title">
<div className="row">
<div className="col-sm-6">
<h2>Manage Clients</h2>
</div>
<div className="col-sm-6">
{<Button className="btn btn-success" type="primary" onClick={this.showAddModal}>
<AddCircle style={this.iconStyles}/> Add New Client
</Button>}
</div>
</div>
</div>
<input id="search" type="text" placeholder="Search..." onKeyUp={search}/>
<Table
size="small"
rowKey="id"
pagination={false}
columns={this.columns}
dataSource={curData}
style={{ marginTop: '20px' }}
/>
</div>
</div>
<div>
</div>
</div>
</div>
}
}
export default withMyHook(UserList);
EditModal.jsx
import { Modal } from 'antd';
import NiceModal, { useModal } from '#ebay/nice-modal-react';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
import { FilePicker, TextInput } from 'evergreen-ui';
import TextArea from 'antd/lib/input/TextArea';
import TextField from '#material-ui/core/TextField';
import { Button } from 'antd';
import React, { useState } from 'react';
function textInputStyles(height='40px') {
return {
width: '350px',
fontSize: '20px',
height: height,
}
}
export default NiceModal.create(({ data, onEdit }) => {
const user = data;
const [firstName, setFirstName] = useState(user.First_Name);
const [lastName, setLastName] = useState(user.Last_Name);
const [address, setAddress] = useState(user.Address);
const [billed, setBilled] = useState(user.Billed);
const [workDate, setWorkDate] = useState(user.Work_Date);
const [notes, setNotes] = useState(user.Notes);
const modal = useModal();
return (
<Dialog
open={modal.visible}
onClose={() => {
modal.hide();
}}
TransitionProps={{
onExited: () => modal.remove(),
}}
>
<DialogTitle id="edit">{"Edit Client"}</DialogTitle><hr></hr>
<DialogContent>
<DialogContentText id="edit_first_name">
First Name
</DialogContentText>
<DialogContentText>
<TextInput
style={textInputStyles()}
defaultValue={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</DialogContentText>
<DialogContentText id="edit_last_name">
Last Name
</DialogContentText>
<DialogContentText>
<TextInput
style={textInputStyles()}
defaultValue={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</DialogContentText>
<DialogContentText id="edit_address">
Address
</DialogContentText>
<DialogContentText>
<TextInput
style={textInputStyles()}
defaultValue={address}
onChange={(e) => setAddress(e)}
/>
</DialogContentText>
<DialogContentText id="edit_billed">
Billed
</DialogContentText>
<DialogContentText>
<TextInput
style={textInputStyles()}
defaultValue={billed}
/>
</DialogContentText>
<DialogContentText id="edit_work_date">
Work Date
</DialogContentText>
<DialogContentText>
<TextField
id="date"
label=""
type="date"
InputLabelProps={{
shrink: true,
}}
defaultValue={new Date(workDate).toISOString().substring(0, 10)}
/>
</DialogContentText>
<DialogContentText id="edit_last_modified_date">
Today's Date
</DialogContentText>
<DialogContentText>
<TextInput
style={textInputStyles()}
value={new Date().toDateString().split(' ').slice(1).join(' ')}
editable={false}
/>
</DialogContentText>
<DialogContentText id="edit_notes">
Notes
</DialogContentText>
<DialogContentText>
<TextArea
style={textInputStyles(175)}
editable={false}
defaultValue={notes}
/>
</DialogContentText>
<DialogContentText>
Invoice
</DialogContentText>
<DialogContentText>
<FilePicker>
onClick={}
</FilePicker>
</DialogContentText>
</DialogContent>
<DialogActions style={{backgroundColor:'#D3D3D3', justifyContent: 'space-between'}}>
<Button onClick={() => modal.hide()} className="btn" style={{backgroundColor:'red', width: '25%'}}>
Cancel
</Button>
<Button onClick={() => {
modal.hide();
const updatedUser = {
'Address': 'asd',
'First_Name': 'asd',
'Billed': '123',
'Index': user.Index,
'Last_Name': 'fadsad',
'Notes': 'sdad',
'Table_Date': new Date(workDate).toISOString().substring(0, 10),
'Work_Date': workDate
};
onEdit(updatedUser);
}} className="btn" style={{backgroundColor: '#47b300', color:'white', width: '25%', float:'right'}}>
Add
</Button>
</DialogActions>
</Dialog>
);
});
Maybe here in componentDidMount and ShowEditModal (but we don't the server response format) in UserList component
componentDidMount() {
console.log("Updating");
const token = this.props.myHookValue.state['token'];
console.log("Token", this.props.myHookValue.state['token']);
let response = fetch('https://x7n8z7pgo5.execute-api.us-east-1.amazonaws.com/dev', {
headers: {
"Authorization": token
}
})
.then(response => response.json())
.then(data => {
console.log("Retrieved Data", data);
this.userData = data["Items"];
console.log("User Data", this.userData);
//this.setState([...data["Items"]]); //re-renders data
this.setState({userData: data["Items"]}); //re-renders data
//or
this.setState({...data["Items"]});
})
.catch(err => console.log(err));
}
showEditModal(value) {
var index = this.userData.findIndex(item => item == value);
NiceModal.show(EditModal, { data: value, onEdit: (updatedUser) => {
this.userData[index] = updatedUser; // Update current user with updated data
//this.setState([...this.userData]); // Re-Render with new data
this.setState({userData: this.userData});
//or
this.setState({...this.userData});
}});
};
Antd Tables require a copy of the data to be able to dynamically update the table.
render() {
if (typeof this.userData !== 'undefined') {
var copy = JSON.parse(JSON.stringify(this.userData));
console.log(copy);
return <div>
<div className="container-xl">
<div className="table-responsive">
<div className="table-wrapper">
<div className="table-title">
<div className="row">
<div className="col-sm-6">
<h2>Manage Clients</h2>
</div>
<div className="col-sm-6">
{<Button className="btn btn-success" type="primary" onClick={this.showAddModal}>
<AddCircle style={this.iconStyles}/> Add New Client
</Button>}
</div>
</div>
</div>
<input id="search" type="text" placeholder="Search..." onKeyUp={search}/>
<Table
size="small"
rowKey="id"
pagination={false}
columns={this.columns}
dataSource={copy}
style={{ marginTop: '20px' }}
/>
</div>
</div>
<div>
</div>
</div>
</div>
}
return <div>
Loading
</div>
}

How to use React Redux state for conditionally showing and Disabling a Button

I have made a shopping cart with React-Redux. Everything works as it should, but I am struggling with 1 particular thing. That is changing the "Add to Cart" button when an item is added to the cart. I want this button to be either disabled, or change this button in a "Delete Item" button.
How can I change this after an item is added?
My Reducer ("ADD_TO_CART" and "REMOVE_ITEM"):
import bg6 from '../../images/bg6.svg'
import bg7 from '../../images/bg7.svg'
import bg8 from '../../images/bg8.svg'
const initState = {
items: [
{ id: 1, title: 'Landing Page', desc: "Template", price: 10, img: bg6 },
{ id: 2, title: 'Adidas', desc: "Lore", price: 10, img: bg7 },
{ id: 3, title: 'Vans', desc: "Lorem ip", price: 10, img: bg8 },
],
addedItems: [],
total: 0,
}
const cartReducer = (state = initState, action) => {
if (action.type === "ADD_TO_CART") {
let addedItem = state.items.find(item => item.id === action.id)
//check if the action id exists in the addedItems
let existed_item = state.addedItems.find(item => action.id === item.id)
if (existed_item) {
addedItem.quantity = 1
return {
...state,
total: state.total + addedItem.price
}
}
else {
let addedItem = state.items.find(item => item.id === action.id)
addedItem.quantity = 1;
//calculating the total
let newTotal = state.total + addedItem.price
return {
...state,
addedItems: [...state.addedItems, addedItem],
total: newTotal
}
}
}
if (action.type === "REMOVE_ITEM") {
let itemToRemove = state.addedItems.find(item => action.id === item.id)
let new_items = state.addedItems.filter(item => action.id !== item.id)
//calculating the total
let newTotal = state.total - (itemToRemove.price * itemToRemove.quantity)
return {
...state,
addedItems: new_items,
total: newTotal
}
}
else {
return state
}
}
export default cartReducer
My Home Component where i want to change the button:
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { addToCart, removeItem } from '../../../store/actions/cartActions'
import { withStyles } from '#material-ui/core/styles';
import { Link, Redirect, withRouter, generatePath } from 'react-router-dom'
import CardActions from '#material-ui/core/CardActions';
import CardContent from '#material-ui/core/CardContent';
import CardMedia from '#material-ui/core/CardMedia';
import CssBaseline from '#material-ui/core/CssBaseline';
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
import Container from '#material-ui/core/Container';
import Button from '#material-ui/core/Button';
import Card from '#material-ui/core/Card';
import Appbar from '../../home/Appbar'
import CheckIcon from '#material-ui/icons/Check';
import DeleteIcon from '#material-ui/icons/Delete';
const styles = theme => ({
main: {
backgroundColor: '#121212',
maxHeight: 'auto',
width: '100%',
padding: '0',
margin: '0',
},
cardGrid: {
height: "auto",
paddingTop: theme.spacing(8),
paddingBottom: theme.spacing(8),
},
card: {
backgroundColor: "#272727",
height: '100%',
display: 'flex',
flexDirection: 'column',
},
cardMedia: {
paddingTop: '56.25%', // 16:9
},
cardContent: {
flexGrow: 1,
},
});
class Home extends Component {
render() {
const { classes, items, addedItems, addedItemID } = this.props
const { } = this.state
console.log(addedItemID)
let allItems =
items.map(item => {
return (
<Grid item key={item.id} xs={12} sm={6} md={4}>
<Card className={classes.card}>
<CardMedia
className={classes.cardMedia}
image={item.img}
alt={item.title}
title={item.title}
/>
<CardContent className={classes.cardContent}>
<Typography gutterBottom variant="h5" component="h2">
{item.title} {item.price}
</Typography>
<Typography>
{item.desc}
</Typography>
</CardContent>
<CardActions className={classes.cardActions}>
<Button
onClick={() => { this.props.addToCart(item.id) }}
style={{ color: "#d84", border: "1px solid #d84" }}
size="small"
color="primary"
disables={} //// HERE I NEED TO DISABLE BUTTON WHEN ITEM IS ADDED ////
>
Add to Cart //// THIS NEEDS TO BE DELETE ITEM ////
</Button>
</CardActions>
</Card>
</Grid>
)
})
return (
<React.Fragment>
<CssBaseline />
<Appbar />
<main className={classes.main}>
<Container className={classes.cardGrid} maxWidth="lg">
<Grid container spacing={4}>
{allItems}
</Grid>
</Container>
</main>
</React.Fragment>
)
}
}
const mapStateToProps = (state) => {
return {
items: state.cartReducer.items,
addedItems: state.cartReducer.addedItems,
addedItemID: state.cartReducer.addedItemID,
}
}
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (id) => { dispatch(addToCart(id)) },
removeItem: (id) => { dispatch(removeItem(id)) },
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Home)));
I can't seem to wrap my around around it. I hope you guys can help.
Thanks in advance.
You can use the this.props.addedItems and check in it has the item you are currently trying to add, add a method and use it for Disabled State.
const isItemExist = (itemToFindId) => {
return this.props.addedItems.findIndex(item => item.id === itemToFindId) === -1;
}
<Button
onClick={() => { this.props.addToCart(item.id) }}
style={{ color: "#d84", border: "1px solid #d84" }}
size="small"
color="primary"
disables={()=>isItemExist(item.id)} //// HERE I NEED TO DISABLE BUTTON WHEN ITEM IS ADDED ////
>
Add to Cart //// THIS NEEDS TO BE DELETE ITEM ////
</Button>
you can also use the same logic and create another button use
{isItemExist(item.id) ? <RemoveCartButton/> : <AddToCartButton/>}
let me know if you have any more issues.
I'll try to solve them
use Conditional Rendering in JSX:
first make additional state for handling the state of the button.
i call this for example 'is_add_to_cart' and every time the button is clicked in both reducer sections we should change the value of is_add_to_cart to the opposite value
i mean if you initially set : is_add_to_cart : true in the init state then you should revert its value and it is something like this : is_add_to_cart = !is_add_to_cart in your new state.
in return part of the jsx code we should do conditional rendering so here how it looks :
is_add_to_cart ? <Button . . . >Add To Cart</Button>
: <Button . . . >Delete Item</Button>
you can do this by checking addedItems length or total's value.
first of all, include total in your mapStateToProps function:
const mapStateToProps = (state) => {
return {
items: state.cartReducer.items,
addedItems: state.cartReducer.addedItems,
addedItemID: state.cartReducer.addedItemID,
total: state.cartReducer.total
}
}
then extract it from component's props:
const { classes, items, addedItems, addedItemID, total } = this.props;
now your button can be something like this:
<Button
onClick={() => { this.props.addToCart(item.id) }}
style={{ color: "#d84", border: "1px solid #d84" }}
size="small"
color="primary"
disabled={total > 0 || addedItems.length > 0 ? true : false}
>
{total > 0 || addedItems.length > 0 ? "Delete Item" : "Add to Cart" }
</Button>

how to unit test component that relies on redux props

I have a component called Comment List, comment list is getting all comments that is apart of this.props.posts Which comes from the dashboard container.
The <CommentList/> component is being called in the <PostList/> component, from there it is passing {post.Comments} as props to
My question is how would i properly test <CommentList/> Component, considering it relies on redux selector, api call etc.
Here is the flow.
1) dashboard container holds the props for this.props.post
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
addContent,
addTitle,
createPostInit,
deleteCommentInit,
initCommentUpdates,
deletePostInit,
dislikePostInit,
getPostsInit,
likePostInit,
notificationInit,
postCommentInit,
} from "../actions/postActions";
import Dashboard from "../components/dashboard/dashboard";
import { getBodyError, getIsNotified, getNotification, getPopPosts, getPosts, getTitleError, getUser, postContent, title } from "./../selectors/selectors";
const mapDispatchToProps = (dispatch: any) => ({
getPostsInit: () => dispatch(getPostsInit()),
initCommentUpdates: () => dispatch(initCommentUpdates()),
notificationInit: () => dispatch(notificationInit()),
likePost: (id: number) => dispatch(likePostInit(id)),
addTitle: (data: string) => dispatch(addTitle(data)),
addContent: (data: string) => dispatch(addContent(data)),
postCommentInit: (commentData: object) => dispatch(postCommentInit(commentData)),
dislikePost: (id: number) => dispatch(dislikePostInit(id)),
deletePostInit: (id: number, userId: number) => dispatch(deletePostInit(id, userId)),
deleteComment: (id: number, postId: number, userId: number) => dispatch(deleteCommentInit(id, postId, userId)),
createPostInit: (postData: object) => dispatch(createPostInit(postData)),
});
const mapStateToProps = createStructuredSelector({
posts: getPosts(),
popPosts: getPopPosts(),
user: getUser(),
isNotified: getIsNotified(),
titleError: getTitleError(),
bodyError: getBodyError(),
title: title(),
postContent: postContent(),
notification: getNotification(),
});
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
this.props.posts gets mapped, and post.Commments gets passed to commentList prop
postList.tsx
{post.Comments.length > 0 ? (
<Fragment>
<Typography style={{ padding: "10px 0px", margin: "20px 0px" }}>Commments</Typography>
<CommentList user={currentUser} deleteComment={props.deleteComment} userId={post.userId} postId={post.id} comments={post.Comments} />
{/* if show more hide show more button and show show less comments button */}
</Fragment>
) : (
<Grid item={true} sm={12} lg={12} style={{ padding: "30px 0px" }}>
<Typography>No Commments Yet</Typography>
</Grid>
)}
CommentList.tsx
import Button from "#material-ui/core/Button";
import Divider from "#material-ui/core/Divider";
import Grid from "#material-ui/core/Grid";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import Typography from "#material-ui/core/Typography";
import OurListItem from "../../common/OurListItem";
import DeleteOutlineOutlinedIcon from "#material-ui/icons/DeleteOutlineOutlined";
import moment from "moment";
import React, { Fragment, useState } from "react";
export default function CommentList(props: any) {
const [showMore, setShowMore] = useState<Number>(3);
const [showLessFlag, setShowLessFlag] = useState<Boolean>(false);
const showComments = (e) => {
e.preventDefault();
setShowMore(12);
setShowLessFlag(true);
};
const showLessComments = (e) => {
e.preventDefault();
setShowMore(3);
setShowLessFlag(false);
};
return (
<Grid>
{props.comments.slice(0, showMore).map((comment, i) => (
<div key={i}>
<List style={{ paddingBottom: "20px" }}>
<OurListItem>
<Typography color="primary" align="left">
{comment.comment_body}
</Typography>
{comment.gifUrl && (
<div style={{ display: "block" }}>
<img width="100%" height="300px" src={`${comment.gifUrl}`} />
</div>
)}
</OurListItem>
{props.user && props.user.user && comment.userId === props.user.user.id ? (
<Typography style={{ display: "inline-block", float: "right" }} align="right">
<span style={{ cursor: "pointer" }} onClick={() => props.deleteComment(comment.id, props.postId, comment.userId)}>
<DeleteOutlineOutlinedIcon style={{ margin: "-5px 0px" }} color="primary" /> <span>Delete</span>
</span>
</Typography>
) : null}
<Typography style={{ padding: "0px 0px" }} variant="caption" align="left">
{comment.author.username}
</Typography>
<Typography style={{ fontSize: "12px" }} variant="body1" align="left">
{moment(comment.createdAt).calendar()}
</Typography>
<Divider variant="fullWidth" component="li" />
</List>
</div>
))}
<Fragment>
{props.comments.length > 3 && showLessFlag === false ? (
<Button onClick={(e) => showComments(e)} variant="outlined" component="span" color="primary">
Show More Comments
</Button>
) : (
<Fragment>
{props.comments.length > 3 && (
<Button onClick={(e) => showLessComments(e)} variant="outlined" component="span" color="primary">
Show Less Comments
</Button>
)}
</Fragment>
)}
</Fragment>
</Grid>
);
}
How would i correctly test this component with redux, considering im not getting it from redux, here is my approach,
CommentList.test.tsx
import React from "react";
import CommentList from "./CommentList";
import Grid from "#material-ui/core/Grid";
import { createShallow } from "#material-ui/core/test-utils";
import toJson from "enzyme-to-json";
const props = {
comments: [
{
userId: 1,
id: 1,
comment_body: "delectus aut autem",
author: {
username: "Bill",
},
},
{
userId: 2,
id: 2,
comment_body: "delectus aut autem",
author: {
username: "Bill",
},
},
{
userId: 3,
id: 3,
comment_body: "delectus aut autem",
author: {
username: "Bill",
},
},
],
};
describe("Should render <CommentList/>", () => {
let wrapper;
let shallow;
beforeEach(() => {
shallow = createShallow();
wrapper = shallow(<CommentList {...props} />);
});
it("should render <CommentList/>", () => {
expect(wrapper.find(Grid)).toHaveLength(1);
});
it("should snap <CommentList/> component", () => {
// expect(toJson(wrapper)).toMatchSnapshot();
});
});
My question is how would i properly test Component, considering it relies on redux selector, api call etc.
I don't see this being true anywhere. Component doesn't rely nor know about Redux, it's only getting props (which you can mock in your tests).
How would i correctly test this component with redux, considering I'm not getting it from redux, here is my approach
You don't - that's testing implementation details. You can test the reducers themselves, but the best way to test this is not to tie them together.
The way I see it, you're trying to test your reducers using this component, not necessarily the component itself as to test the component itself works just fine.
Or to test it all together, you should look at E2E tests.

Material-UI: ListItem onClick not called

I am working with React and Material-UI. I am trying to display a list and call a function when one of the items of the list is clicked.
The code below is a simplified version of my code, but it should contain all the lines concerning the issue.
The onClick function is apparently never called. What am I missing?
App.js:
import React, { Component } from 'react';
import './App.css';
import Grid from '#material-ui/core/Grid';
import Map from './Map.js';
class App extends Component {
render() {
return (
<div >
<Grid container spacing={24}>
<Grid item xs={3}>
Column text
</Grid>
<Grid item xs={9}>
<Map/>
</Grid>
</Grid>
</div>
);
}
}
export default App;
Map.js:
import React from 'react';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import ListItemText from '#material-ui/core/ListItemText';
class Map extends React.Component {
constructor(props) {
super(props);
this.state = {
sensorsList: {
isLoaded: false,
data: null,
},
};
}
componentDidMount() {
// retrieve list of sensors
fetch("https://api-of-things.plenar.io/api/sensors?size=10000")
.then(res => res.json())
.then(
(result) => {
this.setState({
sensorsList: {
isLoaded: true,
data: Object.keys(result.data).map(key => result.data[key].path),
}
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
error
});
}
)
}
selectSensor(name) {
console.log(name);
}
_renderSensorsList() {
const {sensorsList} = this.state;
return sensorsList.isLoaded && (
<div style={{
position: 'absolute',
zIndex: 1,
pointerEvents: 'none',
right: 10,
top: 10,
backgroundColor: 'white',
}}>
<List component="nav">
{ sensorsList.data.map(key =>
<ListItem button key={key} onClick={() => this.selectSensor("ciao")} >
<ListItemText primary={key} />
</ListItem>
)}
</List>
</div>
);
}
render() {
const {sensorsList, selectedSensor} = this.state;
return (
<div>
{ this._renderSensorsList() }
</div>
);
}
}
export default Map;
The problem is that in your div's style you are setting pointerEvents: 'none', preventing the click to reach the DOM event listener, remove it and it will work.
<div style={{
position: 'absolute',
zIndex: 1,
pointerEvents: 'none', //<---- remove this line
right: 10,
top: 10,
backgroundColor: 'white',
}}>
try replacing this
_renderSensorsList() {
with arrow function like this
_renderSensorsList = () => {

React is not pushing items in array

I'm trying to contain my items within one array, however it keeps creating a new array for each item that is pushed in the array. Which makes it impossible to loop through items within an array
And of course I referred this and I have this part right
Lists and Keys
Working Demo
https://codesandbox.io/embed/0xv3104ll0
App.js
import React, {Component} from 'react';
import Navbar from './components/Navbar';
import {withStyles} from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Grid from '#material-ui/core/Grid';
import logo from './logo.svg';
import {Typography, Button} from '#material-ui/core';
import Footer from './components/Footer';
import Emoji from './components/Emoji';
import TextField from '#material-ui/core/TextField';
import EmojiPicker from 'emoji-picker-react';
import JSEMOJI from 'emoji-js';
import Icon from '#material-ui/core/Icon';
let jsemoji = new JSEMOJI();
// set the style to emojione (default - apple)
jsemoji.img_set = 'emojione';
// set the storage location for all emojis
jsemoji.img_sets.emojione.path = 'https://cdn.jsdelivr.net/emojione/assets/3.0/png/32/';
// some more settings...
jsemoji.supports_css = false;
jsemoji.allow_native = true;
jsemoji.replace_mode = 'unified'
const styles = theme => ({
shadows: ["none"],
spacing: 8,
root: {
flexGrow: 1,
minHeight: '800px',
width: '100%',
position: 'relative'
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'left',
width: '500px',
color: theme.palette.text.secondary
},
textField: {
width: '400px'
},
myitem: {
margin: '40px'
},
emoji: {
margin: '40px'
},
emojiButton: {
margin: '20px 0px'
},
myitemList:{
margin:'20px 0px'
},
notFound: {
margin:'20px 0px'
},
cancel: {
margin: '20px 0px'
}
});
class App extends Component {
constructor(props) {
super(props);
this.state = {
emoji: '',
text: '',
items: [],
emojiToggle: false
}
}
onChange = (e) => {
e.preventDefault()
this.setState({text: e.target.value});
}
handleClick = (n, e) => {
let emoji = jsemoji.replace_colons(`:${e.name}:`);
this.setState({
text: this.state.text + emoji,
});
// console.log(this.state.items)
}
handleButton = (e) => {
e.preventDefault();
if(!this.state.emojiToggle){
this.setState({emojiToggle: true})
}
else{
this.setState({emojiToggle: false})
}
}
onSubmit = () => {
let myArray = []
myArray.push(this.state.text)
this.setState({
text: this.state.text,
items: [...myArray]
}, () => console.log(this.state.items));
}
render() {
const {classes} = this.props;
return (
<div className={classes.root}>
<Navbar/>
<Grid container spacing={12}>
<Grid item sm={6} className={classes.myitem}>
<Paper className={classes.paper}>
<Typography variant="h2" component="h2">
Insert An Emoji
</Typography>
{/* Begin Form */}
<form>
<TextField
id="standard-name"
label="Enter Something"
className={classes.textField}
value={this.state.text}
onChange={this.onChange}
margin="normal"
/>
{this.state.emojiToggle ? (
<div>
<EmojiPicker onEmojiClick={this.handleClick}/>
<Button
className={classes.cancel}
onClick={this.handleButton}
color="danger"
variant="outlined">
Close
</Button>
</div>
)
: (
<div>
<Button onClick={this.handleButton} color="primary" variant="outlined">
Show Emojis
</Button>
<Button onClick={this.onSubmit} style={{ marginLeft: '10px'}} color="primary" variant="outlined">
Submit
</Button>
</div>
)}
{/* End Form */}
</form>
</Paper>
</Grid>
<Grid item sm={4} className={classes.myitem}>
<Typography variant="h2" component="h2">
Output
</Typography>
{this.state.items.length > 0 ? (
this.state.items.map( (item, i) => (
<div key={i}>
<Grid item sm={8} className={classes.myitemList}>
<Paper >
<Typography>
{item}
</Typography>
</Paper>
</Grid>
</div>
))
) : (
<div>
<Grid item sm={6} className={classes.notFound}>
<Typography>
No Items
</Typography>
</Grid>
</div>
)}
</Grid>
</Grid>
<Footer/>
</div>
);
}
}
export default withStyles(styles)(App);
Replace your onSubmit Function from this code.
onSubmit = e => {
e.preventDefault();
this.setState(
{
text: this.state.text,
items: [...this.state.items, this.state.text]
},
() => console.log(this.state.items)
);
};
change onSubmit method with this.
onSubmit = e => {
e.preventDefault();
this.setState(
{
text: this.state.text,
items: [this.state.text]
},
() => console.log(this.state.items)
);
};
look into your working demo link onSubmit function it's correct one.
Since you want to list all the emoji items, change your OnSubmit to following:
onSubmit = () => {
const {
state: { text, items = [] }
} = this;
const itemList = [...items];
itemList.push(text);
this.setState(
{
text: text,
items: itemList
},
() => console.log(this.state.items)
);
};
this will update items array and not create new one.

Resources