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.
Related
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>
I am trying to edit form. After first rendering I am not getting existing data from database. Because of state is null. If I click on second button first data is appearing. ( Because of state is null. If I click on second button first data is appearing. )
Reducer:
const intialState ={ channel:null}
Please click here for the image
//form Component
import React, { Fragment } from "react";
import Button from "#material-ui/core/Button";
import TextField from "#material-ui/core/TextField";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogTitle from "#material-ui/core/DialogTitle";
const FormChannel = ({ channelData, ouSubmit, open }) => {
let [formData, setFromData] = React.useState({
channel: channelData ? channelData.channel : "",
channelName: channelData ? channelData.channelName : "",
introduction: channelData ? channelData.introduction : "",
language: channelData ? channelData.language : "",
keywords: channelData ? channelData.keywords : ""
});
const { channel, channelName, introduction, language, keywords } = formData;
const handleClose = () => {
ouSubmit(formData);
setFromData({
channel: "",
channelName: "",
introduction: "",
language: "",
keywords: ""
});
};
const handleChange = channel => e => {
setFromData({ ...formData, [channel]: e.target.value });
};
const view = (
<Fragment>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Create Channel</DialogTitle>
<DialogContent>
<TextField
autoFocus
value={channel}
onChange={handleChange("channel")}
id="channel"
label="Name"
type="emachannelil"
margin="normal"
variant="outlined"
fullWidth
/>
<TextField
value={channelName}
onChange={handleChange("channelName")}
id="channelName"
label="Channel Name"
type="text"
margin="normal"
variant="outlined"
fullWidth
/>
<TextField
label="Language"
id="language"
value={language}
onChange={handleChange("language")}
type="text"
margin="normal"
variant="outlined"
fullWidth
/>
<TextField
value={introduction}
onChange={handleChange("introduction")}
id="introduction"
label="Introduction"
type="text"
margin="normal"
variant="outlined"
fullWidth
/>
<TextField
value={keywords}
onChange={handleChange("keywords")}
id="kewords"
label="Keywords"
type="text"
margin="normal"
variant="outlined"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleClose} color="primary" variant="contained">
Create
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
return <Fragment>{view}</Fragment>;
};
export default FormChannel
Edit
import React, { Fragment } from "react";
import { connect } from "react-redux";
import { Button } from "#material-ui/core";
import FormChannel from "./Form";
const EditChannel = ({ channels: { channel }, open, setOpen }) => {
console.log(channel);
return (
<Fragment>
<FormChannel
open={open}
channelData={channel}
ouSubmit={formData => {
setOpen(false);
console.log(formData);
}}
/>
</Fragment>
);
};
const mapStateToProps = (state, props) => {
console.log(state);
return {
channels: state.channels
};
};
export default connect(mapStateToProps)(EditChannel);
card
import React, { Fragment } from "react";
import { makeStyles } from "#material-ui/core/styles";
import { Typography, Button } from "#material-ui/core";
import Avatar from "#material-ui/core/Avatar";
import { Link } from "react-router-dom";
import { connect } from "react-redux";
import { channelFollow, channelFetchById } from "../../../redux/action/channel";
import EditChannel from "../../Channels/List/Edit";
const useStyles = makeStyles(theme => ({
card: {
flexGrow: 1
},
img: {
height: "60%",
overflow: "hidden",
width: "100%"
},
link: {
textDecoration: "none",
color: "black"
},
link1: {
textDecoration: "none",
color: "white"
},
root: {
display: "flex",
"& > *": {
margin: theme.spacing(0.5)
},
justifyContent: "center"
},
button: {
display: "flex",
justifyContent: "center"
},
text: {
fontWeight: "bold",
fontSize: 15
},
text1: {
color: "gray",
fontSize: 15
},
bigAvatar: {
width: 140,
height: 140
},
buttons: {
marginRight: 5
}
}));
const ChannelCard = props => {
const classes = useStyles();
const { channel, channelFetchById } = props;
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const view = (
<div className={classes.card}>
<Link to={`/channels/${channel._id}`} className={classes.link}>
<div className={classes.root}>
<Avatar
alt="Remy Sharp"
src={channel.avatar}
className={classes.bigAvatar}
/>
</div>
<div className={classes.title}>
<Typography
variant="subtitle1"
align="center"
className={classes.text}
>
{channel.channelName}
</Typography>
<Typography variant="body2" align="center">
{channel.introduction}
</Typography>
</div>
</Link>
<Typography variant="body2" align="center" className={classes.text1}>
{channel.follows ? channel.follows.length : 0} followers <br />
Language:{channel.language}
</Typography>
<div className={classes.center}>
<div className={classes.button}>
<div className={classes.buttons}>
<Button
variant="contained"
color="primary"
onClick={() => {
channelFetchById(channel._id);
handleClickOpen();
}}
>
Edit
</Button>
{open ? <EditChannel setOpen={setOpen} open={open} /> : ""}
</div>
<div>
<Button color="primary" variant="contained">
Delete
</Button>
</div>
</div>
<br />
</div>
</div>
);
return <Fragment>{view}</Fragment>;
};
export default connect(null, { channelFollow, channelFetchById })(ChannelCard);
Reducer
import {
CHANNEL_FETCH,
CHANNEL_FETCH_BY_ID,
CHANNEL_UNFOLLOWED,
CHANNEL_EDIT
} from "../action/typeof";
const initialState = {
channels: [],
channel: null,
loading: true
};
export default (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case CHANNEL_FETCH:
return { ...state, channels: payload, loading: false };
case CHANNEL_FETCH_BY_ID:
return {
...state,
channel: payload,
loading: false
};
case CHANNEL_UNFOLLOWED:
return {
...state,
channelUnfollowedUser: payload,
loading: false
};
case CHANNEL_EDIT:
const channelEdit = state.channes.map(single =>
single._id === payload.id ? { single, ...payload.data } : single
);
return {
...state,
channels: channelEdit,
loading: false
};
default:
return state;
}
};
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.
I have a form in react where I'm using a Material UI TextField. I want a value present in the TextField and I can see that it is being set when I use the Inspect option of Google Chrome. In addition to that, I see that the type="hidden". I have changed this to type="text" and to no avail, the value is still not presented. Curious if anyone here has any understanding as to why this would happen. Below is the primary code that is causing the problem:
<TextField
name="Property"
select
fullWidth
margin="normal"
className={clsx(selectInputStyle.margin, selectInputStyle.textField, selectInputStyle.root)}
value={viewProperties[index].name}
onChange={this.handleSelectChange}
>
{propertyKeys.map((key, index) => (
<MenuItem value={key} key={index}>
{key}
</MenuItem>
))}
</TextField>
Here is the full code file just for a complete context of what is going on.
import React, { Component } from 'react';
import { Container, Form, Button, Row, Col, Nav, NavItem, NavLink, Input, FormGroup } from 'reactstrap';
import { connect } from 'react-redux';
import { reduxForm, FieldArray, arrayRemoveAll } from 'redux-form/immutable';
import * as Immutable from 'immutable';
import _ from 'lodash';
import { bindActionCreators } from 'redux';
import BlockUi from 'react-block-ui';
import MaterialButton from '#material-ui/core/Button';
import DeleteIcon from '#material-ui/icons/Delete';
import { makeStyles } from '#material-ui/core/styles';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import MenuItem from '#material-ui/core/MenuItem';
import Select from '#material-ui/core/Select';
import InputMaterial from '#material-ui/core/Input';
import FormControl from '#material-ui/core/FormControl';
import TextField from '#material-ui/core/TextField';
import OutlinedInput from '#material-ui/core/OutlinedInput';
import clsx from 'clsx';
import { AvText, AvSelect } from '../forms/components';
import { showNotification, loadPayerProperties, updatePayerProperties } from '../actions';
class PayerPropertiesEditor extends Component {
constructor(props) {
super(props);
this.uploadRef = React.createRef();
this.state = {
errors: [],
refeshProperties: false,
blocking: false
};
this.showButton = false;
this.divPadding = { padding: '20px' };
this.doSubmit = this.doSubmit.bind(this);
this.handleInvalidSubmit = this.handleInvalidSubmit.bind(this);
this.renderProperties = this.renderProperties.bind(this);
this.handleSelectChange = this.handleSelectChange.bind(this);
this.useStyles = makeStyles(theme => ({
button: {
margin: theme.spacing(1)
},
leftIcon: {
marginRight: theme.spacing(1)
},
rightIcon: {
marginLeft: theme.spacing(1)
},
iconSmall: {
fontSize: 20
},
root: {
display: 'flex',
flexWrap: 'wrap'
},
formControl: {
margin: theme.spacing(1),
minWidth: 120
},
selectEmpty: {
marginTop: theme.spacing(2)
},
container: {
display: 'flex',
flexWrap: 'wrap'
},
input: {
margin: theme.spacing(1)
}
}));
}
componentDidMount() {
this.setState({ view: 'payer' });
}
componentDidUpdate(prevProps) {
const { loadPayerProperties } = this.props;
if (this.state.refeshProperties) {
this.props.arrayRemoveAll('payerPropertiesEditorForm', 'properties');
loadPayerProperties();
this.setState({ refeshProperties: false });
this.setState({ blocking: false });
this.showButton = false;
}
if (!prevProps.properties && this.props.properties) {
this.props.change('properties', Immutable.fromJS(this.props.properties));
}
}
doSubmit(values) {
const { updatePayerProperties } = this.props;
return new Promise(resolve => {
this.setState({
blocking: true
});
updatePayerProperties(values.toJS(), () => {
this.setState({ refeshProperties: true });
});
resolve();
});
}
handleInvalidSubmit() {
this.props.showNotification({
level: 'error',
message: 'Errors were found.'
});
}
handleSelectChange(event) {
console.log(event);
}
renderProperties({ fields }) {
const inputUseStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexWrap: 'wrap'
},
formControl: {
margin: theme.spacing(1),
minWidth: 120
},
selectEmpty: {
marginTop: theme.spacing(2)
},
container: {
display: 'flex',
flexWrap: 'wrap'
},
margin: {
margin: theme.spacing(1)
},
textField: {
flexBasis: 200
},
input: {
margin: theme.spacing(1)
}
}));
const selectInputStyle = inputUseStyles().input;
const useStyles = this.useStyles();
const { formProperties, unsetPropertiesKeys } = this.props;
const viewProperties = formProperties[`${this.state.view}`];
const propertyKeys = unsetPropertiesKeys[`${this.state.view}`];
return (
<div maxWidth="sm" className={selectInputStyle.root}>
{fields.map((property, index) => (
<Row
key={index}
style={{
display: viewProperties[index].action === 'remove' ? 'none' : ''
}}
>
<Col xs={5}>
<TextField
select
fullWidth
margin="normal"
className={clsx(selectInputStyle.margin, selectInputStyle.textField, selectInputStyle.root)}
value={viewProperties[index].name}
onChange={this.handleSelectChange}
>
{propertyKeys.map((key, index) => (
<MenuItem value={key} key={index}>
{key}
</MenuItem>
))}
</TextField>
</Col>
<Col xs={5}>
<AvText
name={`${property}.value`}
onChange={() => {
if (viewProperties[index].action !== 'add') {
this.props.change(`${property}.action`, 'update');
this.showButton = true;
}
}}
/>
</Col>
<Col xs={1}>
<MaterialButton
variant="contained"
className="{classes.button}"
onClick={() => {
fields.remove(index);
if (viewProperties[index].action !== 'add') {
fields.insert(
index,
Immutable.fromJS(Object.assign({}, viewProperties[index], { action: 'remove' }))
);
this.showButton = true;
}
}}
>
Delete
<DeleteIcon className={useStyles.rightIcon} />
</MaterialButton>
</Col>
</Row>
))}
<Row>
<Col xs={12}>
<Button
color="primary"
onClick={() => {
fields.push(
Immutable.fromJS({
owner: this.props.ownerKeys[this.state.view],
action: 'add'
})
);
this.showButton = true;
}}
>
Add Property
</Button>
</Col>
</Row>
<br />
{this.showButton === true && (
<Row>
<Col xs={12}>
<Button color="primary" type="submit">
Submit
</Button>
</Col>
</Row>
)}
</div>
);
}
render() {
const { handleSubmit, properties, payerName, chsId, parentChsId } = this.props;
const formStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexWrap: 'wrap'
},
formControl: {
margin: theme.spacing(1),
minWidth: 120
},
selectEmpty: {
marginTop: theme.spacing(2)
}
}));
const navItems = ['payer', 'clearinghouse', 'parent'].map(key => (
<Tab
textColor="primary"
key={key}
label={
key === 'payer'
? 'PAYER: ' + payerName
: key === 'clearinghouse'
? 'CLEARING HOUSE: ' + chsId
: 'PARENT: ' + parentChsId
}
onClick={() => this.setState({ view: key })}
/>
));
const overrides = this.state.view === 'payer' ? ['clearinghouse'] : this.state.view === 'parent' ? ['parent'] : [];
const readonly = properties
? overrides
.filter(key => key !== this.state.view)
.map(key => properties[key])
.reduce((acc, val) => acc.concat(val), [])
.map((property, idx) => {
return (
<Row key={idx}>
<Col xs={5}>
<FormGroup>
<Input value={property.name} disabled />
</FormGroup>
</Col>
<Col xs={5}>
<FormGroup>
<Input value={property.value} disabled />
</FormGroup>
</Col>
</Row>
);
})
: [];
return (
<BlockUi tag="div" blocking={this.state.blocking} className="my-2">
<Container maxWidth="sm">
<AppBar position="static" color="default">
<Tabs variant="fullWidth" textColor="primary">
{navItems}
</Tabs>
</AppBar>
<FormControl
fullWidth
className="mt-4"
onSubmit={handleSubmit(this.doSubmit)}
ref={form => (this.formRef = form)}
>
{readonly}
<FieldArray
name={`properties.${this.state.view}`}
component={this.renderProperties}
rerenderOnEveryChange
/>
</FormControl>
</Container>
</BlockUi>
);
}
}
const mapStateToProps = state => {
const {
payerPropertiesStore: {
payer: { payerId, payerName, chsId, parentChsId },
properties,
propertyKeys
},
form: {
payerPropertiesEditorForm: {
values: { properties: formProperties }
}
}
} = state.toJS();
const unsetPropertiesKeys = {};
for (const view of ['payer', 'clearinghouse', 'parent']) {
unsetPropertiesKeys[view] = propertyKeys.filter(key => !_.find(formProperties[view], { name: key }));
}
const ownerKeys = { payer: payerId, clearinghouse: chsId, parent: parentChsId };
return { formProperties, properties, ownerKeys, unsetPropertiesKeys, payerId, payerName, chsId, parentChsId };
};
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showNotification,
loadPayerProperties,
updatePayerProperties,
arrayRemoveAll
},
dispatch
);
export default reduxForm({
form: 'payerPropertiesEditorForm',
enableReinitialize: true,
initialValues: Immutable.fromJS({ properties: {} })
})(
connect(
mapStateToProps,
mapDispatchToProps
)(PayerPropertiesEditor)
);
In the TextField you are setting value from viewProperties like,
value={viewProperties[index].name}
You are getting data in viewProperties from formProperties based on this.state.view
const viewProperties = formProperties[`${this.state.view}`];
As you are setting view state in componentDidMount, on intial render view is not set and don't have value so you are not getting any value from formProperties.
You need to have a default state,
this.state = {
errors: [],
refeshProperties: false,
blocking: false,
view: 'payer' // default set
};
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