Automatically render child component when state has been updated in parent component - javascript

The parent component Dashboard holds the state for every ListItem I add to my Watchlist. Unfortunately, every time I am adding an Item, it gets added to the DB, but only shows up when I refresh the browser.
class UserDashboard extends React.Component {
state = {
data: []
}
componentWillMount() {
authService.checkAuthentication(this.props);
}
isLoggedIn = () => {
return authService.authenticated()
}
getAllCoins = () => {
//fetches from backend API
}
addWishlist = () => {
this.getAllCoins()
.then(things => {
this.setState({
data: things
})
})
console.log("CHILD WAS CLICKED")
}
componentDidMount() {
this.getAllCoins()
.then(things => {
this.setState({
data: things
})
})
}
render() {
return (
<div className="dashboard">
<h1>HI, WELCOME TO USER DASHBOARD</h1>
<SearchBar
addWishlist={this.addWishlist}
/>
<UserWatchlist
data={this.state.data}
/>
</div>
);
}
}
The User Watchlist:
class UserWatchlist extends React.Component {
constructor(props) {
super(props)
}
// componentDidUpdate(prevProps) {
// if (this.props.data !== prevProps.data) {
// console.log("CURRENT", this.props.data)
// console.log("PREVs", prevProps.data)
// }
// }
render() {
return (
<div>
<h2>These are tssssyou are watching:</h2>
<ul className="coin-watchlist">
{
this.props.data.map((coin, idx) => {
return <ListItem key={idx}
coin={coin.ticker}
price={coin.price}
/>
})
}
</ul>
</div>
)
}
}
The search Bar that shows potential Items to watch over:
class SearchBar extends React.Component {
constructor(props) {
super(props)
this.state = {
coins: [],
searchValue: ""
}
}
searchHandler = e => {
e.preventDefault()
const value = e.target.value
this.setState({
searchValue: value
});
if (value === "") {
this.setState({
coins: []
})
} else {
this.getInfo()
}
}
getInfo = () => {
// Searches the API
}
addWishlist = () => {
this.props.addWishlist();
}
render() {
const {coins, searchValue} = this.state
return (
<div className="coin-search">
<form>
<input
type="text"
className="prompt"
placeholder="Search by ticker symbol"
value={searchValue}
onChange={this.searchHandler}
/>
</form>
<ul className="search-suggestions">
{
coins.filter(searchingFor(searchValue)).map( coin =>
<Currency
coin={coin}
addWishlist={this.addWishlist}
/>
)
}
</ul>
</div>
);
}
}
And the actual Currency that gets clicked to be added:
class Currency extends React.Component {
addToWatchlist = () => {
// POST to backend DB to save
};
fetch("/api/add-coin", settings)
.catch(err => {
return err
})
}
clickHandler = () => {
this.addToWatchlist()
this.props.addWishlist()
}
render() {
return(
<div className="search-results">
<li>
<h3> { this.props.coin.currency } </h3>
<button
className="add-to-list"
onClick={this.clickHandler}
>
+ to Watchlist
</button>
</li>
</div>
)
}
}
As you can see, I am sending props down all the way down to child. When I click the button to Add to Watchlist, I see the console.log message appear, saying "CHILD WAS CLICKED". I've even tried just calling the method to fetch from backend API again.
Also, in UserWatchlist, I've tried a componentDidUpdate, but both prevProps and this.props show the very same array of data. Somewhere in the chain, my data is getting lost.
This is also my first time posting a question here, so if it can be improved, I am happy to add extra details and contribute something to this community

You probably forgot to wait for addToWatchlist to complete:
addToWatchlist = () => {
// POST to backend DB to save
return fetch("/api/add-coin", settings)
.catch(err => {
return err
})
}
clickHandler = () => {
this.addToWatchlist().then(() => {
this.props.addWishlist()
})
}

Related

Toggle specific item but not others when using map() and onClick() in React

