Display data from an API when clicking a button. Using ReactJS - javascript

I am trying to display the data of each character when I click the Info Button.
I know it is because in the onButtonClickHandler function it can not see the state. I have also tried this.state.person but it gives me an error saying "can not read state". And if I try just state.person it will give me "undefined".
What is the best way to do that? Thank you
API Link: https://swapi.dev/people/
import React from "react";
export default class FetchActors extends React.Component {
state = {
loading: true,
person: null
};
async componentDidMount() {
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
this.setState({ person: data.results, loading: false });
}
render() {
if (this.state.loading) {
return <div>loading...</div>;
}
if (!this.state.person.length) {
return <div>didn't get a person</div>;
}
function onButtonClickHandler(state) {
console.log(state.person);
};
return (
<div>
<h1>Actors</h1>
{this.state.person.map(person =>(
<div>
<div>
{person.name}
<button onClick={onButtonClickHandler}>Info</button>
</div>
</div>
))}
<button onClick={onButtonClickHandler}>Enter</button>
</div>
);
}
}

Please correct me if I'm wrong
The most likely reason why you are seeing this is because of the way javascript internally works. The syntax:
function xyz() {
}
has an implicit this
Maybe try changing your code from:
function onButtonClickHandler(state) {
console.log(state.person);
};
to:
const onButtonClickHandler = () => {
console.log(this.state.person);
};
Further Reading: Here

