I have an object that looks something like this
messageData = { items: [{name:'1st msg' draft:{contact: endpoint}},
{name:'1st msg' draft:{contact: endpoint}},
{name:'1st msg' draft:{contact: endpoint}]}
I need to map over the items and do an api call that expands the data from the api endpoint and return them in a table row
renderMessageTableRow() {
let dataRef = this.state.messageData.items,
return(_.map(dataRef, (message) => {
// Get vars from message data including contact method endpoint
let id = message['#id'],
status = message.status,
draftData = message.draft.recipientAddress
return (
<tr>
<td>{id}</td>
<td>{status}</td>
</tr>
)
})
)
}
so far my function looks like this but draftData is an endpoint I need to expand and populate my table row with, I'm using React and Redux. I've been looking a libraries like async and co but not sure how I would plug this in my map function. I just need to call the endpoint make some variables from that data and populate the rest of my table with it. Any help would be much appreciated
There's a number of ways you can get data and store it for rendering (redux for example). Here's a simple example that just uses the component's state and makes the ajax calls on componentDidMount. Most likely you'd do this kind of thing out of the component, but this should give you a sense of some of the flow. It uses isomorphic fetch which you can get on NPM.
import React from 'react';
require('es6-promise').polyfill();
require('isomorphic-fetch');
const messageData = {
items: [
{ name: '1st msg', draft: { contact: 'http://something.something/api' } },
{ name: '1st msg', draft: { contact: 'http://something.something/api' } },
{ name: '1st msg', draft: { contact: 'http://something.something/api' } },
],
};
class MyComponent extends React.Component {
constructor() {
super();
this.state = { rows: undefined };
}
componentDidMount() {
// make an array of promises that are your api calls
const promises = messageData.items.map((item, i) => fetch(item.draft.contact).then(data => {
messageData.items[i].draft.data = data;
return data;
}));
// resolve all the api calls in parallel and populate the messageData object as they resolve
Promise.all(promises).then(responses => {
// messageData.items are populated now, but if you want to do anything additional, the data from the ajax calls is all here
console.log(responses);
// set the populated items in state
this.setState({ rows: messageData.items });
});
}
render() {
// conditionally populate the rows constant
const rows = (this.state.rows) ? this.state.rows.map(row => (
<tr>
<td>{row.draft.data.somePropFromApi}</td>
</tr>
)) : null;
return (
<table>
{rows}
</table>
);
}
}
export default MyComponent;
Related
Hello (I am learning React), I am working with an API that returns pictures of random people, however the problem I am having is when I use axios.get I am getting the response from the API, and I can see the results in the console, but when I try to access them it says "Cannot read properties of picture".
The thing I am making is that when the user press the input field it gets an URL of a random picture from the API and the value from that inputText changes to the URL, but it says "Cannot read properties of picture" when clicking on the input, but the API is returning me the data in the console.
Here is what my API returns me.
Here is my code.
class PersonasInsert extends Component {
urlPersonas = "https://randomuser.me/api/?inc=picture";
constructor(props) {
super(props);
this.state = {
peticionImagen: null,
name: "",
last: "",
image: "",
};
}
peticionImagenPersona = async () => {
await axios.get(this.urlPersonas).then(
(response) => {
this.setState({ peticionImagen: response.data.results });
},
(error) => {
console.log(error);
}
);
};
handleChangeImage = async (event) => {
this.peticionImagenPersona();
const peticionImagen = this.state.peticionImagen.picture.large
this.setState({ peticionImagen });
};
render() {
const { peticionImagen } = this.state;
return (
<Wrapper>
<Title>Insertar Persona</Title>
<Label>image: </Label>
<InputText
type="text"
value={peticionImagen}
readOnly
onClick={this.handleChangeImage}
/>
</Wrapper>
);
}
}
export default PersonasInsert;
Thank you in advance.
Does the API return an array of objects or a single object? It looks like an array from the log you posted, you will need to traverse the array through .map or if you want only the first element then do something like this: this.state.peticionImagen[0].picture.large
I need data from 2 different APIs, both support pagination.
How can I display the joined data in a table in a performant way?
I need to join on their id, but one API returns less data than the other, so I cannot simply match 1 by 1. I need to implement a filter.
Is the only way to brute force map data source A to B?
If the data comes from two different APIs and you are making to separate requests you have a number of options. My personal preference is to have state in your controller in which you map each response by id and then you can select the additional data by id:
import React, { useState, useEffect } from 'react';
import { keyBy } from 'lodash';
function TableComponent(props) {
// Destructure your props...I'm assuming you pass some id into fetch data
const { id } = props;
// State
const [tableData, setTableData] = useState([]);
// Load data when id changes
useEffect(() => {
fetchData()
}, [id]);
async function fetchData() {
// Get your data from each source
const apiData_A = await fetchDataFromAPI_A(id);
const apiData_B = await fetchDataFromAPI_B(id);
// Key each data set by result ids
const resultsMappedById_A = keyBy(apiData_A, 'id');
const resultsMappedById_B = keyBy(apiData_B, 'id');
// Combine data into a single set
// this assumes your getting same results from each api
const combinedDataSet = Object.keys(resultsMappedById_A)
.reduce((acc, key) => {
// Destructure results together, merging objects
acc.push({
...resultsMappedById_A[key],
...resultsMappedById_B[key]
});
return acc;
}, []);
setTableData(combinedDataSet);
}
async function fetchDataFromAPI_A(id) {
// Fetch your data and return results
}
async function fetchDataFromAPI_A(id) {
// Fetch your data and return results
}
function renderTableRow(data) {
return (
<tr>
<td>{data.id}</td>
<td>{data.apiAProp}</td>
<td>{data.apiBProp}</td>
</tr>
);
}
return (
<table>
{ tableDataSet.map(renderTableRow) }
</table>
);
}
Note, there are probably more efficient ways to do this depending on how you're fetching data and what the responses hold, but the concept provided here should do the trick assuming my assumptions are correct based on the information you have provided.
Long story short, I have a class component that constructs a poll. Before sending the data to the server I need to transform it a little so it fits the API request. I created a transformData method on my class component that transforms the data derived from the state. As a side effect it sets the data in separate this.state.data property so I can attach it with the API request. The problem is that the method mutates the other properties of the state.
transformData = () => {
const { title, sections } = this.state
const transformedSections = sections.map(section => {
delete section.isOpen
const transformedQuestions = section.questions.map(question => {
question.label = question.question
question.type = toUpper(question.type)
delete question.question
return question
})
section.questions = {
create: transformedQuestions,
}
return section
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
}
So I get this:
state: {
data: {...} //our transformed data
sections: {...} //transformed as well!!
}
instead of getting this:
state: {
data: {...} //our transformed data
sections: {...} //same before calling the method
I re-wrote the method with different approach — basically replaced all Array.map with Array.forEach and it worked as expected.
transformData = () => {
const { title, sections } = this.state
const transformedSections = []
sections.forEach(section => {
const transformedQuestions = []
section.questions.forEach(question => {
transformedQuestions.push({
label: question.question,
type: toUpper(question.type),
max: question.max,
min: question.min,
instruction: question.instruction,
isRequired: question.isRequired,
placeholder: question.placeholder,
})
})
transformedSections.push({
title: section.title,
questions: { create: transformedQuestions },
})
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
Can anyone explain what's going on here? How can I accidentally mutate a state property without explicitly calling this.setState on the aforementioned property? The thing is that the originally written method mutates the state even if I return the data object without calling this.setState whatsoever. Like so:
//This still mutates the state
return {
data: {
title,
sections: { create: transformedSections },
}
}
//without this!
//this.setState({
// data: {
// title,
// sections: { create: transformedSections },
// },
// })
Thanks!
javascript behave like this way,
its called variable referencing.
it works like pointer variable in C.
if your console those variable such as console.log(var1 == var2) it will show true cuz both references from same memory location
if you want to prevent mutate original variable then you have to create another brand new variable to mutate
like this way :
const { title, sections } = this.state
// create new variable following old one (spreading es6 way)
const tempSections = [...sections]
...
also
sections.forEach(section => {
const transformedQuestions = []
const tempQuestions = [...section.questions]
tempQuestions.forEach(question => {
...
always have to create a brand new variable of object/array/... to prevent auto mutation
for further info here
Issue here is of Shallow Copying :
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
// state is muate already
console.log("---- After map -----" , this.state);
You can run the below code snippet and check both console.log, and check for "isOpen": true
Hope this will clear all your doubts :
const { useState , useEffect } = React;
class App extends React.Component {
state = {
title : "questions" ,
sections : [{
isOpen : true ,
questions : ["que1" , "que2" , "que3"]
}]
}
transfromData = () => {
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
console.log("---- After map -----" , this.state);
}
render() {
return (
<div>
<button onClick={this.transfromData}>transfromData</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
You should never update the state without using the setState method. It is asyncronous, and if you don't set it properly you never know what might happen - and that's what you're seeing in the first part of your answer. See the docs
By doing
section.questions = {
create: transformedQuestions,
}
you are improperly altering the state, so you'll see this.state.sections transformed as well, because each element inside this.state.sections has now an attribute questions that contains create with the value transformedQuestions
I have been pounding my head against this problem, and need help with a solution. I have an array of IDs within a JSON, and I am iterating over that array and making a GET request of each ID. I want to then push the response of those GET requests into an array.
Here is the function I am using to push the registrations into the array. It is iterating through an array of IDs:
getRegistrations = async (event) => {
let registrations = [];
await event.registrations.forEach(registration => axios.get('/event/getRegistration', {
params: {
id: registration
}
}).then(res => {
registrations.push(res.data.properties)
}
).catch(err => console.log(err)));
return registrations;
};
Here is where I am calling that code:
render() {
let event = this.getEvent();
let registrations2 = [{
age: 19,
bio: 'test',
firstName: 'hello',
lastName: 'bye',
password: 'adadas',
telephone: "4920210213"
}];
if (this.props.listOfEvents.events.length !== 0 && !this.props.listOfEvents.gettingList && event) { //check if the array is empty and list has not been rendered yet
let columns = [];
let registrations = this.getRegistrations(event);
console.log(registrations);
let eventProperties = event.properties[0];
Object.keys(eventProperties).forEach(key => columns.push({
title: eventProperties[key].title,
dataIndex: key,
key: key
}));
console.log(registrations);
console.log(registrations2);
return (
<h1>hi</h1>
)
}
return <Loading/>
}
When I console-log 'registrations' vs 'registrations2' they should be very identical. However, in the javascript console on Google Chrome, 'registrations appears as '[]' where 'registrations2' appears as '[{...}]'.
I know that it is an issue related to promises (I am returning the registrations array before actually pushing) but I have no idea how to fix it! Some friendly help would be very much appreciated!
I recommend Promise.all, it will resolve single Promise after all promises have resolved. And technically async function is also promise so it will return promise.
here the example.
https://codesandbox.io/s/jzz1ko5l73?fontsize=14
You need to use componentDidMount()lifecycle method for proper execution and state to store the data.
constructor (props) {
super(props);
this.state = {registrations :[]}
}
componentDidMount () {
let response = this.getRegistrations()
this.setState({registrations : response});
}
Then access that state in render method. It's not good practice to call api from render mothod.
Since getRegistrations(event) returns a promise, you should perform operations on its return value inside then.
Instead of
let registrations = this.getRegistrations(event);
console.log(registrations);
Do this
this.getRegistrations(event).then(registrations => {
console.log(registrations);
// other operations on registrations
});
I am having trouble figuring out how to get an API call to re-render to the screen. I have an apiCall function that passes this.state and changes the state via passed ref but it does not trigger a rerender on the props change.
searchBody.js
class SearchBody extends Component {
constructor(props) {
super(props)
const queryString = require('query-string');
const queryTerm = queryString.parse(this.props.location.search);
this.state = { urlSearchTerm: queryTerm.search,
searchTerm: '',
loaded: false,
buttonClicked: null,
apiData: [],
tableHeaders: [],
tableRows: []
}
// check if URL has search term if so pass term for apiCall
if (this.state.urlSearchTerm) {
this.state.searchTerm = this.state.urlSearchTerm
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
// capture input text field to state variable
handleChange = searchTerm => event => {
this.setState({ searchTerm: event.target.value })
//console.log(this.state.searchTerm)
}
// handle form submission
handleSubmit = (event) => {
console.log('Inside HandleSubmit')
console.log('button clicked update url to /?search=' + this.state.searchTerm)
this.props.history.push('/?search=' + this.state.searchTerm);
this.setState({buttonClicked: true})
event.preventDefault();
}
// load search from API if search term is in URL
componentDidMount() {
console.log('Inside compDidMount')
if (this.state.urlSearchTerm){
this.setState({apiData: apiCall(this.state)})
}
}
render() {
const { classes } = this.props;
let table = ''
//check if API has loaded data and show results if true
if (this.state.loaded){
if (this.state.apiData.length === 0 && this.state.buttonClicked){
table = 'No Results Found'
//reset search button State
this.setState({buttonClicked: false})
} else {
table = <TableData tableHead={this.state.tableHeaders} tableData={this.state.tableRows} />
//reset search button State
this.setState({buttonClicked: false})
}
}
return (
<Fragment>
<hr/>
<form /*className={classes.container}*/ noValidate autoComplete="off" onSubmit={this.handleSubmit} >
<TextField
id="search"
label="Search field"
type="search"
/* className={classes.textField}*/
margin="normal"
onChange={this.handleChange('search')}
/>
<Button color='primary' letiant="outlined" type="submit" >Search DB</Button>
</form>
<h1>Results: </h1>
{table}
</Fragment>
)
}
}
export default SearchBody
methods.js
// break API data into arry of data for table component rows.
export const parseTableHeaders = input => {
// console.log(input)
if (input !== undefined && input.length !== 0) {
let head = []
for(let key in input[0]){ head.push(key);}
//console.log(head)
return head
}
}
///break API data into array of headers for table component
export const parseTableRows = (input) => {
let rows = [];
for(let o in input) {
rows.push(Object.values(input[o]));
}
//console.log(head)
return rows
}
//get api data from AWS
export function apiCall(props) {
const searchTerm = props.searchTerm
let apigClientFactory = require('aws-api-gateway-client').default;
const config = {
//apiKey: 'xxxx',
invokeUrl:'https://xxxx.execute-api.us-east-2.amazonaws.com'
}
let apigClient = apigClientFactory.newClient(config);
let params = {
//This is where any header, path, or querystring request params go. The key is the parameter named as defined in the API
// userId: '1234',
search_keyword: searchTerm
};
// Template syntax follows url-template https://www.npmjs.com/package/url-template
let pathTemplate = '/beta/testDB'
let method = 'GET';
let additionalParams = {
//If there are any unmodeled query parameters or headers that need to be sent with the request you can add them here
headers: { },
queryParams: {
search_keyword: searchTerm
}
}
apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
//This is where you would put a success callback
console.log('apiCall Returned. searchTerm; ', searchTerm)
console.log(result)
props.loaded = true
props.tableHeaders = parseTableHeaders(JSON.parse(result.data))
props.tableRows = parseTableRows(JSON.parse(result.data))
return JSON.parse(result.data)
}).catch( function(result){
//This is where you would put an error callback
})
}
Am I structuring the code wrong? My understanding is that when a prop changes it will force a re-render. Should I pass the "this.state.apiData" into apiCall instead of the entire state like this?
apiCall(this.state.apiData)
This is running within componentDidMount() I believe this is the correct location to call an API, but it's not re-rendering upon callback. I can see in the debugger the state variables are updating as expected.
Should I set a return variable in apiCall() and have the return value update the state within the componentDidMount()? Would this force a re-render once the data is returned?
something like this?
this.setState({apiData: apiCall()})
If I return this.state.apiData from apiCall() and have it parse the table headers and rows inside apiCall, when the state variable is returned will this force an update?
You are running an Async Call to get some rest api Data. Async by definition means you have no idea when your code will finish. This means you will need some type of callback to run after your apiCall has finished.
What you have here is a rest api call that returns a promise object. The promise object is basically an interface for adding callbacks to asynchronous code. I recommend you take one of these options for running a callback after your restApi call.
1.) You can pass a callback function into restApi() as a second parameter. You would invoke this callback as such:
let that = this;
apiCall(props, function(result) {
that.setState({apiData: result});
});
export function apiCall(props, callback) {
...
apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
...
callback(result);
...
});
...
}
2.) Your other option would be to handle the resolution of your apiCall by latching onto the promise that is created by the api call. When you execute async code the call to the async method immediately returns the promise object which you can return to the calling function to allow the caller to attach callbacks. this may sound a bit confusing, I am not the greatest at explaining things but see the following:
let that = this;
apiCall(props).then(function(result) {
that.setState({apiData: result});
});
export function apiCall(props) {
...
return apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
...
});
}
The key difference here is that you are returning the actual async call to apigClient.invokeApi. this allows whoever calls apiCall() to attach any callback functionality in the .then() method.
Ultimately you want to make sure you are calling setState when the restApi data actually gets back to the caller. .then() is the easiest way to trigger that call and reliably get the returned result.
Note: you should also look into promises in JS as the .then method can accept 2 parameters, One function that handles the successful return of data and one that handles error reporting.
Eric Hasselbring helped me get to this answer.
I was calling apiCall() and it was not returning anything inside of componentDidMount. I added return before the methods.js apigClient.invokeAPI() so that it would return the result of the function
return apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
//This is where you would put a success callback
console.log('apiCall Returned. searchTerm; ', searchTerm)
console.log(result)
debugger
props.tableHeaders = parseTableHeaders(JSON.parse(result.data))
props.tableRows = parseTableRows(JSON.parse(result.data))
return JSON.parse(result.data)
}).catch( function(result){
//This is where you would put an error callback
})
then I updated my function call from within SearchBody.js to
componentDidMount() {
console.log('Inside compDidMount')
if (this.state.urlSearchTerm){
apiCall(this.state).then(apiResponse => this.setState({
loaded: true,
apiData: apiResponse
})
)
}
}
So now apiCall returns a result from a promise and the apiCall is now a promise because of the .then function added. this allows the state to be changed inside of componentDidMount so that react sees there was a prop change and re-renders.