I have a page that renders questions that have been posted. I want to create a button that displays only answered questions based on the state = {isAnswered: true}.
Is the state isAnswered is true then onClick will display answered questions only where isAnswered is set to true in the object.
How can I used this Filter button to conditionally render these based on their state.
Should the function be stored as constant called in the render function or before this?
this.state.posts is an array of these objects on the back end:
Here is what I have attempted.
class Posts extends Component {
state = {
posts: []
}
render () {
let posts = <p style={{ textAlign: 'center' }}>Something went wrong!</p>;
let {isAnswered} = this.state;
const renderAuthButton = () => {
if (isAnswered === true) {
if ( !this.state.error ) {
posts = this.state.posts.map( (post) => {
return (
<Post
key={post.key}
id={post.key}
title={post.title}
type={post.type}
body={post.body}
answer={post.answer}
onChange={(value, id) => this.postAnswerHandler(value,id)}
clicked={(body) => this.displayAnswerHandler(body)}
/>
);
} );
}
}
}
}
return (
<button onClick={renderAuthButton()}>Filter</button>
{posts}
)
You are misinterpreting your data structure. this.state has a property this.state.posts which is an array. Each element in the array is an object with multiple properties including isAnswered.
When you do this:
let {isAnswered} = this.state;
You are looking for a property this.state.isAnswered which does not exist. There is no top-level isAnswered property. It is something that exists within each post object and is different for every post. So you need to be looking at isAnswered inside of your loop.
There's honestly a lot that's weird and backwards here. Don't create a callback inside of render()! Don't return JSX from a callback!
Here's my attempt to clean it up. I am adding a property to this.state which tells us whether or not to filter the posts. Clicking the button changes this.state.isFiltered. The render function renders appropriately based on the current state.
class Posts extends Component {
state = {
posts: [],
isFiltered: false,
isError: false
};
async componentDidMount() {
// do your API fetch and set the state for `posts` and `isError`
try {
const fetchedPosts = someApiFunction();
this.setState({
posts: fetchedPosts
});
} catch (error) {
this.setState({
isError: true
});
}
}
onClickFilter = () => {
// toggles filter on and off
this.setState((prevState) => ({
isFiltered: !prevState.isFiltered
}));
};
render() {
if (this.state.isError) {
return <p style={{ textAlign: "center" }}>Something went wrong!</p>;
}
// show only answered posts if isFiltered is true, or all posts if false
const visiblePosts = this.state.isFiltered
? this.state.posts.filter((post) => post.isAnswered)
: this.state.posts;
return (
<>
<button onClick={this.onClickFilter}>Filter</button>
{visiblePosts.map((post) => {
return (
<Post
key={post.key}
id={post.key}
title={post.title}
type={post.type}
body={post.body}
answer={post.answer}
onChange={(value, id) => this.postAnswerHandler(value, id)}
clicked={(body) => this.displayAnswerHandler(body)}
/>
);
})}
</>
);
}
}
Related
I've been learning Reactjs for couple weeks now and I'm trying to build a game where a single object question is being rendered to the DOM however I'm unsure on how to do that? I've built a helper function to create all the components from that API response but of course it renders all my components and not just one. Is CSS involved to change the display or can it be done with some JS logic?
class Main extends Component{
state = {
quesArr: [],
ansArr: []
}
componentDidMount(){
fetch('http://localhost:3500/questions')
.then(quesArr => quesArr.json())
.then(newQuesArr => {
this.setState({
quesArr: [...newQuesArr]
})
})
fetch('http://localhost:3500/answers')
.then(answerArr => answerArr.json())
.then(newAnsArr =>{
this.setState({
ansArr: newAnsArr
})
})
}
render(){
let questions = this.state.quesArr
const renderQues = () => (
questions.map((question, index) => <Question questionObj={question} key={index}/>)
)
return (
<div>
Questions go here:
{renderQues()}
Answers go here:
</div>
)
}
}
export default Main;
You don't need a map for that you just need to display the first question and then have a button to increase the active question index. (I added //change above the lines where I made changes)
class Main extends Component{
state = {
//change
activeQ: 0,
quesArr: [],
ansArr: []
}
componentDidMount(){
fetch('http://localhost:3500/questions')
.then(quesArr => quesArr.json())
.then(newQuesArr => {
this.setState({
quesArr: [...newQuesArr]
})
})
fetch('http://localhost:3500/answers')
.then(answerArr => answerArr.json())
.then(newAnsArr =>{
this.setState({
ansArr: newAnsArr
})
})
}
//change
updateActiveQuestion = () => this.setState((prevState) => {activeQ: prevState.activeQ + 1})
render(){
let questions = this.state.quesArr
const renderQues = () => (
questions.map((question, index) => <Question questionObj={question} key= {index}/>)
)
return (
<div>
Questions go here:
//change
<Question questionObj={questions[this.state.activeQ]}/>
<button onClick={() => this.updateActiveQuestion()}>Next Question</button>
Answers go here:
</div>
)
}
}
This is not a complete solution, it doesn't restrict the user from clicking next when there are no more questions.
I have a component which you can toggle on/off by clicking on it:
clickHandler = () => {
this.setState({active: !this.state.active})
this.props.getSelection(this.state.active)
}
render() {
const { key, children } = this.props;
return (
<button
key={key}
style={{...style.box, background: this.state.active ? 'green' : ''}}
onClick={() => this.clickHandler()}
>
{children}
</button>
);
}
In the parent component, I pass down a method in order to try and get the value of the selected element pushed into an array, like so:
getSelection = (val) => {
const arr = []
arr.push(val);
console.log(arr, 'arr');
}
My problem is that it only ever adds one element to the array, so the array length is always 1 (even if more than one item has been clicked).
Current result (after you've clicked all three)
console.log(arr, 'arr') // ["Birthday"] "arr"
Expected result (after you've clicked all three)
console.log(arr, 'arr') // ["Birthday", "Christmas", "School achievement"] "arr"
Link to Codepen
Any ideas?
Two things:
setState is async, so on the next line you might or might not get the latest value, so I recommend changing
clickHandler = () => {
this.setState({active: !this.state.active})
this.props.getSelection(this.state.active)
}
to
clickHandler = () => {
this.setState({active: !this.state.active}, () => {
this.props.getSelection(this.state.active)
})
}
The second argument to the setState is a callback function that will be executed right after the setState is done.
The second thing, on getSelection you are defining a new array each time you get there, so it won't have the values from the previous run. You should store it somewhere.
There are 2 problems here:
arr is local variable. It doesn't keep the previous onClick result.
setState is an asynchronous event. According to documentation:
setState() does not always immediately update the component.
setState((state, props) => {}, () => { /*callback */}) should be used.
class Box extends React.Component {
state = {
active: false
};
clickHandler = () => {
this.setState(
state => ({ active: !state.active }),
() => {
this.props.getSelection(this.state.active);
}
);
};
render() {
const { children } = this.props;
return (
<button
style={{ ...style.box, background: this.state.active ? "green" : "" }}
onClick={this.clickHandler}
>
{children}
</button>
);
}
}
Minor note:
The key value isn't in the child component's this.props, so you don't have to pass it, but it will not affect the outcome.
In App component, let's create an array in class level for the sake of display:
class App extends React.Component {
state = {
needsOptions: ["Birthday", "Christmas", "School achievement"]
};
arr = [];
getSelection = val => {
this.arr.push(val);
console.log(this.arr);
};
}
CodePen here
I have three files: ShopsContainer.js ShopsComponent.js and ShopsItemComponent.js
ShopsContainer maintains an array of shop items in local state that gets passed down into ShopsComponent as props. ShopsComponent then maps through the items array that is being received as props and renders a ShopsItemComponent for each item in the array.
Within my ShopsContainer file, I have a method that removes a shop item from state using the following code:
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
When this happens, the correct item is removed from the items array in state, however, whatever the last ShopItem is that is in the DOM at the time of the removeShop call will get removed no matter if it is the correct item that should be removed or not. In other words, when removeShop gets called and the items array in state gets updated correctly, the wrong ShopItemComponent gets removed from the DOM.
What I would like to happen (or what I think should happen) is when removeShop gets called, that shop gets removed from the items array in state and ShopsContainer re-renders causing ShopsComponent to re-render with the updated props being received. And lastly ShopsComponent would map through the newly updated items array in props displaying a `ShopItemComponent for the correct items. Perhaps the problem has to do with the props being updated?
My code is as follows:
ShopsContainer.js
class ShopsContainer extends Component {
constructor() {
this.state = {
items: null
}
this.getAll();
this.removeShop = this.removeShop.bind(this);
}
getAll = () => {
// API request that fetches items and updates state
}
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
render() {
return (
<div>
{this.state.items ? <ShopComponent items={this.state.items} removeShop={this.removeShop} /> : <div><h1>Loading...</h1></div>}
</div>
);
}
}
ShopsComponent.js
class ShopsComponent extends Component {
constructor() {
this.handleRemove = this.handleRemove.bind(this);
}
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem, i) => {
return (<ShopItemComponent key={i} item={shopItem} removeShop={this.handleRemove} />);
})}
</React.Fragment>
);
}
}
Your code is working great, but you only has one mistake , your ShopComponent is assign index as a key for each ShopItemComponent and react is tracking those indexes to update the correct component, so you need to set key as a unique value between items, then I realize that shopAccount should be your id for each item.
The solution code is below.
class ShopsComponent extends Component {
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem) => <ShopItemComponent key={shopItem.shopAccount} item={shopItem} removeShop={this.handleRemove} />)}
</React.Fragment>
);
}
}
I hope you can find useful.
Note, when you are using a arrow function into your class, don't bind that method into the constructor, so remove it, because
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
is already binded.
I'm having an issue getting my child component to update with new props. I'm deleting an item from the global state and it's successful. But when the item gets deleted, I'm expecting that item to no longer show. It's still being shown but if I were to move to another screen then back, it's gone. Any idea on what I might be missing here?
Thanks!!
export default class Summary extends Component {
constructor(props) {
super(props);
this.state = {
pickupData: this.props.pickup
};
}
handleDelete(item) {
this.props.deleteLocationItem(item);
}
render() {
const pickup = this.state.pickup;
return (
<View>
{pickup.map((item, i) => (
<LocationItem
name={item}
onPressDelete={() => this.handleDelete(item)}
/>
))}
</View>
);
}
}
const LocationItem = ({ onPressDelete, name }) => (
<TouchableOpacity onPress={onPressDelete}>
<Text>Hi, {name}, CLICK ME TO DELETE</Text>
</TouchableOpacity>
);
------- Additional Info ------
case 'DELETE_LOCATION_INFO':
return Object.assign({}, state, {
pickup: state.pickup.filter(item => item !== action.action)
})
export function deleteLocationInfo(x){
return {
type: DELETE_LOCATION_INFO,
action: x
}
}
Your deleteLocationItem must be something like this:
deleteLocationItem(id) {
this.setState({
items: this.state.items.filter(item => item.id !== id)
});
}
Then inside your Summary class you dont need to set the prop again. Just receive pickup from props like this:
render (
const { pickup } = this.props;
return(
<View>
{ pickup.map
...
Render is happening based on the state which is not updated other than in constructor. When the prop updates from parent, it is not reflected in the state.
Add componentWillReceiveProps method to receive new props and update state, which will cause new data to render
But more preferably, if the state is not being changed in any way after initialization, render directly using the prop itself which will resolve this issue
I have a map that render few items and one of its line is below
<a onClick={()=> this.setState({"openDeleteModal":true)}>Delete</a>
Obviously I want to open a modal when user click the delete, but I have to pass a few things like the name of the item, id of the item to perform the deletion. How can I pass says the name to the modal?
I can bind the obj name to a like this
Delete
Am I on the right track?
When working on React applications, try not to think in terms of passing values to other components, but rather updating state that your components are exposed to.
In your example, assuming your modal component is a child of the same component your list of a tags belongs to, you could set the values you are interested in exposing to the modal on the state, as well as updating the property that signals whether the modal is open or not. For example:
class Container extends React.Component {
constructor(props) {
super(props)
this.state = {
openDeleteModal: false,
activeItemName: '', //state property to hold item name
activeItemId: null, //state property to hold item id
}
}
openModalWithItem(item) {
this.setState({
openDeleteModal: true,
activeItemName: item.name,
activeItemId: item.id
})
}
render() {
let buttonList = this.props.item.map( item => {
return (<button onClick={() => this.openModalWithItem(item)}>{item.name}</button>
});
return (
<div>
{/* Example Modal Component */}
<Modal isOpen={this.state.openDeleteModal}
itemId={this.state.activeItemId}
itemName={this.state.activeItemName}/>
{ buttonList }
</div>
)
}
}
Copying over my answer from How to pass props to a modal
Similar scenario
constructor(props) {
super(props)
this.state = {
isModalOpen: false,
modalProduct: undefined,
}
}
//****************************************************************************/
render() {
return (
<h4> Bag </h4>
{this.state.isModalOpen & (
<Modal
modalProduct={this.state.modalProduct}
closeModal={() => this.setState({ isModalOpen: false, modalProduct: undefined})
deleteProduct={ ... }
/>
)
{bag.products.map((product, index) => (
<div key={index}>
<div>{product.name}</div>
<div>£{product.price}</div>
<div>
<span> Quantity:{product.quantity} </span>
<button onClick={() => this.props.incrementQuantity(product, product.quantity += 1)}> + </button>
<button onClick={() => this.decrementQuantity(product)}> - </button> // <----
</div>
</div>
))}
)
}
//****************************************************************************/
decrementQuantity(product) {
if(product.quantity === 1) {
this.setState({ isModalOpen: true, modalProduct: product })
} else {
this.props.decrementQuantity(product)
}
}
Try this: this is the form which has the button, and is a child component of some other component that passes the handleButtonAction method as props, and the button takes the input data and invokes this parent component method
handleSubmit = (e) => {
e.preventDefault();
const data = e.target.elements.option.value.trim();
if (!data) {
this.setState(() => ({ error: 'Please type data' }));
} else {
this.props.handleButtonAction(data, date);
}
}
{this.state.error && <p>{this.state.error}</p>}
<form onSubmit={this.handleSubmit}>
<input type="text" name="option"/>
<div>
<button>Get data</button>
</div>
</form>
The parent component:
handleButtonAction = (data) => {
axios.get(`http://localhost:3000/someGetMethod/${data}`).then(response => {
const resData = response.data;
this.setState({
openModal: true,
status: response.status,
data: resData
});
}).catch((error) => {
if (error.message.toLowerCase() === 'network error') {
this.setStateWithError(-1, {});
}
else { // not found aka 404
this.setStateWithError(error.response.status, '', {currency, date: ddat});
}
});
}