You have defined your function onButtonClickHandler as a function that takes one argument, and logs the person property of that argument. The argument state in your function has nothing to do with the state of your component. As javascript sees it, they are two totally unrelated variables which just happen to have the same name.
function onButtonClickHandler(state) {
console.log(state.person);
};
When button calls onClick, it passes the event as the argument. So your onButtonClickHandler is logging the person property of the event, which obviously doesn't exist.
Since you are not using any information from the event, your function should take no arguments. As others have said, you should also move this function outside of the render() method so that it is not recreated on each render. The suggestion to use bind is not necessary if you use an arrow function, since these bind automatically.
export default class FetchActors extends React.Component {
/*...*/
onButtonClickHandler = () => {
console.log(this.state.person);
};
}
Inside render()
<button onClick={this.onButtonClickHandler}>Enter</button>
You could also define the function inline, as an arrow function which takes no arguments:
<button onClick={() => console.log(this.state.person)}>Enter</button>
If you are new to react, I recommend learning with function components rather than class components.
Edit:
Updating this answer regarding our comments. I was so caught up in explaining the errors from doing the wrong thing that I neglected to explain how to do the right thing!
I am trying to display the data of each character when I click the Info Button.
Once we call the API, we already have the info loaded for each character. We just need to know which one we want to display. You can add a property expanded to your state and use it to store the index (or id or name, whatever you want really) of the currently expanded item.
When we loop through to show the name and info button, we check if that character is the expanded one. If so, we show the character info.
Now the onClick handler of our button is responsible for setting state.expanded to the character that we clicked it from.
{this.state.person.map((person, i) =>(
<div>
<div>
{person.name}
<button onClick={() => this.setState({expanded: i})}>Info</button>
{this.state.expanded === i && (
<CharacterInfo
key={person.name}
person={person}
/>
)}
</div>
CodeSandbox Link

there are a few ways you can resolve your issue; I'll give you the more common approach.
You want to define your click handler as a class (instance) method, rather than declare it as a function inside the render method (you can define it as a function inside the render method, but that's probably not the best way to do it for a variety of reasons that are out of scope).
You will also have to bind it's 'this' value to the class (instance) because click handlers are triggered asynchronously.
Finally, add a button and trigger the fetch on click:
class Actors extends React.Component {
state = {
loading: false,
actors: undefined,
};
constructor(props) {
super(props);
this.fetchActors = this.fetchActors.bind(this);
}
async fetchActors() {
this.setState({ loading: true });
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
this.setState({ actors: data.results, loading: false });
}
render() {
console.log('Actors: ', this.state.actors);
return <button onClick={this.fetchActors}>fetch actors</button>;
}
}

Sometimes i takes react a min to load the updated state.
import React from "react";
export default class FetchActors extends React.Component {
state = {
loading: true,
person: null
};
async componentDidMount() {
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
if(!data.results) { // throw error }
this.setState({ person: data.results, loading: false }, () => {
console.log(this.state.person) // log out your data to verify
});
}
render() {
if (this.state.loading || !this.state.person) { // wait for person data
return <div>loading...</div>;
}else{
function onButtonClickHandler(state) { // just make a componentDidUpdate function
console.log(state.person);
};
return (
<div>
<h1>Actors</h1>
{this.state.person.map(person =>(
<div>
<div>
{person.name}
<button onClick={onButtonClickHandler}>Info</button>
</div>
</div>
))}
<button onClick={onButtonClickHandler}>Enter</button>
</div>
);
}
}}

Related

How to call data setState to in react render return?

I can call data in console.log(this.state.eventUser); render return and its showing all of the data in eventUser. But when I try to call console.log(this.state.eventUser._id); its showing error of this Uncaught TypeError: Cannot read property '_id' of undefined. How can I solve this issue?
componentDidMount(){
Tracker.autorun(() => {
Meteor.subscribe('allUsers');
const userId = this.props.location.state.event.userID;
const eventUser = Meteor.users.findOne({_id: userId});
this.setState({ eventUser });
});
}
render(){
return(
<div>
{console.log(this.state.eventUser._id)}
</div>
);
}
Probably at some point this.state.eventUser in the render is undefined.
Try this
{this.state.eventUser && console.log(this.state.eventUser._id)
You can't use console.log inside JSX without enclosing in {}
Try below code
render(){
return(
<div>
{put javascript here}
</div>
);
}
Edit:
Your code is executing query on Database which is asynchronous
So you need to wait for it to complete.
Below is an implementation of async/await (es7+ features of javascript)
Try below code
componentDidMount(){
Tracker.autorun(async () => {
Meteor.subscribe('allUsers');
const userId = this.props.location.state.event.userID;
const eventUser = await Meteor.users.findOne({_id: userId});
this.setState({ eventUser });
});
}
It looks like by the time your render() function runs, the user hasn't yet be saved to the component state.
To fix this, add a constructor to your component in which you define the eventUser as an empty object.
class MyClass extends Component {
constructor(props) {
super(props);
this.state {
eventUser: {}
};
}
componentDidMount() {
Tracker.autorun(() => {
Meteor.subscribe('allUsers');
const userId = this.props.location.state.event.userID;
const eventUser = Meteor.users.findOne({_id: userId});
this.setState({ eventUser });
});
}
render(){
console.log(this.state.eventUser._id)
return(
<div>My Test Page</div>
);
}
}
Hope that helps!
componentDidMount will fire after the component has already been mounted (as the name states). At the point where it reaches the enclosed statement the state has not been set yet.
I'm not sure why you are using a console.log inside of your render return, but if you are looking to actually display the user ID, conditional rendering is your friend: {this.state.eventUser && this.state.eventUser._id}
For the first time when your component will render, if you have knowledge about react lifecycle hooks, after constructor the render method will be triggered then componentDidMount hook.
so for the first render the eventUser is undefined then after componentDidMount the state will be fulfilled.
So solution is:
First don't put console.log in JSX, it is better to put it before return.
Second first check if the object is defined then return your data
Code:
render(){
const { eventUser } = this.state;
// if eventUser is defined then log it
{eventUser && console.log(eventUser._id)}
// or you can add very simple load state
if(!eventUser) return <h1>Loading....</h1>
return(
<div>
{probably info about user }
</div>
);
}

How to make an API call on props change?

I'm creating a hackernews-clone using this API
This is my component structure
-main
|--menubar
|--articles
|--searchbar
Below is the code block which I use to fetch the data from external API.
componentWillReceiveProps({search}){
console.log(search);
}
componentDidMount() {
this.fetchdata('story');
}
fetchdata(type = '', search_tag = ''){
var url = 'https://hn.algolia.com/api/v1/search?tags=';
fetch(`${url}${type}&query=${search_tag}`)
.then(res => res.json())
.then(data => {
this.props.getData(data.hits);
});
}
I'm making the API call in componentDidMount() lifecycle method(as it should be) and getting the data correctly on startup.
But here I need to pass a search value through searchbar component to menubar component to do a custom search. As I'm using only react (not using redux atm) I'm passing it as a prop to the menubar component.
As the mentioned codeblock if I search react and passed it through props, it logs react once (as I'm calling it on componentWillReceiveProps()). But if I run fetchData method inside componentWillReceiveProps with search parameter I receive it goes an infinite loop. And it goes an infinite loop even before I pass the search value as a prop.
So here, how can I call fetchdata() method with updating props ?
I've already read this stackoverflow answers but making an API call in componentWillReceiveProps doesn't work.
So where should I call the fetchdata() in my case ? Is this because of asynchronous ?
Update : codepen for the project
You can do it by
componentWillReceiveProps({search}){
if (search !== this.props.search) {
this.fetchdata(search);
}
}
but I think the right way would be to do it in componentDidUpdate as react docs say
This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).
componentDidMount() {
this.fetchdata('story');
}
componentDidUpdate(prevProps) {
if (this.props.search !== prevProps.search) {
this.fetchdata(this.props.search);
}
}
Why not just do this by composition and handle the data fetching in the main HoC (higher order component).
For example:
class SearchBar extends React.Component {
handleInput(event) {
const searchValue = event.target.value;
this.props.onChange(searchValue);
}
render() {
return <input type="text" onChange={this.handleInput} />;
}
}
class Main extends React.Component {
constructor() {
this.state = {
hits: []
};
}
componentDidMount() {
this.fetchdata('story');
}
fetchdata(type = '', search_tag = '') {
var url = 'https://hn.algolia.com/api/v1/search?tags=';
fetch(`${url}${type}&query=${search_tag}`)
.then(res => res.json())
.then(data => {
this.setState({ hits: data.hits });
});
}
render() {
return (
<div>
<MenuBar />
<SearchBar onChange={this.fetchdata} />
<Articles data={this.state.hits} />
</div>
);
}
}
Have the fetchdata function in the main component and pass it to the SearchBar component as a onChange function which will be called when the search bar input will change (or a search button get pressed).
What do you think?
Could it be that inside this.props.getData() you change a state value, which is ultimately passed on as a prop? This would then cause the componentWillReceiveProps function to be re-called.
You can probably overcome this issue by checking if the search prop has changed in componentWillReceiveProps:
componentWillReceiveProps ({search}) {
if (search !== this.props.search) {
this.fetchdata(search);
}
}

State updates but does not render

I am new to ReactJS and am unsuccessfully attempting to manage a state change. The initial state renders as expected, the state successfully changes, however the elements do not render afterwards. There are no errors in the DOM console to go off of. I've made sure to set the initial state in the constructor of the component class, and I've also tried binding the method I'm using in the constructor since I've read auto-binding is not a part of ES6. The relevant component code is as follows:
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
myIDs: Array(6).fill('0')
};
this.getMyIDs = this.getMyIDs.bind(this);
};
componentDidMount() {
var ids = this.getMyIDs();
ids.then((result)=> {
this.setState({ myIDs: result }, () => {
console.log(this.state.myIDs)
});
})
};
componentWillUnmount() {
this.setState({
myIDs: Array(6).fill('0')
});
};
getMyIDs() {
return fetch('/api/endpoint').then((response) =>{
return response.json();
}).then((myIDs) => {
return myIDs.result
})
};
render() {
return (
<Tweet tweetId={this.state.myIDs[0]} />
<Tweet tweetId={this.state.myIDs[1]} />
);
}
}
export default MyComponent
UPDATE: The 'element' being updated is the 'Tweet' component from react-twitter-widgets. Its source is here:
https://github.com/andrewsuzuki/react-twitter-widgets/blob/master/src/components/Tweet.js'
export default class Tweet extends React.Component {
static propTypes = {
tweetId: PropTypes.string.isRequired,
options: PropTypes.object,
onLoad: PropTypes.func,
};
static defaultProps = {
options: {},
onLoad: () => {},
};
shouldComponentUpdate(nextProps) {
const changed = (name) => !isEqual(this.props[name], nextProps[name])
return changed('tweetId') || changed('options')
}
ready = (tw, element, done) => {
const { tweetId, options, onLoad } = this.props
// Options must be cloned since Twitter Widgets modifies it directly
tw.widgets.createTweet(tweetId, element, cloneDeep(options))
.then(() => {
// Widget is loaded
done()
onLoad()
})
}
render() {
return React.createElement(AbstractWidget, { ready: this.ready })
}
}
As in React docs:
componentWillMount() is invoked just before mounting occurs. It is
called before render(), therefore calling setState() synchronously in
this method will not trigger an extra rendering. Generally, we
recommend using the constructor() instead.
Avoid introducing any side-effects or subscriptions in this method.
For those use cases, use componentDidMount() instead.
you should not use ajax calls in componentWillMount
call ajax inside: componentDidMount
another thing: why do you use
componentWillUnmount
the object will be removed no reason to have that call there.
The only issue that is present in your current code is that you are returning multiple Element component instances without wrapping them in an array of a React.Fragment or a wrapper div. With the latest version of react, you must write
render() {
return (
<React.Fragment>
<Element Id={this.state.myIDs[0]} />
<Element Id={this.state.myIDs[1]} />
</React.Fragment>
);
}
}
Also as a practice you must have your Async calls in componentDidMount instead of componentWillMount as the React docs also suggest. You might want to read this answer on where write async calls in React for more details
Another thing that you must remember while using prop Id in your Element component is that componentWillMount and componentDidMount lifecycle functions are only called on the initial Render and not after that, so if you are using this.props.Id in one of these function in Element component then you will not be able to see the update since the result of async request will only come later, check this answer on how to tacke this situation

How can I have different data in different ReactJS Component instances?

Let's say I have 2 <Logs/> components. They're only displayed when their parent <Block/> has been clicked. Each <Block/> only has one <Logs/> but there will be many <Blocks/> on the page.
class Block extends React.Component {
toggleExpanded() {
this.setState({
isExpanded: !this.state.isExpanded
});
}
render() {
let blockId = // ...
return (
<div>
<button type="button" onClick={() => this.toggleExpanded()}>Toggle</button>
{this.state.isExpanded && <Logs blockId={blockId} />}
</div>
);
}
}
export default Block;
As the <Logs/> is created, I want to get data from the server using Redux. There could be a lot of <Logs/> some day so I don't want to load in all data to begin with, only data as needed.
class Logs extends React.Component {
componentDidMount() {
if (!this.props.logs) {
this.props.fetchBlockLogs(this.props.blockId);
}
}
render() {
return (this.props.logs && this.props.logs.length)
? (
<ul>
{this.props.logs.map((log, i) => <li key={i}>{log}</li>)}
</ul>
)
: (
<p>Loading ...</p>
);
}
}
SupportLogs.defaultProps = {
logs: null
}
const mapStateToProps = (state) => ({
logs: state.support.logs
});
const mapDispatchToProps = (dispatch, ownProps) => ({
fetchBlockLogs: (blockId) => {
dispatch(ActionTypes.fetchBlockLogs(blockId));
}
});
export default connect(mapStateToProps, mapDispatchToProps)(SupportLogs);
Currently, as I toggle one parent <Block/> closed and then re-opened, the data is remembered. That's a nice feature - the data won't change often and a local cache is a nice way to speed up the page. My problem is that as I toggle open a second <Block/>, the data from the first <Logs/> is shown.
My question is: how can I load in new data for a new instance of <Logs/>?
Do I have to re-load the data each time and, if so, can I clear the logs property so that I get the loading animation back?
Maybe think about changing the logs in your store to be an object with logs loaded into it keyed by block id:
logs: {
'someBlockId': [
'some logline',
'some other logline'
]
]
Have your reducer add any requested logs to this object by block id, so as you request them they get cached in your store (by the way I really like that your component dispatches an action to trigger data fetching elsewhere as a side effect, rather than having the fetch data performed inside the component :) ).
This reducer assumes that the resulting fetched data is dispatched in an action to say it has received the logs for a particular block, {type: 'UPDATE_LOGS', blockId: 'nic cage', receivedLogsForBlock: ['oscar', 'winning', 'actor']}
logsReducer: (prevState, action) => {
switch(action.type)
case 'UPDATE_LOGS':
return Object.assign({}, prevState, {
[actions.blockId]: action.receivedLogsForBlock
})
default: return prevState
}
}
And in your component you can now look them up by id. If the required block is not defined in the logs object, then you know you need to send the action for loading, and can also show loader in meantime
class Logs extends React.Component {
componentDidMount() {
const {logs, blockId} = this.props
if (!logs[blockId]) {
this.props.fetchBlockLogs(blockId);
}
}
render() {
const {logs, blockId} = this.props
return (logs && logs[blockId]) ? (
<ul>
{logs[blockId].map((log, i) => <li key={i}>{log}</li>)}
</ul>
) : (
<p>Loading ...</p>
)
}
}
This has the added bonus of logs[blockId] being undefined meaning not loaded, so it is possible to distinguish between what needs to be loaded and what has already loaded but has empty logs, which your original would have mistaken for requiring a load