I've created a project where I call an API to list all of the countries in the world and some facts about them. I have each country on a different card, and I want the card to say the country's name on the front and then flip to the back and show the country's continent and language when the user clicks the card. The problem is that when the user clicks the card, all of the cards flip. I realize now that I need to use id or something similar to target specific cards, but I can't figure out how.
What I've tried: There are various versions of this question on here and elsewhere, but often the code is much longer and it's harder to follow the advice, and some of those deal with changing the CSS or situations different from what I'm trying to do. I tried to create a state of 'clickedArray' to create an array that shows true of false for if any specific index is clicked, but I couldn't figure out where I could call a method that fills that array, and also I don't know if that's the correct strategy anyway.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
clicked: false
}
}
componentDidMount() {
fetch('https://restcountries.eu/rest/v2/all')
.then(response => response.json())
.then(json => this.setState({data: json}));
this.setState({loading: false});
}
clickHappens = () => {
this.setState({clicked: this.state.clicked ? false : true});
}
render() {
return (
<div className="container">
{this.state.data?.length > 0 && this.state.data.map((item, id) => (
<div className="box" key={id} onClick={this.clickHappens}>
{this.state.clicked === false ?
<Countryname name={item["name"]}/>
:
<Countryinformation continent={item["subregion"]} language={item["languages"][0]["name"]} />
}
</div>
))}
</div>
)
}
}
class Countryname extends React.Component {
render() {
return (
<h1>{this.props.name}</h1>
)
}
}
class Countryinformation extends React.Component {
render() {
return (
<>
<p>{this.props.continent}</p>
<p>{this.props.language}</p>
</>
)
}
}
export default App;
This is because you are using a single state value to queue all the elements from if they are clicked or not.
If you want to toggle a single card at a time, use the index to match by.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
clicked: null // <-- start with null state
}
}
componentDidMount() {
fetch('https://restcountries.eu/rest/v2/all')
.then(response => response.json())
.then(data => this.setState({ data }))
.finally(() => this.setState({ loading: false }));
}
clickHappens = (id) => () => {
this.setState(prevState => ({
clicked: prevState.clicked === id ? null : id, // <-- toggle back to null or to new id
}));
}
render() {
return (
<div className="container">
{this.state.data?.map((item, id) => (
<div
className="box"
key={id}
onClick={this.clickHappens(id)} // <-- pass id to toggle
>
{this.state.clicked === id ? // <-- check id match
<Countryinformation
continent={item["subregion"]}
language={item["languages"][0]["name"]}
/>
:
<Countryname name={item["name"]}/>
}
</div>
))}
</div>
)
}
}
If you want to toggle multiple then use a map object to store the ids.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
clicked: {} // <-- start with empty object
}
}
componentDidMount() {
fetch('https://restcountries.eu/rest/v2/all')
.then(response => response.json())
.then(data => this.setState({ data }))
.finally(() => this.setState({ loading: false }));
}
clickHappens = (id) => () => {
this.setState(prevState => ({
clicked: {
...prevState.clicked,
[id]: !prevState.clicked[id], // <-- toggle clicked boolean
},
}));
}
render() {
return (
<div className="container">
{this.state.data?.map((item, id) => (
<div
className="box"
key={id}
onClick={this.clickHappens(id)} // <-- pass id to toggle
>
{this.state.clicked[id] ? // <-- check id match
<Countryinformation
continent={item["subregion"]}
language={item["languages"][0]["name"]}
/>
:
<Countryname name={item["name"]}/>
}
</div>
))}
</div>
)
}
}

How to render extra components inside a component after button click?

