React - change this.state onClick rendered with array.map() - javascript

I'm new to React and JavaScript.
I have a Menu component which renders an animation onClick and then redirects the app to another route, /coffee.
I would like to pass the value which was clicked (selected) to function this.gotoCoffee and update this.state.select, but I don't know how, since I am mapping all items in this.state.coffees in the same onClick event.
How do I do this and update this.state.select to the clicked value?
My code:
class Menus extends Component{
constructor (props) {
super(props);
this.state = {
coffees:[],
select: '',
isLoading: false,
redirect: false
};
};
gotoCoffee = () => {
this.setState({isLoading:true})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
renderCoffee = () => {
if (this.state.redirect) {
return (<Redirect to={`/coffee/${this.state.select}`} />)
}
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map(c =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee()}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
export default withRouter(Menus);
I have tried passing the value like so:
gotoCoffee = (e) => {
this.setState({isLoading:true,select:e})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.state.select)
}
an like so:
<div onClick={(c) => this.gotoCoffee(c)}
or so:
<div onClick={(event => this.gotoCoffee(event.target.value}
but console.log(this.state.select) shows me 'undefined' for both tries.
It appears that I'm passing the Class with 'c'.
browser shows me precisely that on the uri at redirect:
http://localhost/coffee/[object%20Object]
Now if I pass mapped 'c' to {this.renderCoffee(c)}, which not an onClick event, I manage to pass the array items.
But I need to pass not the object, but the clicked value 'c' to this.gotoCoffee(c), and THEN update this.state.select.
How do I fix this?

You can pass index of element to gotoCoffee with closure in render. Then in gotoCoffee, just access that element as this.state.coffees[index].
gotoCoffee = (index) => {
this.setState({isLoading:true, select: this.state.coffees[index]})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map((c, index) =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee(index)}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}

so based off your code you could do it a couple of ways.
onClick=(event) => this.gotoCoffee(event.target.value)
This looks like the approach you want.
onClick=() => this.gotoCoffee(c)
c would be related to your item in the array.

All the answers look alright and working for you and it's obvious you made a mistake by not passing the correct value in click handler. But since you're new in this era I thought it's better to change your implementation this way:
It's not necessary use constructor at all and you can declare a state property with initial values:
class Menus extends Component{
state= {
/* state properties */
};
}
When you declare functions in render method it always creates a new one each rendering which has some cost and is not optimized. It's better if you use currying:
handleClick = selected => () => { /* handle click */ }
render () {
// ...
coffees.map( coffee =>
// ...
<div onClick={ this.handleClick(coffee) }>
// ...
}
You can redirect with history.replace since you wrapped your component with withRouterand that's helpful here cause you redirecting on click and get rid of renderCoffee method:
handleClick = selected => () =>
this.setState(
{ isLoading: true},
() => setTimeout(
() => {
const { history } = this.props;
this.setState({ isLoading: false });
history.replace(`/${coffee}`);
}
, 5000)
);
Since Redirect replaces route and I think you want normal page change not replacing I suggest using history.push instead.

You've actually almost got it in your question. I'm betting the reason your state is undefined is due to the short lived nature of event. setState is an asynchronous action and does not always occur immediately. By passing the event off directly and allowing the function to proceed as normal, the event is released before state can be set. My advice would be to update your gotoCoffee function to this:
gotoCoffee = (e) => {
const selectedCoffee = e.target.value
this.setState({isLoading:true,select:selectedCoffee},() =>
{console.log(this.state.select})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
Note that I moved your console.log line to a callback function within setState so that it's not triggered until AFTER state has updated. Any time you are using a class component and need to do something immediately after updating state, use the callback function.

Related

React: Data rendering on second click, but not on first

I have a modal displaying data that I'm receiving in props. When I open the modal, I should see select data displayed from my props. However, the modal is empty the first time I open it, and populates the second time.
If I go on to change the data in props, the modal stays the same on the first new click, and refreshes on the second new click.
I've tried forcing it with setTimeout, messing with combos of componentDidMount, componentDidUpdate, and other lifecycle methods, but nothing seems to work. I'm sure it has something to do with my using the prevData param in componentDidMount. But even thought react devtools shows this.state.pricesData updates, when I try rendering from state I get blanks every time. When I invoke a console log as a callback of setState, I get an empty array bracket in the console log (which I can expand to show all the correct array data, but I guess that's populated async after the log).
Here's the code:
import React, { Component } from "react";
import "../../App.css";
let explanations = [];
export default class ExplanationModal extends Component {
constructor(props) {
super(props);
this.state = {
pricesData: [],
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.pricesData !== prevState.pricesData) {
return { pricesData: nextProps.pricesData };
} else {
return null;
}
}
// to allow for async rendering
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.pricesData !== this.state.pricesData) {
return this.state.pricesData;
}
}
componentDidMount = () => {
this.setState({ pricesData: this.props.pricesData }, () =>
console.log(this.state.pricesData)
);
};
componentDidUpdate = (prevData) => {
this.renderExp(prevData.pricesData);
};
renderExp = (data) => {
explanations = [];
data.forEach((set) =>
explanations.push({ title: set.titel, explanation: set.explenation })
);
};
onClose = () => {
this.props.hideModal();
};
render() {
return (
<div className="modal">
<div>
{explanations.map((item) => (
<span>
<h4>{item.title}</h4>
<p>{item.explanation}</p>
</span>
))}
</div>
<button onClick={this.onClose} className="update">
Close
</button>
</div>
);
}
}
you have to keep your explanation array in your state. then update the state when new data arrives. because react doesn't trigger a re render if you don't update the state .
your constructor should be
super(props);
this.state = {
pricesData: [],
explanations : []
};
}
and your renderExp function should be
renderExp = (data) => {
explanations = [];
data.forEach((set) =>
explanations.push({ title: set.titel, explanation: set.explenation })
);
this.setState({ explanations })
};
inside your render function
render() {
return (
<div className="modal">
<div>
{this.state.explanations.map((item) => (
<span>
<h4>{item.title}</h4>
<p>{item.explanation}</p>
</span>
))}
</div>
<button onClick={this.onClose} className="update">
Close
</button>
</div>
);
}
}
This way you will get the updated data when it arrives.

How to add event to react function and re-render function

I have a function that renders content to page based on a state populated by API data, but I need to have an onClick event to refine that content;
So currently getPosts returns information from the state 'posts' which is provided with data from our API, but i want to filter this content further, so my idea is to have some sort of event listener, and if actioned, change the data coming out of getPosts.
constructor() {
super();
this.state = {
posts: ""
}
this.getPosts = this.getPosts.bind(this);
}
async componentWillMount(){
var data = await api.posts();
this.setState({posts: data.data});
console.log(this.state.posts);
}
getPosts(type){
if(this.state.posts.length){
return this.state.posts.map((content,index) => {
var url = content.Title.replace(/[^\w\s]/gi, '');
url = url.replace(/\s+/g, '-').toLowerCase();
if(type === content.PostType){
//output something different
}
else{
return(
<Col md={4} className="mb-4" key={index}>
{content.title}
</Col>
);
}
})
}
}
render() {
return (
<div>
<p><button onClick={()=>{this.getPosts('blog')}}>blog</button> <button onClick={()=>{this.getPosts('news')}}>news</button></p>
{this.getPosts()}
</div>
)
}
So my getPosts works fine without any type, how to do tell it to re-output the function on the page based in the onClick event?
Without getting into the complexities of context and keys, a component requires a change in props or state to re-render. To read more about state and component life-cycle, the docs have a great explanation for class components.
Your component does not re-render after the onClick event handler's call to getPosts because getPosts does not update internal component state. getPosts works within render because those values are being returned to React. By using getPosts as an onClick event handler, you are creating React elements and trying to return them to the window.
What follows should be treated as psuedo code that shows how to trigger your component to render different posts:
Consider adding another key to state in your constructor,
constructor(props) {
super(props);
this.state = {
posts: "",
type: null
};
this.getPosts = this.getPosts.bind(this);
this.onClick = this.onClick.bind(this);
}
and creating a click handler that doesn't try to return React elements
function onClick(evt) {
this.setState({ type: evt.target.value });
}
and values to your buttons
<button onClick={this.onClick} type="button" value="blog">blog</button>
Now your button will update state with your new post type, causing your component to re-render:
render() {
return (
<div>
<p>
<button onClick={this.onClick} type="button" value="blog">blog</button>
<button onClick={this.onClick} type="button" value="news">news</button>
</p>
{this.getPosts()}
</div>
);
}
With the content type being stored in state, you can now implement your getPosts call in any way that works for you. Good luck!
It strays from the question asked, but it is worth noting componentWillMount is being deprecated, and componentDidMount is a preferable life-cycle function for side-effects and asynchronous behavior. Thankfully, the documentation has lots of details!
Ok so you should start by changing your default this.state to
this.state = {
posts: []
}
remember that you want to iterate over an array of data instead of iterate a string, that will throw an error if you do that, so better keep from the beginning the data type you want to use.
Then you need to separate the responsibility for your getPosts method, maybe getPostByType is a better name for that, so you have
getPostByType(type) {
// if type is same as content.PostType then return it;
const nextPosts = this.state.posts.filter((content) => type === content.PostType);
this.setState({ posts: nextPosts });
}
and finally you can iterate over posts, like this
render() {
// better use content.id if exists instead of index, this is for avoid problems
// with rerender when same index key is applied.
const posts = this.state.posts.map((content, index) => (
<Col md={4} className="mb-4" key={content.id || index}>
{content.title}
</Col>
));
return (
<div>
<button onClick={() => this.getPostByType('blog')}>Show Blog Posts</button>
{posts}
</div>
);
}
Then you can use getPostsByType any time in any click event passing the type that you want to render.

getDerivedStateFromProps, change of state under the influence of changing props

I click Item -> I get data from url:https: // app / api / v1 / asset / $ {id}. The data is saved in loadItemId. I am moving loadItemId from the component Items to the component Details, then to the component AnotherItem.
Each time I click Item the props loadItemId changes in the getDerivedStateFromProps method. Problem: I'll click Element D -> I see in console.log 'true', then I'll click Element E --> It display in console.log true andfalse simultaneously, and it should display only false.
Trying to create a ternary operator {this.state.itemX ['completed'] ? this.start () : ''}. If {this.state.itemX ['completed'] call the function this.start ()
Code here: stackblitz
Picture: https://imgur.com/a/OBxMKCd
Items
class Items extends Component {
constructor (props) {
super(props);
this.state = {
itemId: null,
loadItemId: ''
}
}
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://app/api/v1/asset/${id}`,
method: "GET",
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
this.setState({
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render () {
return (
<div >
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
>
<Details
loadItemId={this.state.loadTime}
/>
</div>
)
}
Item
class Item extends Component {
render () {
return (
<div onClick={() => this.props.selectItem(item.id}>
</div>
)
}
}
Details
class Details extends Component {
constructor() {
super();
}
render () {
return (
<div>
<AnotherItem
loadItemId = {this.props.loadItemId}
/>
</div>
)
}
}
AnotherItem
class AnotherItem extends Component {
constructor() {
super();
this.state = {
itemX: ''
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.loadItemId !== prevState.loadItemId) {
return { itemX: nextProps.loadItemId }
}
render () {
console.log(this.state.itemX ? this.state.itemX['completed'] : '');
{/*if this.state.loadX['completed'] === true, call function this.start()*/ }
return (
<button /*{this.state.loadX['completed'] ? this.start() : ''}*/ onClick={this.start}>
Start
</button>
);
}
}
here:
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
you call setState(), then 'Item' and 'Details' and 'AnotherItem' call their render method. so you see log for previous 'loadItemId'.
when 'load' method work done. here:
this.setState({
loadItemId: response.data
});
you setState() again, then 'Item' and 'Details' and 'AnotherItem' call their render method again. in this time you see log for new 'loadItemId'.
solution
setState both state in one place. after load method done, instead of:
this.setState({
loadItemId: response.data
});
write:
this.setState({
itemId: id,
loadItemId: response.data
});
and remove:
this.setState({
itemId: id
})
from 'selectItem' method.
Need some clarification, but think I can still address this at high level. As suggested in comment above, with the information presented, it does not seem that your component AnotherItem actually needs to maintain state to determine the correct time at which to invoke start() method (although it may need to be stateful for other reasons, as noted below).
It appears the functionality you are trying to achieve (invoke start method at particular time) can be completed solely with a comparison of old/new props by the componentDidUpdate lifecycle method. As provided by the React docs, getDerivedStateFromProps is actually reserved for a few 'rare' cases, none of which I believe are present here. Rather, it seems that you want to call a certain method, perhaps perform some calculation, when new props are received and meet a certain condition (e.g., not equal to old props). That can be achieved by hooking into componentDidUpdate.
class AnotherItem extends Component {
constructor(props) {
super(props);
this.state = {}
}
start = () => { do something, perform a calculation }
// Invoked when new props are passed
componentDidUpdate(prevProps) {
// Test condition to determine whether to call start() method based on new props,
// (can add other conditionals limit number of calls to start, e.g.,
// compare other properties of loadItemId from prevProps and this.props) .
if (this.props.loadItemId && this.props.loadItemId.completed === true) {
//Possibly store result from start() in state if needed
const result = this.start();
}
}
}
render () {
// Render UI, maybe based on updated state/result of start method if
// needed
);
}
}
You are encountering this behaviour because you are changing state of Items component on each click with
this.setState({
itemId: id
})
When changing its state, Items component rerenders causing AnotherItem to rerender (because that is child component) with it's previous state which has completed as true (since you've clicked element D before). Then async request completes and another rerender is caused with
this.setState({
loadItemId: response.data
});
which initiates another AnotherItem rerender and expected result which is false.
Try removing state change in selectItem and you'll get desired result.
I'd suggest you read this article and try to structure your code differently.
EDIT
You can easily fix this with adding loader to your component:
selectItem = (id) => {
this.setState({
itemId: id,
loading: true
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://jsonplaceholder.typicode.com/todos/${id}`,
method: "GET"
})
.then(response => {
this.setState({
loading: false,
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render() {
return (
<div >
<ul>
{this.state.items.map((item, index) =>
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
/>
)
}
</ul>
{this.state.loading ? <span>Loading...</span> : <Details
itemId={this.state.itemId}
loadItemId={this.state.loadItemId}
/>}
</div>
)
}
This way, you'll rerender your Details component only when you have data fetched and no unnecessary rerenders will occur.

Is there a way to pass in a variable dynamically into a toggle function without causing a looping error

I am trying to create a dynamic handler function that allows me to choose which state it is toggling true or false and pass this into a component
So far I get maximum depth exceeded
class Demo extends React.Component {
state = {
state1: false,
state2: false,
}
handleChange = (input) => {
this.setState(prevState => ({[input]: !prevState.input}));
};
render() {
const { state1 } = this.state
return (
<DemoComp2 handleChange={this.handleChange} state1={state1}/>
)
}
}
Next component
class DemoComp2 extends React.Component {
render() {
const { state1, handleChange } = this.props;
return(
<button onClick={handleChange('state1')}>test</button>
{ state1 === true ? <p> true </p> : <p> false </p> }
)
}
}
Because in DemoComp2 onClick is calling handleChange() and not handleChange it infinitely loops.
But I need to pass state1 to the function with handleChange('state1') so it knows which state to toggle.
Is there another way of doing this that is as concise or do I need to break it out into separate functions?
I see two problems here.
Your initial state is:
{
state1: false,
state2: false,
}
But when you update it, you're changing the format. You should use the spread operator to get the other parts of prevState:
handleChange = input => {
this.setState(prevState => {
return {
...prevState,
[input]: !prevState[input],
}
})
}
Secondly, handleChange isn't receiving the state1 value - it's receiving the JS event. You need to call a function that calls handleChange:
<button onClick={() => handleChange('state1')}>test</button>
What is happen here is that your onClick func is being called on each render instead only on onClick event. Just pass it an arrow func to avoid it.
onClick={() => handleChange(‘state1’)}
First of all, I do not think the code you shared works.
{ state === true ? <p> true </p> : <p> false </p> }
you have not declared state anywhere on Demo2 component
Second of all, you are executing the handleChange function right the way, instead of executing it when the button is clicked.
Solution
class DemoComp2 extends React.Component {
render() {
const { state1, handleChange } = this.props;
return(
<button onClick={() => handleChange('state1')}>test</button>
{ state1 === true ? <p> true </p> : <p> false </p> }
)
}
}

Dispatch to props only when clicked

The issue I am having is that the mapDispatchToProps is getting sent as a whole, where I want it to only send if I click on the button delete.
This is my class, its fetching the fetchList good, everything is working as expected but when I've added the delete button its seems to mess it all up, it seems to call the delete for every refresh on the page, any idea why?
Could it be the render() where I create the Button maybe it gets triggered without me clicking it? Just by creating the list, because it gets triggered for every occasion that each itemInList gets created via the map.
class List extends Component {
componentWillMount() {
this.props.fetchList();
}
componentWillReceiveProps(nextProps) {
if (nextProps.newItem) {
this.props.list.unshift(nextProps.newItem);
}
}
onDelete = (id) => {
this.props.deleteItem(id);
}
render() {
const listItems = this.props.list.map(itemInList => (
<div key={itemInList.id}>
<h3 className="title__font smaller">{itemInList.title}
<Button
btnType="Delete"
onClick={this.onDelete(itemInList.id)}>
<i className="fas fa-trash-alt"></i>
</Button>
</h3>
<p className="body__font">{itemInList.body}</p>
</div>
));
return (
<div>
<h1 className="title__font">List</h1>
{ listItems }
</div>
);
};
};
List.propTypes = {
fetchList: PropTypes.func.isRequired,
list: PropTypes.array.isRequired,
newItem: PropTypes.object
};
const mapStateToProps = state => ({
list: state.list.items,
newItem: state.list.item
});
const mapDispatchToProps = dispatch => {
return {
fetchList: () => dispatch( actions.fetchList() ),
deleteItem: (id) => dispatch( actions.deleteItem(id) )
};
};
export default connect(mapStateToProps, mapDispatchToProps)(List);
This is the actions for the delete item:
export const deleteItem = (id) => dispatch => {
console.log(id);
dispatch({
type: actionTypes.DELETE_ITEM,
payload: filtered
})
};
That log gets triggered 10 times in the actions file.
You're going to want to pass a function declaration into onClick and you'll need to pass the id in somehow. We don't want to declare any functions in the render method for performance issues, but we need some way to pass the id into the method upon invocation. Data attributes are a great solution to this problem.
Here is some relevant documentation.
React: Handling Events
HTMLElement.dataset
babel-plugin-proposal-class-properties
First, ensure the this context of your method is bound to the component as follows:
constructor(props) {
super(prop)
this.onDelete = this.onDelete.bind(this)
}
The above is required because class methods are actually defined on their prototype and not on individual instantiations. Side note: If you're using a build system that has something along the lines of babel-plugin-proposal-class-properties you could declare your method as follows:
onDelete = (e) => { this.props.deleteItem(e.target.dataset.id) }
You'll need to update your onDelete method as follows:
onDelete = (e) => {
this.props.deleteItem(e.target.dataset.id);
}
and you'll also need to update the Button markup in your render method like this:
<Button
btnType="Delete"
data-id={itemInList.id}
onClick={this.onDelete}
>
<i className="fas fa-trash-alt"></i>
</Button>
EDIT:
Here is a working Code Sandbox to demonstrate how it all works. I had to make some changes to your code such as excluding the non-included <Button/> component. I hope this helps you get where you need to be.
You're calling onDelete immediately, so it will dispatch on render.
Try replacing:
onDelete = (id) => {
this.props.deleteItem(id);
}
with
onDelete = (id) => () => this.props.deleteItem(id);

Categories

Resources