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);
}
}
Related
I can't quite wrap my head around this.
I'm having to pass data that's fetched asynchronously. Issue is, the props are asynchronous as well. Here's a simplified version of the component:
import React, { Component } from 'react'
import CSVLink from 'react-csv'
import generateData from './customApi/generateData
type Props = { job?: JobType | undefined }
type State = { csvData: string[][] }
class MyComponent extends Component<Props, State> {
state = {
csvData = [],
}
generateCsv = async (jobId: string) => {
const csvData = await generateData(jobId)
this.setState({ csvData })
}
async componentDidMount() {
const { job } = this.props
await this.generateCsv(job.id)
}
render() {
const { csvData } = this.state
return (
<CSVLink data={csvData}>
<p>Download csv</p>
</CSVLink>
)
}
}
export default connectFirestore(
(db, params) => ({ getJob(db, params.id) })
)
Basically my props are fetched from an API call to firestore, so it takes a while to load the job. Issue is, when I'm trying to pass the jobId inside the async componentDidMount(), it ends up passing undefined, because the job props are not loaded yet.
I don't link the whole passing state to async call business going on, but I can't think of any other way, how I would passing the csvData from the generateDate() async call only once the Promise is resolved.
I guess the easiest way to approach this would be, to perhaps somehow check if the props inside the componentDidMount() are already fetched?
What would be the correct way to approach this?
You are missing to implement the constructor where the props are set
constructor(props){
super(props);
this.state = {
csvData = [],
};
}
componentDidMount(){
//This will work
console.log(this.props.job);
};
If job property should by async, do it async: rather than passing a value which will change in future, you can pass a Promise which resolves with the id, than change your componentDidMount as follows:
componentDidMount() {
const id = await this.props.job;
this.generateCsv(id)
}
Hope this helps.
Maybe you should change the code inside your parent component, I imagine that you are making the API call there, as you are passing this data as props in this component you are showing to us.
I also imagine that you are making the API call with the fetch command, which can have a .then(()=>{}) method triggered when the API call finished loading, after that you can change the state of that component carrying the API fetched data and THEN render this child. Something I used recently for my project was to load from API, update state and conditionally render the child component, which will not know I made an API call because I am passing already loaded data. Normally while it is waiting I put something like this:
if(this.state.dataFetched == null)
return(<h1>Loading page...</h1>)
else return <childComponent loadedData = {this.state.dataFetched}/>
And then access that data as this.props.loadedData
Not sure if it's the optimal solution, but it works.
I've decided to use componentDidUpdate() life-cycle method, where I'm comparing whether the props have already update and once they did, I'm calling the asynchornous function for generating the csv data.
async componentDidUpdate(prevProps: Props) {
if (prevProps !== this.props && !!this.props) {
const { job } = this.props
if (job) {
await this.generateCsv(job.id)
}
}
}
This way we generate new data every time the data inside the props.job changed and also we don't attempt to call generateCsv() on undefined while it's still being fetched from firestore.
i have a problem where one of my components has to await to get some data while its rendering but i can't find a way to do that.
so i have the render method
render() {
const getComponentProps = async () => {
return await this.props.Store.getComponentProperties(id);
};
componentProps = getComponentProps(id);
return <MyComponent
.
.
data={componentProps}/>;
}
the problem is that my component is rendering before the data is fetched. i can't make the whole render await, i also tried making the componentProps a state on the hope it would rerender once it's ready, but that also didn't work. and finally i tried the new Suspense/Lazy feature in the new react version, which also didn't work.
the data i'm fetching is making a REST call to my database and I have to await it. also, the render it mapping over a list of components not just one, and for each component is has to get its properties and load them.
any thoughts on how to make this async data fetch in render ???
To achieve this, you will have to use your state.
When your component is mounted, you can call the function setState and wait for your data to be fetched. Once the data is fetched, your component will re-render with the correct data :
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
data: null
}
}
componentDidMount = async() => {
this.setState({ data: await this.props.Store.getComponentProperties(id) })
}
render() {
return <MyComponent2 data={this.state.data} />;
}
}
If you do not want your child component to receive empty data, you can choose to not render it while your data has not been fetched :
render() {
return this.state.data ? <MyComponent data={this.state.data} /> : <div/>
}
I have a complete running code, but it have a flaw. It is calling setState() from inside a render().
So, react throws the anti-pattern warning.
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount
My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
checkData: true,
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost !== nextProps.cost) {
this.setState({
checkData: true
});
}
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
if (this.props.cost.length && this.state.checkData) {
const tmp = this.props.cost;
//some calculations
....
....
this.setState({
theData: tmp,
checkData: false
});
}
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
<PieGraph theData={this.state.theData} />
</div>
);
}
}
The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.
handleFormSubmit = (e) => {
this.props.onGpChange(this.state.value);
e.preventdefaults();
}
The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?
Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.
checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.
Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost != nextProps.cost) {
this.setState({
theData: this.props.cost
});
}
}
shouldComponentUpdate(nextProps, nextState){
if(nextProps.cost !== this.props.cost){
return true;
}
return false;
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
{this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
</div>
);
}
}
PS:- The above solution is for version React v15.
You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.
There are other ways!
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render
method, both on the initial mount and on subsequent updates. It should
return an object to update the state, or null to update nothing.
So in your case
...component code
static getDerivedStateFromProps(props,state) {
if (this.props.cost == nextProps.cost) {
// null means no update to state
return null;
}
// return object to update the state
return { theData: this.props.cost };
}
... rest of code
You can also use memoization but in your case it's up to you to decide.
The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps
For example updating a list (searching) after a prop changed
You could go from this:
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
to this:
import memoize from "memoize-one";
class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
I am having a bit of an issue rendering components before the state is set to the data from a returned asynchronous API request. I have a fetch() method that fires off, returns data from an API, and then sets the state to this data. Here is that block of code that handles this:
class App extends Component {
constructor() {
super();
this.state = {
currentPrice: null,
};
}
componentDidMount() {
const getCurrentPrice = () => {
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url).then(data => data.json())
.then(currentPrice => {
this.setState = ({
currentPrice: currentPrice.bpi.USD.rate
})
console.log('API CALL', currentPrice.bpi.USD.rate);
}).catch((error) => {
console.log(error);
})
}
getCurrentPrice();
}
You will notice the console.log('API CALL', currentPrice.bpi.USD.rate) that I use to check if the API data is being returned, and it absolutely is. currentPrice.bpi.USD.rate returns an integer (2345.55 for example) right in the console as expected.
Great, so then I assumed that
this.setState = ({ currentPrice: currentPrice.bpi.USD.rate }) should set the state without an issue, since this data was received back successfully.
So I now render the components like so:
render() {
return (
<div>
<NavigationBar />
<PriceOverview data={this.state.currentPrice}/>
</div>
);
}
}
export default App;
With this, I was expecting to be able to access this data in my PriceOverview.js component like so: this.props.data
I have used console.log() to check this.props.data inside my PriceOverview.js component, and I am getting 'null' back as that is the default I set intially. The issue I am having is that the components render before the API fetch has ran it's course and updated the state with the returned data. So when App.js renders the PriceOverview.js component, it only passes currentPrice: null to it, because the asynchronous fetch() has not returned the data prior to rendering.
My confusion lies with this.setState. I have read that React will call render any time this.setState is called. So in my mind, once the fetch() request comes back, it calls this.setState and changes the state to the returned data. This in turn should cause a re-render and the new state data should be available. I would be lying if I didn't say I was confused here. I was assuming that once the fetch() returned, it would update the state with the requested data, and then that would trigger a re-render.
There has to be something obvious that I am missing here, but my inexperience leaves me alone.. cold.. in the dark throws of despair. I don't have an issue working with 'hard coded' data, as I can pass that around just fine without worry of when it returns. For example, if I set the state in App.js to this.state = { currentPrice: [254.55] }, then I can access it in PriceOverview.js via this.props.data with zero issue. It's the async API request that is getting me here, and I am afraid it has gotten the best of me tonight.
Here App.js in full:
import React, { Component } from 'react';
import './components/css/App.css';
import NavigationBar from './components/NavigationBar';
import PriceOverview from './components/PriceOverview';
class App extends Component {
constructor() {
super();
this.state = {
currentPrice: null,
};
}
componentDidMount() {
const getCurrentPrice = () => {
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url).then(data => data.json())
.then(currentPrice => {
this.setState = ({
currentPrice: currentPrice.bpi.USD.rate
})
console.log('API CALL', currentPrice.bpi);
}).catch((error) => {
console.log(error);
})
}
getCurrentPrice();
}
render() {
return (
<div>
<NavigationBar />
<PriceOverview data={this.state.currentPrice}/>
</div>
);
}
}
export default App;
Here is PriceOverview.js in full:
import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
class PriceOverview extends Component {
constructor(props) {
super(props);
this.state = {
currentPrice: this.props.data
}
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.state.currentPrice != null ? <div className="price">{this.state.currentPrice}</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
export default PriceOverview;
Thank you in advance to any help, it's much appreciated.
this.setState ({
currentPrice: currentPrice.bpi.USD.rate
})
Do not put an = in this.setState
Ok First thing, when you're writting code on React the components that hold state are the class base components so ... What I see here is that you're creating two class base components so when you pass down props from your app class component to your PriceOverview wich is another class base component you're essentially doing nothing... Because when your constructor on your PriceOverview get call you're creating a new state on that Component and the previous state ( that's is the one you want to pass down) is being overwritten and that's why you're seem null when you want to display it. So it should work if you just change your PriveOverview component to a function base component ( or a dumb component). So this way when you pass down the state via props, you're displaying the correct state inside of your div. This is how would look like.
import React from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
const PriceOverview = (data) => {
return (
<div className="overviewBar">
<div className="currentPrice panel">
//Im calling data here because that's the name you gave it as ref
//No need to use 'this.props' you only use that to pass down props
{data != null ? <div className="price">
{data}</div> : <div className="price">Loading...</div>
}
</div>
</div>
)
}
}
export default PriceOverview;
Whenever you're writing new components start always with function base components if you component is just returning markup in it and you need to pass some data go to his parent component update it (making the api calls there or setting the state there) and pass down the props you want to render via ref. Read the React docs as much as you can, hope this explanation was useful (my apologies in advance if you don't understand quite well 'cause of my grammar I've to work on that)
The thing is constructor of any JS class is called only once. It is the render method that is called whenever you call this.setState.
So basically you are setting currentPrice to null for once and all in constructor and then accessing it using state so it will always be null.
Better approch would be using props.
You can do something like this in your PriceOverview.js.
import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
class PriceOverview extends Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.props.data!= null ? <div className="price">{this.props.data}</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
export default PriceOverview;
Or you can use react lifecycle method componentWillReceiveProps to update the state of PriceOverview.js
componentWillReceiveProps(nextProps) {
this.setState({
currentPrice:nextProps.data
});
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.state.currentPrice != null ? <div className="price">{this.state.currentPrice }</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
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