I have blogposts that I need to render. The first 4 are shown. When clicking on the button underneath it, two more need to show up. When the button is clicked again, two more need to show up and so on.
Unfortunately, I am not able to do so.
Here is my code:
import React from 'react';
import axios from 'axios';
import Blogpost from './Blogpost.js';
class BlogpostReader extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
blogposts: [],
};
}
componentDidMount() {
// Loading all blogposts in state
}
renderBlogpost(i) {
// Render one blogpost
}
//This function has to be replaced by one that renders extra blogposts
showAlert(){
alert("Im an alert");
}
render() {
const {error, isLoaded} = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
for (let i = 1; i < this.state.blogposts.length && i < 5; i++) {
this.state.renderedBlogposts.push(
<div key={this.state.blogposts[i].id} className="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 whole-blogpost">
{this.renderBlogpost(this.state.blogposts[i])}
</div>)
}
return (
<div>
<div className="row">
{this.state.renderedBlogposts}
</div>
<div className="centered-button">
<button className="styled-button" onClick={this.showAlert}>Meer laden</button>
</div>
</div>
);
}
}
}
export default BlogpostReader;
How can I show extra blogposts after clicking the button? Please help me out!
You can do something like this :
import React from 'react';
import axios from 'axios';
import Blogpost from './Blogpost.js';
class BlogpostReader extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
blogposts: [],
count:5
};
}
componentDidMount() {
// Loading all blogposts in state
if(blogposts.length<5){
this.setState({
count:blogposts.length
})
}
}
renderBlogpost(i) {
// Render one blogpost
}
renderBlogposts(){
const blogposts=[];
const count=this.state.count;
for (let i = 1; i < count; i++) {
blogposts.push(
<div key={this.state.blogposts[i].id} className="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 whole-blogpost">
{this.renderBlogpost(this.state.blogposts[i])}
</div>)
}
return blogposts;
}
//This function has to be replaced by one that renders extra blogposts
addMore=()=>{
let newCount=this.state.count + 2;
if(this.state.count===this.state.blogposts.length) return;
if(this.state.count+1 === this.state.blogposts.length){
newCount=this.state.count+1
}
this.setState({
count:newCount
})
}
render() {
const {error, isLoaded} = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
}
return (
<div>
<div className="row">
{this.renderBlogposts()}
</div>
<div className="centered-button">
<button className="styled-button" onClick={this.addMore}>Meer laden</button>
</div>
</div>
);
}
}
}
Oh no honey. In React the recommended approach is to make things as declarative as possible. Which means that instead of imperatively pushing items onto an array and then render that array you can just render a slice of the array.
I.e. try something like this
class BlogpostReader extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
blogposts: [], // this will contain all posts
postsToShow: 2, // a simple number controls how many posts are shown
};
}
componentDidMount() {
// Loading all blogposts in state
}
increasePostsShown() {
this.setState(({ postsToShow }) => {
postsToShow: postsToShow + 1;
});
}
render() {
const { error, isLoaded, blogposts, postsToShow } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
}
const postsShown = blogposts.slice(0, postsToShow); // get only the ones you want to show
return (
<div>
<div className="row">
{postsShown.map(blog => (
<div>{blog}</div> {/* or display them however you like */}
))}
</div>
<div className="centered-button">
<button className="styled-button" onClick={this.increasePostsShown}>
Meer laden
</button>
</div>
</div>
);
}
}
Is your blogpost array containing already all the blog posts? My suggestion would be that everytime the user clicks on the button, you increment a value from the state.
this.state = {
error: null,
isLoaded: false,
blogposts: [],
nbPostToDisplay: 4
};
In your loop:
for (let i = 0 /* start at 0, not 1 */; i < this.state.blogposts.length && i < nbPostToDisplay; i++) {
Some function to increment:
function incrementNbPosts() {
this.setState(prevState => return ({nbPOstsToDisplay: prevState.nbPostsToDisplay + 2});
}
Use function above in your button callback. This will trigger a re-render of your component.
IMPORTANT: do not forget to bind your functions in the construrtor or (better) use ES6 notation.
I would better keep things simple, so that button would just set new state.posts with +1 post, thus triggering render(), that in turn will render added element.
addPost = () => {
...
this.setState({
posts: [...posts, { id: posts.length + 1 }]
});
};
renderPosts = () => {
...
};
render() {
return (
<div>
<button onClick={this.addPost}>Add</button>
{this.renderPosts()}
</div>
);
}
Made a quick sandbox illustrating provided code.
https://codesandbox.io/embed/vjlp468jk7
Here's all you need. I also cleaned up your code a little bit
class BlogpostReader extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
blogposts: [],
limit: 4
};
this.showMore = this.showMore.bind(this);
this.renderBlogpost = this.renderBlogpost.bind(this);
}
componentDidMount() {
// Loading all blogposts in state
}
renderBlogpost(i) {
// Render one blogpost
}
//This function has to be replaced by one that renders extra blogposts
showMore() {
this.setState(state => ({ limit: state.limit + 2 }));
}
render() {
const { error, isLoaded, blogpost, limit } = this.state;
if (error) {
return <div>Error: {error.message}</div>);
}
if (!isLoaded) {
return <div>Loading...</div>;
}
return (
<div>
<div className="row">
{
blogposts.map((post, index) => {
if (index + 1 !== limit) {
return (
<div key={post.id} className="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 whole-blogpost">
{ this.renderBlogpost(post) }
</div>
);
}
})
}
</div>
<div className="centered-button">
<button className="styled-button" onClick={this.showMore}>Meer laden</button>
</div>
</div>
);
}
}
If you want to also make showMore to accept any number of posts, you can do this...
showMore(value = 2) {
this.setState(state => ({ limit: state.limit + value }));
}
Then now you can call it with any number of posts you want. If you don't specify any value, the limit will be incremented by 2.
UPDATE
Since you've mentioned that you have to start when index is 1, then you can update your blogposts.map in the render like this
{
blogposts.map((post, index) => {
if (index && index !== limit) {
// the condition above ensures that the posts at index 0, and also when index equals limit, are not returned
// the rest of the code goes here...
}
})
}
After doing that, you can set limit to 5 in the constructor if you want to show only 4 entries at first load.
The following was the working code:
class BlogpostReader extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
blogposts: [],
limit: 5,
start: 1
};
this.showMore = this.showMore.bind(this);
}
componentDidMount() {
// Loading all blogposts into state
}
renderBlogpost(i) {
// Render a single blogost
}
showMore(){
this.setState(state => ({
start: state.limit,
limit: state.limit + 2
}));
}
render() {
const {error, isLoaded, limit} = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
var startedAt = this.state.start
for (startedAt; startedAt < this.state.blogposts.length && startedAt < limit; startedAt++) {
this.state.renderedBlogposts.push(
<div key={this.state.blogposts[startedAt].id} className="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 whole-blogpost">
{this.renderBlogpost(this.state.blogposts[startedAt])}
</div>
)
}
return (
<div>
<div className="row">
{this.state.renderedBlogposts}
</div>
<div className="centered-button">
<button className="styled-button" onClick={this.showMore}>Meer laden</button>
</div>
</div>
);
}
}
}

