Change state of buttons - reactjs

Conditional rendering buttons doesn't work
I tried to change the state of the buttons and add instead another component but none got worked
/// This is the button component
const AddToList: React.FC<IAddToListProps> = (props) => {
let [showBtn, setShowBtn] = useState(true);
const classes = useStyles(props);
let addToList = () => {
fetch(`http://127.0.0.1:3000/${props.action}/${props.id}`, {method:
'post'})
.then((response) => {
console.log(response);
});
}
return (
<div>
{
showBtn ?
<Button
onClick={addToList}
variant="contained"
color="primary"
className={classes.button}>
{props.label}
</Button>
: null
}
</div>
);
//This is the movieCard component
export default function MovieCard() {
const [movieTitle, setMovieTitle] = useState('lorem ipsum');
const [year, setYear] = useState('1999');
const classes = useStyles();
return (
<Card className={classes.card}>
<CardActionArea>
<CardMedia
className={classes.media}
image='#'
title="anotherTitle"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{movieTitle}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{year}
</Typography>
<AddToList
id={10}
label={'Add To Watch'}
action={'towatch'}
/>
<AddToList
id={10}
label={'Add To Seen'}
action={'watched'} />
</CardContent>
</CardActionArea>
<CardActions>
</CardActions>
</Card>
);
Expected results:
When I click "Add to Watch" button, "Add to Seen" must be removed and "Add to Watch" must be transformed in "Remove from watch list"

I would split up the code between logic and presentation. As you already reused AddToList it's just a visual component and should not contain any logic.
So I would move all logic to one component and then use the state to render the correct presentation:
const AddToList: React.FC<IAddToListProps> = props => {
const classes = useStyles(props);
return (
<div>
<Button
onClick={props.onClick}
variant="contained"
color="primary"
className={classes.button}
>
{props.label}
</Button>
</div>
);
};
With this you can provide any function which is called when clicking the button and you have a multipurpose component for resuse anywhere else. So a more generic name may be useful.
export default function MovieCard() {
const [movieTitle, setMovieTitle] = useState("lorem ipsum");
const [year, setYear] = useState("1999");
const [watched, setWatched] = useState(false);
const classes = useStyles();
const addToList = (action, id) => {
fetch(`http://127.0.0.1:3000/${action}/${id}`, {
method: "post"
}).then(response => {
console.log(response);
});
};
return (
<Card className={classes.card}>
<CardActionArea>
<CardMedia className={classes.media} image="#" title="anotherTitle" />
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{movieTitle}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{year}
</Typography>
{!watched && (
<AddToList
label={"Add To Watch"}
onClick={() => { addToList("towatch", 10); setWatched(true)} }
/>
)}
{watched && (
<AddToList
label={"Add To Seen"}
onClick={() => addToList("watched", 10)}
/>
)}
</CardContent>
</CardActionArea>
<CardActions />
</Card>
);
}
As you can see the MovieCard is now handling the whole logic of calling backend functions and also cares about showing the correct button. With this basic idea you can advance even more with loading the correct watched state and not starting with false or any other thing.

Related

Warning: Each child in a list should have a unique "key" prop; Check the render method of `Post`

when I click by tag, it works fine, but when I click creators, it just return "No posts".
I check the console, and there's an error says the key in list should be unique. Why this happens? Since searchbytag works, does this mean the render method of 'Post' is fine?
updated code
import axios from 'axios';
const API = axios.create({ baseURL: 'http://localhost:5000' });
API.interceptors.request.use((req) => {
if (localStorage.getItem('profile')) {
req.headers.Authorization = `Bearer
${JSON.parse(localStorage.getItem('profile')).token}`;
}
return req;
});
export const createPost = (newPost) => API.post('/posts', newPost);
const Post = ({ post, setCurrentId }) => {
const user = JSON.parse(localStorage.getItem('profile'));
const [likes, setLikes] = useState(post?.likes);
const dispatch = useDispatch();
const history = useHistory();
const classes = useStyles();
const userId = user?.result?._id;
const hasLikedPost = post.likes.find((like) => like === userId);
const handleLike = async () => {
dispatch(likePost(post._id));
if (hasLikedPost) {
setLikes(post.likes.filter((id) => id !== userId));
} else {
setLikes([...post.likes, userId]);
}
};
const Likes = () => {
if (likes.length > 0) {
return likes.find((like) => like === userId)
? (
<><ThumbUpAltIcon fontSize="small" /> {likes.length > 2 ? `You and ${likes.length - 1} others` : `${likes.length} like${likes.length > 1 ? 's' : ''}`}</>
) : (
<><ThumbUpAltOutlined fontSize="small" /> {likes.length} {likes.length === 1 ? 'Like' : 'Likes'}</>
);
}
return <><ThumbUpAltOutlined fontSize="small" /> Like</>;
};
const openPost = (e) => {
// dispatch(getPost(post._id, history));
history.push(`/posts/${post._id}`);
};
return (
<Card className={classes.card} raised elevation={6}>
<ButtonBase
component="span"
name="test"
className={classes.cardAction}
onClick={openPost}
>
<CardMedia className={classes.media} image={post.selectedFile || 'https://user-images.githubusercontent.com/194400/49531010-48dad180-f8b1-11e8-8d89-1e61320e1d82.png'} title={post.title} />
<div className={classes.overlay}>
<Typography variant="h6">{post.name}</Typography>
<Typography variant="body2">{moment(post.createdAt).fromNow()}</Typography>
</div>
{(user?.result?._id === post?.creator) && (
<div className={classes.overlay2} name="edit">
<Button
onClick={(e) => {
e.stopPropagation();
setCurrentId(post._id);
}}
style={{ color: 'white' }}
size="small"
>
<MoreHorizIcon fontSize="default" />
</Button>
</div>
)}
<div className={classes.details}>
<Typography variant="body2" color="textSecondary" component="h2">{post.tags.map((tag) => `#${tag} `)}</Typography>
</div>
<Typography className={classes.title} gutterBottom variant="h5" component="h2">{post.title}</Typography>
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">{post.message.split(' ').splice(0, 20).join(' ')}...</Typography>
</CardContent>
</ButtonBase>
<CardActions className={classes.cardActions}>
<Button size="small" color="primary" disabled={!user?.result} onClick={handleLike}>
<Likes />
</Button>
{(user?.result?._id === post?.creator) && (
<Button size="small" color="secondary" onClick={() => dispatch(deletePost(post._id))}>
<DeleteIcon fontSize="small" /> Delete
</Button>
)}
</CardActions>
</Card>
);
};
export default Post;
Here's my code
const CreatorOrTag = () => {
const { name } = useParams();
const dispatch = useDispatch();
const { posts, isLoading } = useSelector((state) => state.posts);
const location = useLocation();
useEffect(() => {
if (location.pathname.startsWith('/creators')) {
dispatch(getPostsByCreator({ name: name }));
}
else {
dispatch(getPostsBySearch({ tags: name }));
}
}, []);
if (!posts.length && !isLoading) return 'No posts';
return (
<div>
<Typography variant="h2">{name}</Typography>
<Divider style={{ margin: '20px 0 50px 0' }} />
{isLoading ? <CircularProgress /> : (
<Grid container alignItems="stretch" spacing={3}>
{posts?.map((post) => (
<Grid key={post._id} item xs={12} sm={12} md={6} lg={3}>
<Post post={post} />
</Grid>
))}
</Grid>
)}
</div>
);
};
export default CreatorOrTag;
What this warning indicates is that the value you're providing for key is the same in at least two items of the array. I'd put money on the _id property for at least two of them being null or undefined.

React Material UI Cards w/Modal

I'm trying to configure this React JS / Material UI modal dialog so that when the user clicks on the card, it opens a corresponding full-sized image (with title and subtitle). Data for each card is mapped from a JSON file (via AXIOS).
I can get the modal window to open, but it is showing all of the card images in the modal and they are stacked on top of each other. The console.log("SELECTED CAMPAIGN: ", selectedCampaign) code inside the handleOpen() function is one click behind...it actually logs the object that was selected prior to the current click event.
I'm relatively new to functional components and hooks, so I know that I am making simple and fundamental mistakes...please help me figure out the proper way to set this up:
const CampaignItems = ({campaigns, loading}) => {
const classes = useStyles();
const [open, setOpen] = useState(false);
const [selectedCampaign, setSelectedCampaign] = useState();
const handleOpen = (campaign) => {
setSelectedCampaign(campaign);
console.log("SELECTED CAMPAIGN: ", selectedCampaign);
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
..................
<div>
<GridContainer>
{campaigns && search(campaigns).map((campaign) => (
<GridItem key={campaign.id} xs={12} sm={6} md={4}>
<Card className={classes.root}>
<CardActionArea>
<CardMedia
component="img"
alt={campaign.alt}
height="140"
image={campaign.image}
title={campaign.title}
onClick={() => handleOpen(campaign)}
/>
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{campaign.title}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{campaign.subtitle}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<IconButton
size="medium"
color="primary"
aria-label="More Information"
onClick={() => handleOpen(campaign)}
>
<InfoIcon />
</IconButton>
<Modal
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500
}}
>
<Fade
in={open}
>
<div className={classes.paper}>
<h6>{campaign.title}</h6>
<p>{campaign.subtitle}</p>
<img src={campaign.image} />
</div>
</Fade>
</Modal>
</CardActions>
</Card>
</GridItem>
))}
</GridContainer>
</div>
..................
It seems your issue is that you use only a single open state to trigger your modals to open, so they are all triggered open concurrently.
I suggest to instead using the selectedCampaign of the card you're interacting with and use the campaign id that to match which modal to open.
const CampaignItems = ({campaigns, loading}) => {
...
const [selectedCampaign, setSelectedCampaign] = useState(null);
const handleOpen = (campaign) => () => {
setSelectedCampaign(selectedCampaign =>
selectedCampaign.id === campaign.id ? null : campaign
);
};
const handleClose = () => {
setSelectedCampaign(null);
};
...
<div>
<GridContainer>
{campaigns && search(campaigns).map((campaign) => (
<GridItem key={campaign.id} xs={12} sm={6} md={4}>
<Card className={classes.root}>
<CardActionArea>
<CardMedia
...
onClick={handleOpen(campaign)}
/>
...
</CardActionArea>
<CardActions>
<IconButton
...
onClick={handleOpen(campaign)}
>
...
</IconButton>
<Modal
...
open={selectedCampaign.id === campaign.id} // <-- check id match
onClose={handleClose}
...
>
...
</Modal>
</CardActions>
</Card>
</GridItem>
))}
</GridContainer>
</div>
...

Solve: Use a function from the child component to assign a value into the props?

In my parent component, there's a state that stores the selected task id from the child component. I used the props to pass the data from child to parent component. But my problem is I will use a function that will generate the selected task id inside the child component. The result that i want is if the user selects a button it will trigger the function that will generate the task id. Then it will store the data into props that will be pass to the parent component. The reason why the i will used a function instead of using onClick={e => e.currentTarget.getAttribute('taskkd')} I will also need to pass other props that will display the dialog box.
Parent Component
const Home = () => {
let userInfo = localStorage.getItem('userData');
const [taskDeleteId, setTaskDeleteId] = useState();
const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {
console.log(taskDeleteId);
}, [history, taskDeleteId]);
return (
<>
<Grid item xs={12} sm={8} className={baseClasses.mainTaskContainer}>
<TasksList
deleteId={(deleteId) => setTaskDeleteId(deleteId)}
showPrompt={(showPrompt) => setShowPrompt(showPrompt)}
/>
<PromptComponent showPrompt={showPrompt} shownewArr={newArr} />
</Grid>
</Grid>
</>
);
};
Child Component
const TasksList = ({ deleteId, showPrompt }) => {
const selectedTask = (e) => {
deleteId = e.currentTarget.getAttribute('taskid'); <--- the task id that i need to pass into my parent component
};
return (
<>
<Container
component='div'
maxWidth='xl'
className={classes.taskContainer}
><Container
component='div'
maxWidth='xl'
className={classes.taskContainer}
>
<Container
component='div'
maxWidth='xl'
className={classes.todoContainer}
onDragOver={(e) => onDragOverTask(e)}
onDragLeave={(e) => onDragLeaveTask(e)}
onDrop={(e) => onDropTask(e)}
>
<Typography variant='h6' className={classes.containerTitle}>
<span>To do</span>
<span>{taskTodo.length}</span>
</Typography>
{taskTodo.map((currentTask, index) => {
return (
<Card
variant='outlined'
className={classes.cardTask}
key={index}
style={{ background: '#f3f3f3', marginTop: '1rem' }}
onDragStart={(e) => onDragStartTask(e, currentTask._id)}
draggable
>
{currentTask.desc && (
<Collapse
in={expanded === `todo-panel_${index}`}
timeout='auto'
unmountOnExit
className={classes.collapsePanel}
color='primary'
>
<CardContent className={classes.descPrevContainer}>
<Typography
variant='body1'
className={classes.text}
dangerouslySetInnerHTML={createMarkup(
textTruncate(currentTask.desc, 50)
)}
></Typography>
</CardContent>
</Collapse>
)}
<CardActions
disableSpacing
className={classes.bottomActionsContainer}
>
<Tooltip title='Delete'>
<IconButton
aria-label='delete'
className={classes.BottomDelete}
taskid={currentTask._id}
onClick={(() => showPrompt(true), selectedTask)} <--- props
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title='Edit'>
<IconButton
aria-label='edit'
className={classes.BottomEdit}
taskid={currentTask._id}
>
<EditIcon />
</IconButton>
</Tooltip>
</CardActions>
</Card>
);
})}
</Container>
</>
);
};

React Material UI CardMedia issue

I used CardMedia to show images, when I go to home page, it doesn't show the images and have some errors. When I click some links and use 'Go Back' button in Browser, images come back.
I use getInitialProps() to get image url in Home component and pass as a property to child (Catalog) component.
I thought Image url would be received in Home component then CardMedia will render the image receiving from Home component as a prop.
Any thoughts?
export default function Home({ categories }) {
return (
<div>
<Header categories={ categories}/>
<Carousel />
<Catalog categories={ categories}/>
<Footer/>
</div>
)
}
Home.getInitialProps = async () => {
const response = await fetch('http://localhost:1337/categories/')
const categories = await response.json();
return {
categories:categories
}
}
export default function Catalog(props) {
const classes = useStyles();
const cards = props.categories
const server = 'http://localhost:1337'
console.log(cards)
return (
<React.Fragment>
<CssBaseline />
<main>
{/* Hero unit */}
<div className={classes.heroContent}>
<Container maxWidth="sm">
<Typography component="h1" variant="h2" align="center" color="textPrimary" gutterBottom>
Album layout
</Typography>
<Typography variant="h5" align="center" color="textSecondary" paragraph>
Something short and leading about the collection below—its contents, the creator, etc.
Make it short and sweet, but not too short so folks don&apos;t simply skip over it
entirely.
</Typography>
</Container>
</div>
<Container className={classes.cardGrid} maxWidth="md">
{/* End hero unit */}
<Grid container spacing={4}>
{cards.map((card) => (
<Link as={`/products/${card.category}`} href='/products/[bathroom]' key={card.id}>
<Grid item key={card.category} xs={12} sm={6} md={4} >
<Card className={classes.card}>
<CardMedia
className={classes.cardMedia}
image={server+ card.img.url}
category={card.category}
/>
<CardContent className={classes.cardContent}>
<Typography gutterBottom variant="h5" component="h2">
{card.category}
</Typography>
<Typography>
Product description.
</Typography>
</CardContent>
</Card>
</Grid>
</Link>
))}
</Grid>
</Container>
</main>
{/* Footer */}
{/* End footer */}
</React.Fragment>
);
}
It turns out that CSS rendering is a bit different in Next.js.
see this doc https://material-ui.com/guides/server-rendering/

How to control each card using react hooks

I'm trying to use react with material-ui
There are two cards,
When I click the expand button, both of them are open at the same time
How to open only one card in one click ?
I know is because when it setExpanded, all of the item will be set,
I have try add the key but it still don't work.
Here is the code I tested.
https://stackblitz.com/edit/react-kwexep?file=src%2FApp.js
a simple solution is to store the index value of the expanded card and then pass the expanded prop based on the index value stored in the expanded state.
export default function App() {
let data=[{
id:1,
title:"2"
},
{
id:2,
title:"3"
}]
const classes = useStyles();
const [expanded, setExpanded] = React.useState(false);
const handleExpandClick = (index) => {
expanded === index ? setExpanded(null) : setExpanded(index)
};
return (
data.map((i,index)=>(
<Card className={classes.root} >
<CardActions disableSpacing>
<IconButton
className={clsx(classes.expand, {
[classes.expandOpen]: expanded ===index,
})}
onClick={()=>handleExpandClick(index)}
aria-expanded={expanded === index}
aria-label="show more"
>
<ExpandMoreIcon />
</IconButton>
</CardActions>
<Collapse in={expanded===index} timeout="auto" unmountOnExit>
<CardContent>
<Typography paragraph> </Typography>
<Typography paragraph>
{i.title}
</Typography>
</CardContent>
</Collapse>
</Card>
))
);
}
Here is the working demo: https://stackblitz.com/edit/react-yyycsr?file=src%2FApp.js

Resources