Functions inside componentDidMount are undefined

I have the following block of code:
class App extends Component {
constructor(props) {
super(props);
this.state = {
avatar: '',
...some more data...
}
this.fetchUser = this.fetchUser.bind(this);
}
render() {
return (
<div>
<SearchBox onSubmit={this.fetchUser}/>
<Card data={this.state} />
<BaseMap center={this.state.address}/>
</div>
);
}
componentDidMount() {
function fetchUser(username) {
let url = `https://api.github.com/users/${username}`;
this.fetchApi(url);
};
function fetchApi(url) {
fetch(url)
.then((res) => res.json())
.... some data that is being fetched ....
});
};
let url = `https://api.github.com/users/${this.state.username}`;
}
}
export default App;
However, I get a TypeError: Cannot read property 'bind' of undefined for the following line: this.fetchUser = this.fetchUser.bind(this); in the constructor where I bind the function to this.
How can I make the function, which is inside componentDidMount method, visible and available for binding?
EDIT:
The reason I'm putting the functions inside componentDidMount was because of a suggestion from another user on StackOverflow. He suggested that:
#BirdMars that because you don't really fetch the data in the parent,
and the state doesn't really hold the address object. call the fetch in
componentDidMount of the parent and update the state there. this will
trigger a second render that will pass in the new state with the
address object (the first render will be with an empty state.address
of course until you finish the fetch and update the state)
There is some fundamental misunderstanding here, you can still call the functions inside componentDidMount, however you should not define them inside it.
Simply define the functions outside componentDidMount and this will solve your problems, here is a short example
componentDidMount() {
this.fetchUser(this.state.username);
}
function fetchUser(username) {
let url = `https://api.github.com/users/${username}`;
this.fetchApi(url);
};
function fetchApi(url) {
fetch(url)
.then((res) => res.json())
.... some data that is being fetched ....
});
};
Its a simple matter of scope:
function outer(){
function inner(){
}
}
inner(); // error does not exist
As birdmars suggested you should call this.fetchUser() inside component did mount. but declare the function outside!
class App extends Component {
render() {
return (
<div>
<SearchBox onSubmit={this.fetchUser}/>
<Card data={this.state} />
<BaseMap center={this.state.address}/>
</div>
);
}
fetchUser(username) {
let url = `https://api.github.com/users/${username}`;
this.fetchApi(url);
};
fetchApi(url) {
fetch(url)
.then((res) => res.json())
.... some data that is being fetched ....
});
};
componentDidMount() {
let url = username; //frome somewhere, most probably props then use props Changed function instead!
var user = his.fetchUser(url)
this.setState(() => {user: user});
}
}
export default App;
If you declare the functions inside componentDidMount they are only visible in that scope and get be accessed by the component itself. Declare them in your component.
What he was talking about in this post was to call the functions from componentDidMount, but not to declare them in there.
"Another guy from StackOverflow" suggest you to call functions in componentDidMount() method, but not to define them there.
So you should do this instead
class App extends Component {
constructor(props) {
...
this.fetchUser = this.fetchUser.bind(this);
this.fetchApi= this.fetchApi.bind(this);
}
...
fetchUser(username) {...}
fetchApi(url) {...}
componentDidMount() {
this.fetchUser(...);
...
}
}

Categories