Can't render parent props

I'm making a React app using openweathermap API. Right now I receive the list of weather data. I'm trying to highlight the weather if I click it.
To make this happen, I wrote on App.js to pass a prop to WeatherDetail.js, but so far seems like WeatherDetail.js doesn't recognize props from its parent.
class App extends React.Component {
constructor(props) {
super(props);
}
state = { forecasts: [], selectedWeather: null }
getWeather = async city => {
const response = await weather.get('/forecast', {
params: {
q: city
}
});
this.setState ({
forecasts: response.data.list,
city: response.data.city.name,
selectedWeather: response.data.list[0]
})
}
}
onWeatherSelectFunction = (item) => {
this.setState({ selectedWeather: item });
};
render() {
return (
<div>
<Form loadWeather={this.getWeather} />
<WeatherDetail itemToChild={this.state.selectedWeather} />
<WeatherList
onWeatherSelect={this.onWeatherSelectFunction}
weathers={this.state.forecasts}
city={this.state.city}
/>
</div>
);
}
}
export default App;
const WeatherDetail = ({forecasts, itemToChild}, props) => {
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
const WeatherItem = ({item, onWeatherSelectFromList, humidity, city, temp }) => {
return (
<div>
<div onClick={() => onWeatherSelectFromList(item)} >
{city}<br /> <-- Appears on screen
{humidity}<br /> <-- Appears on screen
</div>
</div>
);
};
const WeatherList = ({weathers, onWeatherSelect, city}) => {
const renderedList = weathers.map((item) => {
return (
<div>
<WeatherItem
city={city}
temp={item.main.temp}
humidity={item.main.humidity}
temperature={item.weather.icon}
onWeatherSelectFromList={onWeatherSelect}
/>
</div>
);
});
return (
<div className="flex">
{renderedList}
</div>
);
}
class Form extends React.Component {
state = { term: '' };
onFormSubmit = (event) => {
event.preventDefault();
this.props.loadWeather(this.state.term);
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<input
ref="textInput"
type="text"
value={this.state.term}
onChange={event => this.setState({term: event.target.value})}
/>
<button>Get Weather</button>
</form>
</div>
);
}
}
How do I connect App.js and WeatherDetail.js using props?
In your App.js file you are passing only one props called itemToChild
<WeatherDetail itemToChild={this.state.selectedWeather} />
In your WeatherDetail file from where you're getting forecasts? do you get forecasts from redux store?
const WeatherDetail = ({forecasts, itemToChild}, props) => {
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
change your code with this.
const WeatherDetail = (props) => {
console.log("props.itemToChild", props.itemToChild) // check this console that do you get data as you want.
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
You have already destructured the props so there is no need to mention props in WeatherDetail component
and also there is an extra parenthesis after the return statement you should remove that also...
Old:
const WeatherDetail = ({forecasts, itemToChild}, props) => {
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
New:
const WeatherDetail = ({ forecasts, itemToChild }) => {
const weather = itemToChild;
if (!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div>;
};

ReactJS - passing data from a child component to its parent

I have the following structure of components in the application:
class Car extends Component {
constructor() {
super();
this.state = {
cars: [],
...
}
}
componentDidMount() {
axios.get('/api/cars')
.then((response) => {
this.setState({cars: response.data});
console.log('cars: ', cars);
}).catch(err => {
console.log('CAUGHT IT! -> ', err);
});
}
render() {
return (
...
<CarAddNew />
<CarSearch />
<CarList cars={this.state.cars} />
)
}
}
and then
export default class CarSearch extends Component {
constructor(){...}
handleSearchSubmit(e) {
e.preventDefault();
..
axios.post('/api/cars/search', searchCars)
.then(response => {
console.log('response.data: ', response.data);
})
}
render() {
return(
... search form ...
)
}
When I search data in the database through the CarSearch component, it will fetch and load the right data, that's great. However, how do I pass this "new" found data to the CarList component, so I can display the on the page?
What I would do is the following:
class Car extends Component {
constructor() {
super();
this.state = {
cars: [],
...
}
}
componentDidMount() {
axios.get('/api/cars')
.then((response) => {
this.setState({cars: response.data});
console.log('cars: ', cars);
}).catch(err => {
console.log('CAUGHT IT! -> ', err);
});
}
handleSearch = () => {
axios.post('/api/cars/search', searchCars) // not sure where you are getting searchCars from, but you should get the idea
.then(response => {
this.setState({cars: response.data})
console.log('response.data: ', response.data);
})
}
render() {
return (
...
<CarAddNew />
<CarSearch onSearch={this.handleSearch} />
<CarList cars={this.state.cars} />
)
}
}
export default class CarSearch extends Component {
constructor(){...}
handleSearchSubmit(e) {
e.preventDefault();
this.props.onSearch() // I'm assuming you probably want to pass something here
}
render() {
return(
... search form ...
)
}
One option is to propagate the data up through a prop on CarSearch. Consider the (truncated) example...
handleSearchSubmit(e) {
e.preventDefault();
axios.post('/api/cars/search', searchCars).then(response => {
this.props.onData(response.data);
});
}
where, onData calls back up to the following (then later setting state)...
constructor() {
// [...]
this.onSearchResult = this.onSearchResult.bind(this);
}
onSearchResult(cars) {
this.setState({cars}); // results from CarSearch
}
render() {
return (
<CarAddNew />
<CarSearch
onData={this.onSearchResult} />
<CarList
cars={this.state.cars} />
)
}

React - onChange function 'this.state' is undefined

I'm experimenting with React and I'm trying to create a Search to filter a list of items. I have two components, the main one displaying the list of items which calls the Search component.
I have an onChange function that sets the term in the state as the input value and then calls searchItems from the main component to filter the list of items. For some reason in searchItems, this.state is undefined. I thought adding bind to onInputChange in the Search component would sort it out but it did not make any difference. Maybe there's something I'm missing.
Main Component
import React, { Component } from 'react';
import _ from 'lodash';
import Search from './search';
class Items extends Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("[url].json")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result
});
}
),
(error) => {
this.setState({
isLoaded: true,
error
})
}
}
searchItems(term) {
const { items } = this.state;
const filtered = _.filter(items, function(item) {
return item.Name.indexOf(term) > -1;
});
this.setState({ items: filtered });
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
}
else if (!isLoaded) {
return <div>Loading...</div>;
}
else {
return (
<div>
<Search onSearch={this.searchItems}/>
<ul>
{items.map(item => (
<li key={item.GameId}>
{item.Name}
</li>
))}
</ul>
</div>
)
}
}
}
export default Items;
Search Component
import React, { Component } from 'react';
class Search extends Component {
constructor(props) {
super(props);
this.state = {
term: ''
};
}
render() {
return (
<div>
<input type="text" placeholder="Search" value={this.state.term} onChange={event => this.onInputChange(event.target.value)} />
</div>
);
}
onInputChange(term) {
this.setState({ term });
this.props.onSearch(term);
}
}
export default Search;
You didn't bind searchItems() in the Items component.
Try changing it to an arrow function:
searchItems = () => {
// blah
}
or otherwise binding it in the constructor():
constructor() {
// blah
this.searchItems = this.searchItems.bind(this);
}
or when you call it.
You can read more about this here.

Categories