How to call API and render a new component - reactjs

What would be a way to call an API in a component class (do not use a functional component) and render the received data in a new component. I am asking for the order of operation to setState. My current solution includes setState and a function as a second argument that set anther state. Function getStockInfo calls API and then I want to render the GetQuote component switching showComponent to true. Note that in ComponentDidMount I call an initial API. Please let me know if this is a valid solution or there is a neater way.
class Portfolio extends Component {
constructor(props) {
super(props);
this.state = {
res: " ",
data: [],
showComponent: false,
};
this.showStockiaDetails = this.showStockDetails.bind(this);
this.getStockInfo = this.getStockInfo.bind(this);
this.handleClick = this.handleClick.bind(this);
}
async getStockInfo(item) {
const stock = item.symbol;
const API_KEY = "1D";
const PATH = `https://www.alphavantage.co/query?function=OVERVIEW&symbol=${stock}&apikey=${API_KEY}`;
await axios
.get(PATH)
.then((response) => {
this.setState({ res: response.data }, () => this.showStockDetails());
console.log(response.data);
})
.catch(function (error) {
console.log(error);
});
}
handleClick(item) {
this.getStockInfo(item);
}
showStockDetails() {
console.log("-->", this.state.res);
this.setState({ showComponent: true });
}
componentDidMount() {
axios
.get("http://127.0.0.1:8000/api/data")
.then((response) => {
this.setState({ data: response.data });
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div className="container">
<h1>Day Eight</h1>
<div className="row col-12">
<div className="col-md-5 col-lg-4 ">
<ul className="list-group">
{this.state.dayeightData.map((s) => (
<li
key={s.id}
onClick={() => this.handleClick(s)}
className="list-group-item"
aria-current="true"
>
{s.symbol}
</li>
))}
</ul>
</div>
<div className="col-md-7 col-lg-8 order-md-last">
{this.state.showComponent ? (
<GetQuote stock={this.state.res} />
) : null}
</div>
</div>
</div>
);
}
}
export default Portfolio;

You did it right. There would be many other alternatives too depending on what GetQuote component is rendering. To make it look a little bit more cleaner you can do something like this:
{
this.state.res ? <GetQuote stock={this.state.res} /> : ''
}
But make sure to initialize res as "" (without any space) as "" will be false and " " will be true.
In that way, you don't really have to call showStockDetails function.

Related

How to find all elements with text react-test-renderer

I have a component that makes a GET request and sends users their information. I need to test whether there is such an "li" with a certain text using a test. Now in my code the error is "promise returned from findByType query must be handled". How can I solve it?
describe("List Component", () => {
it("find li", () => {
const wrapper = renderer.create(<List />);
const testInstance = wrapper.root;
expect(testInstance.findByType("li")).toBeTruthy();
});
});
class List extends Component {
constructor(props) {
super(props);
this.state = {};
}
async componentDidMount() {
getUser(this.props.user)
.then((response) => {
this.setState(response.data);
})
.catch((error) => {
this.setState({ error: "request error" });
});
}
render() {
return (
<div>
<ul>
{Object.keys(this.state).map((i) => (
<li key={i}>
{i}: {this.state[i]}
</li>
))}
</ul>
</div>
);
}
}

Map doesn't work in render after fetch in React JS

I'm struggling with fetching data and render to the screen in React JS
class Home extends Component{
componentWillMount(){
foods=[];
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => res.json())
.then(data => foodlist=data)
.then(
() => console.log("f:",foodlist),
)
.then(
() => {foodlist.map(item => foods.push({title:item, img:"http://192.249.19.243:0280/main/image/"+item}));
console.log("foods", foods);
this.render();
}
);
}
componentDidMount(){
}
render(){
console.log("render in!");
return (
<div>
<ul>
{
console.log(foods), // this works fine -> 4 elements
foods.length!=0 ?
foods.map(item=>
<Item
title={item.title}
img={item.img}/>
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
in the render(), I checked console.log(foods) print 4 elements,
but Nothing appears in the screen..
I don't know why.. Please help me..
In react: it is not you who manage the render. If you want to render an element you need to call this.setState with the data that changed. You can see my example:
class Home extends Component{
state = {
foods: []
}
componentWillMount(){
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => res.json())
.then(data => foodlist=data)
.then(
() => console.log("f:",foodlist),
)
.then(
() => {
this.setState({foods: foodlist.map(item => ({title:item, img:"http://192.249.19.243:0280/main/image/"+item})));
}
);
}
componentDidMount(){
}
render(){
console.log("render in!");
return (
<div>
<ul>
{
this.state.foods.length!=0 ?
this.state.foods.map(item=>
<Item
title={item.title}
img={item.img}/>
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
It looks like you are relatively new to React. I spot quite a few errors with this.
Please read the docs on class based components carefully
I have tried to refactor it without context. Give it a bash
class Home extends Component {
//initialize state in the constructor for class based components
constructor(props) {
super(props);
//foods must be an empty array otherwise .length may fail
this.state = { foods: [] }
};
//once the component has mounted, call the method which will perform the fetch
componentDidMount() {
this.fetchFoodData();
}
//calls the endpoint which returns a promise. The promise will then set the components state, which will trigger a render
fetchFoodData = () => {
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => {
const foodData = res.json();
//not sure what your body looks like, but foods should be an array containing your food objects
const foods = foodData.map(item => foods.push({ title: item, img: "http://192.249.19.243:0280/main/image/" + item}));
//calling setState will cause react to call the render method.
this.setState({ foods: foods })
}).catch(err => {
//handle errors here
console.log(err);
});
};
//React calls this method when props or state change for this component
render() {
return (
<div>
<ul>
{
foods.length != 0 ?
foods.map(item =>
<Item
title={item.title}
img={item.img} />
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
Thats not the correct way to handle data in a react component. You should maintain list of foods in component state. Code sandbox: https://codesandbox.io/s/falling-bush-b9b78
As an example
import React from "react";
export default class App extends React.Component {
constructor() {
super();
this.state = {
foods: []
};
}
componentDidMount() {
const fetchMock = url =>
new Promise(resolve => {
setTimeout(() => resolve(["Barley", "Chicken", "Oats"]), 2000);
});
fetchMock("http://192.249.19.243:0280/main/get_recipes").then(foods => {
this.setState({
foods
});
});
}
render() {
console.log("render in!");
const { foods } = this.state;
return (
<div>
<ul>
{foods.length !== 0 ? (
foods.map(food => <h1 key={food}>{food}</h1>)
) : (
<p id="loadingMsg">Data Loading...</p>
)}
</ul>
</div>
);
}
}

how to iterate through json data from api in react.js

I just started learning React and am trying to loop through an array of JSON data. However, I am getting some syntax errors. I'm trying to use the array.map function, but it's not working properly, and I'm not exactly sure how to implement it to make it display each element in the JSON array instead of just one. Any help is greatly appreciated - thanks!
import React, { Component } from 'react';
import axios from "axios";
import './App.css';
import UserForm from "./components/UserForm.js";
class App extends Component {
state = {
name: "",
stars: "",
icon: "",
trails: [], isLoaded: false
}
getUser = (e) => {
e.preventDefault();
const address = e.target.elements.address.value;
if (address) {
axios.get(`https://www.hikingproject.com/data/get-trails?lat=40.0274&lon=-105.2519&maxDistance=10&key=200279581-dd891420fa2c470dbb683b34e017062a`)
.then((res) => {
console.log(res);
const trailList = res.data.trails.map((trail) => {
console.log(trail.name)
console.log(trail.stars)
return <div> <p>{trail.name}</p> </div>
})
this.setState({ trails: trailList, isLoaded: true });
const name = res.data.trails.name;
const stars = res.data.trails.stars;
const icon = res.data.trails.imgMedium;
this.setState({ name });
this.setState({ stars });
this.setState({ icon });
})
}
else return;
}
render() {
return (
<div>
<div className="App">
<header className="App-header">
<h1 className="App-title">HTTP Calls in React</h1>
</header>
<UserForm getUser={this.getUser} />
<div className="newmessage">
{this.state.trails.map((obj) => {
return(
<div>
<p>{obj.name}</p> >
<p> {obj.stars}</p>
</div>
);
}}
</div>
</div>
</div>
</div>
);
}
};
export default App;
A good start would be to fetch your data in the componentDidMount either with fetch or axios. Never used axios, so I am going to answer the question with fetch
Leave the constructor as it is. Then write a componentDidMount like so:
componentDidMount() {
fetch('https://www.hikingproject.com/data/get-trails?lat=40.0274&lon=-105.2519&maxDistance=10&key=200279581-dd891420fa2c470dbb683b34e017062a')
.then(res => res.json())
.then(data => this.setState({ trails: data.trails }))
.catch(e => console.log(e))
}
then in a sub-render method, such as renderData, write the following code:
renderData() {
if (!this.state.trails) {
return null;
}
return this.state.trails.map(trail => <p>{trail.name}</p>);
}
Then call {this.renderData()} in your render
render() {
return (
<div>{this.renderData()}</div>
)
}
This code has been tested on my local environment and it was working as it should.

Update Apollo store after filtered query

I have a list of items which can be filtered out using certain criteria. Whenever i perform a search, i want to grab a filtered item and update its content which seems to update the apollo store correctly, but for some reason changes are not being shown. Perhaps the componentWillReceiveProps lifecycle method is not being fired and i need to implement it by myself?
I tried updating the store manually using "update" after the mutation but it wont work also.
This is my code so far:
ClientList.js
class ClientList extends Component {
constructor(props) {
super(props);
this.state = {
hasMoreItems: true,
loading: false,
clients: [],
searchText: ''
}
}
_executeSearch = async () => {
const { searchText } = this.state;
this.setState({ loading: true });
const result = await this.props.client.query({
query: ALL_CLIENTS_QUERY,
variables: { searchText },
fetchPolicy: 'network-only'
})
this.setState({
clients: result.data.allClients,
loading: false
});
}
render() {
let { allClients, loading, fetchMore } = this.props.data;
const { hasMoreItems, clients, searchText } = this.state;
if (clients.length > 0) {
allClients = clients;
loading = this.state.loading;
}
return (
<section>
<h1 className="text-center">Clients</h1>
<InputGroup>
<InputGroupButton>
<Button onClick={() => this._executeSearch()}>I'm a button</Button>
</InputGroupButton>
<Input
onChange={(e) => this.setState({ searchText: e.target.value })}
placeholder="Search by social or comercial name"
/>
</InputGroup>
{loading ?
<div className="text-center mt-4">
<i className="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
</div>
: <div className="mt-3">
{allClients.map(client =>
<div key={`client-${client.id}`} className="client-content">
<Link to={`/clients/${client.id}`}>
<h1 className="mb-1">
{client.socialName}
<small className="text-muted ml-3">{client.comercialName}</small>
</h1>
</Link>
</div>
})
</div>
</section>
);
};
}
export default withApollo(graphql(ALL_CLIENTS_QUERY)(ClientList));
ClientEdit.js
class ClientEdit extends Component {
onSubmit = async (e) => {
e.preventDefault();
this.setState({ loading: true });
const payload = {
id: this.props.match.params.id,
rfc: this.state.rfc,
socialName: this.state.socialName,
legalRepresentative: this.state.legalRepresentative,
comercialName: this.state.comercialName
}
// Mutation updates the store but doesnt show results
const resp = await this.props.mutate({
variables: payload,
update: (store, { data: { updateClient } }) => {
// Tried updating but it doesnt show changes also;
}
});
}
}
export default compose(
graphql(GET_CLIENT_QUERY, {
options: props => ({
variables: {
id: props.match.params.id
}
})
}),
graphql(UPDATE_CLIENT_MUTATION)
)(ClientEdit);
better if we check that the data is ready and we can render it out , and when data still fetching so no render associated with that apollo data object should be done:
render(){
const {loading} = this.props.data;
if(loading) {return <div>Loading...</div>}
return (
...
)
I manage to fix this using the componentWillReceiveProps lifecycle method. I dont know if this is a bug or maybe there is another solution;
componentWillReceiveProps(newProps) {
const { allClients, loading } = newProps.data;
if (!loading) {
const clients = _.intersectionBy(allClients, this.state.clients, 'id');
this.setState({ clients });
}
}

Setting State Array and Appending Value on Update

I'm still learning about state and lifecycle with ReactJS and have run into a scenario where I have a form that on submit should save the form value and then append the returned JSON object to the end of an array which would re-render the component storing the original array.
With my current setup, I have the components setup and form submit with returned JSON object, but the state contains an empty array rather than the object spread {...comment} and it doesn't look like the setState is updating component, but that could be due to the empty array mentioned before. Can anyone point me in the right direction?
Comment:
import React from 'react';
import fetch from 'node-fetch';
//record Comment - Comment Form Handle POST
class CommentForm extends React.Component {
constructor(props){
super(props);
this.state = {
value: '',
comments: []
};
this.onChange = this.onChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
postComment(comment, recordId, csrfToken) {
var body = { comment: comment };
var route = 'http://localhost:3000/record/' + recordId + '/comment';
fetch(route,
{
method: 'POST',
body: JSON.stringify(body),
headers: {
'X-CSRF-Token': csrfToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
let commentsArr = this.state.comments;
this.setState({comments: commentsArr.concat(data)});
})
.catch(err => {
console.log(err);
});
}
onChange(e){
this.setState({
value: e.target.value
});
}
handleSubmit(e){
e.preventDefault();
this.postComment(this.state.value, this.props.recordId, this.props.csrf);
}
render(){
return (
<div className="record-comment__form">
<div className="row">
<form action={"/record/" + this.props.recordId + "/comment"} method="post" onSubmit={this.handleSubmit}>
<input type="hidden" name="_csrf" value={this.props.csrf}/>
<textarea name="comment" className="record-comment__form-text-area" onChange={e => this.setState({ value: e.target.value })} value={this.state.value}></textarea>
<button type="submit" className="record-comment__form-button" disabled={!this.state.value}>Comment</button>
</form>
</div>
</div>
)
}
}
//record Comment - Comment
const Comment = props => {
return (
<div className="row">
<div className="col-md-12">
<h5>{props.user_id}</h5>
<h4>{props.comment}</h4>
<h3>{props.synotate_user.fullNameSlug}</h3>
</div>
</div>
)
}
//record Comment - Container
export default class Comments extends React.Component {
render() {
return (
<div className="record-comment-container">
<CommentForm recordId={this.props.recordId} csrf={this.props.csrf}/>
{ this.props.record_comments.map((comment, i) =>
<Comment {...comment} key={this.props.recordCommentId}/>
)}
</div>
);
}
}
Record (Parent component)(Where Comment is being set):
//GET /api/test and set to state
class RecordFeedContainer extends React.Component{
constructor(props, context) {
super(props, context);
this.state = this.context.data || window.__INITIAL_STATE__ || {records: []};
}
fetchList() {
fetch('http://localhost:3000/api/test')
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
this.setState({ records: data.record, user: data.user, csrf: data.csrfToken });
})
.catch(err => {
console.log(err);
});
}
componentDidMount() {
this.fetchList();
}
render() {
return (
<div className="container">
<h2>Comments List</h2>
<RecordFeed {...this.state} />
</div>
)
}
};
//Loop through JSON and create Record and Comment Container Component
const RecordFeed = props => {
return (
<div>
{
props.records.map((record, index) => {
return (
<div className="row">
<div className="col-md-6 col-md-offset-3 record-card">
<RecordCard {...record} key={record.recordIdHash} user={props.user} />
<Comments {...record} key={index} recordId={record.recordIdHash} csrf={props.csrf}/>
</div>
</div>
);
})
}
</div>
)
}
Your problem is that when rendering <Comments>, the this.props.record_comments is not the comments you've updated in the state of the <CommentForm> component. Each component has it's own internal state.
You need to pass the state along to your <Comments> component. You will need to move your state up to the top level or use a state management system like Redux which will allow you to access a shared state which could contain your comments array.
From the top level component you could manage the state there, like so:
this.state = {
comments: [],
// other shared state
};
You can pass along an update comments function, named for example updateCommentsFunc() to <CommentForm> like so:
<CommentForm updateComments={this.updateCommentsFunc} recordId={this.props.recordId} csrf={this.props.csrf}/>
Which will allow you to pass the updated comments back up to the parent component via something like:
const updateCommentsFunc = (newComments) => {
this.setState({comments: [...this.state.comments, newComments]});
}
Your postComment() function doesn't appear to be properly bound to your enveloping <CommentForm/> component's this. As a result; calling this.setState() from within the function isn't really doing anything.
Try binding it within your constructor method.
constructor(props) {
// ...
this.postComment = this.postComment.bind(this)
}
Or by declaring it using an arrow function.
postComment = (comment, recordId, csrfToken) => {
// ...
}
See this article for more info on React binding patterns.

Resources