How can I chain asynchronous Firebase updates in my React app? - reactjs

React & Firebase newbie here. I have a React component that needs to look up some stuff in Firebase before rendering. My database design requires first getting the correct doohick ids and subsequently looking up the doohick details, but I'm not sure how to do that with the asynchronous nature of Firebase database access. This doesn't work:
class Widget extends React.Component {
componentDidMount() {
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
});
this.state.doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
render() {
if (this.state.doohick-ids) {
return null;
} else {
// render the Doohick subcomponents
}
}
}
I can think of a few solutions here, but none that I like. What's the recommended way to chain together Firebase calls, or perhaps redesign this to eliminate the problem?

I think you should split one component Widget to two WidgetList and WidgetItem.
WidgetItem
subscribe and unsubscribe to firebase.database().ref(/doohick-details/${id})
class WidgetItem extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = {};
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
const { id } = this.props;
this.dbRef = firebase.database().ref(`/doohick-details/${id}`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
// update state
this.setState(dataSnapshot);
}
render() {
return (
<pre>{JSON.stringify(this.state, null, 2)}</pre>
);
}
}
WidgetList
subscribe and unsubscribe to firebase.database().ref(/users/${username}/doohick-ids)
class WidgetItem extends React.Component {
constructor(props) {
super(props);
this.state = { doohick_ids: [] };
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
// Note: I've just copied your example. `username` is undefined.
this.dbRef = firebase.database().ref(`/users/${username}/doohick-ids`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
this.setState({ doohick_ids: dataSnapshot });
}
render() {
const { doohick_ids } = this.state;
if (doohick_ids.length === 0) {
return 'Loading...';
}
return (
<React.Fragment>
{doohick_ids.map(id => <WidgetItem key={id} id={id} />)}
</React.Fragment>
);
}
}

And code that requires the data from the database needs to be inside the callback that is invoked when that data is available. Code outside of the callback is not going to have the right data.
So:
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
});
There's many optimizations possible here, but they all boil down to the code being inside the callback and updating the state when a value comes from the database.

Related

Call a function of one of a list of children components of the parent component using reference

For my website I want to include a feature that helps users randomly click a link programatically. The event happens in the parent component called StreamingPlaza, and its has a list of children components called StreamingCard, each containing a streaming link. Below is my code:
StreamingPlaza
class StreamingPlaza extends Component {
state = {
......
}
roomclicks = [];
componentDidMount() {
//Approach 1//
this.roomclicks[0].current.handleClick();
//Approach 2//
this.roomclicks[0].props.click = true;
......
}
setRef = (ref) => {
this.roomclicks.push(ref);
}
renderRoom = (room) => {
return <StreamingCard info={room} ref={this.setRef} click={false}></StreamingCard>;
}
render () {
const rooms = this.props.rooms;
return (
{ rooms && rooms.map (room => {
return this.renderRoom(room);
})
}
);
}
StreamingCard
class StreamingCard extends Component {
constructor(props){
super(props);
this.state = {
......
}
}
handleClick = () => {
document.getElementById("link").click();
}
render() {
return (
✔️ Streaming Link: <a id="link" href=......></a>
);
}
Regarding Approach 1, the console reported the error Cannot read property handClick of undefined. After I removed "current", it said that this.roomclicks[0].handleClick is not a function. Regarding Approach 2, I was not able to modify the props in this way, as the console reported that "click" is read-only.
Approach 1 is basically how its need to be done, but with React API.
See React.createRef
class StreamingPlaza extends Component {
roomclicks = React.createRef([]);
componentDidMount() {
// 0 is room.id
this.roomclicks.current[0].handleClick();
}
renderRoom = (room) => {
return (
<StreamingCard
info={room}
ref={(ref) => (this.roomclicks.current[room.id] = ref)}
click={false}
></StreamingCard>
);
};
render() {
const rooms = this.props.rooms;
return rooms.map((room) => {
return this.renderRoom(room);
});
}
}

Error: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application

I'm getting the above error and I don't know how to handle it.
I got a component. And in the render() i'm looping through an array and placing another component and parsing a value to that component like this:
render() {
let allProducts = this.state.products.map((product, i) => {
return (
<div key={product.article}>
...
<PriceStock value={product.article} />
...
</div>
)
})
}
In the PriceStock component i'm fetching some data with axios like the code below:
export default class PriceStock extends React.Component {
constructor(props) {
super(props);
this.state = ({
buttoprice: ''
})
this.getPriceAndStock = this.getPriceAndStock.bind(this)
}
getPriceAndStock(articleNo) {
return axios.post('LINK_TO_URL', {
articleNo: articleNo
}).then(result => {
return result.data
})
}
async componentDidMount() {
let pricestock;
pricestock = await this.getPriceAndStock(this.props.value)
let bruttoPrice = PRICE_TO_PARSE_TO_THE_STATE;
this.setState({ buttoprice: bruttoPrice })
}
render() {
return (
<div >
{this.state.buttoprice}
</div>
);
}
}
The error seems to happen when I try to setState in the componentDidMount, any suggestions?
this is an error occurs because you are updating state before it gets initialized
perform your loading activities in the constructor it is the right way to do it
getPriceAndStock(orderNumber, articleNo) {
return axios.post('LINK_TO_URL', {
orderNr: orderNumber, vareNr: articleNo
}).then(result => {
return result.data
})
}
constructor() {
this.getPriceAndStock(this.props.value)
.then(pricestock=>{
let bruttoPrice = PRICE_TO_PARSE_TO_THE_STATE;
this.state({ buttoprice: bruttoPrice })
})
.catch(console.log)
}
Found the answear in this question: https://github.com/material-components/material-components-web-react/issues/434
It's remindend me a little bit about the comment with another stackoverflow question.

ReactJS - Pass Updated Value To Sub-Component Method

I'm working on an environment that is basically set up with a Main Component like this:
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
selectedValues: []
};
}
render() {
const { selectedValues } = this.state;
return (
// Other components
<SubComponent selectedValues = {selectedValues} />
// Other components
);
}
}
export default MainComponent;
And a Sub Component like this:
class SubComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isExporting: false,
selectedValues: props.selectedValues
};
}
performTask = () => {
this.setState({ isWorking: true });
const { selectedValues } = this.state;
console.log(`Selected Values: ${selectedValues}`);
fetch('/api/work', {
method: 'GET'
})
.then(res => res.json())
.then((result) => {
// Handle the result
this.setState({ isWorking: false });
})
.catch((error) => {
console.log(error);
this.setState({ isWorking: false });
});
};
render() {
const { isWorking } = this.state;
return (
<Button
bsStyle="primary"
disabled={isWorking}
onClick={() => this.performTask()}
>
{isWorking ? 'Working...' : 'Work'}
</Button>
);
}
}
SubComponent.propTypes = {
selectedValues: PropTypes.arrayOf(PropTypes.string)
};
SubComponent.defaultProps = {
selectedValues: []
};
export default SubComponent;
In the Main Component, there are other components at work that can change the selectedValues. The functionality I'd like to see is that when the performTask method fires, it has the most recent and up to date list of selectedValues. With my current setup, selectedValues is always an empty list. No matter how many values actually get selected in the Main Component, the list never seems to change in the Sub Component.
Is there a simple way to do this?
I would suggest you 2 of the following methods to check this problem:
Maybe the state.selectedItems doesn't change at all. You only declare it in the contractor but the value remains, since you didn't setState with other value to it. Maybe it will work if you will refer to this.props.selectedItems instead.
Try to add the function component WillReceiveProps(newProps) to the sub component and check the value there.
If this method doesn't call, it means the selectedItems doesnt change.
Update if some of it works.
Good luck.
selectedValues in SubComponent state has not updated since it was set in SubComponent constructor. You may need to call setState again in componentWillReceivedProps in SubComponent

How to force children rerendering after axios call in React?

I'm working on a form with interactive inputs. They have to actualise themselves with information into parent state.
I use Axios to get the data to show, getting them from an external API. I tried to set default values, but they never actualise with newer values.
class Form extends React.Component {
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState(res.data)
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker />
</form>
</div>
)
}
}
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: new Date(),
runMin: new Date(),
runMax: new Date()
};
}
getDate() {
console.log('DAD');
try { // if axios didn't finish, to avoid undefined
this.setState({
runMin: super.state.RunMin,
runMax: super.state.RunMax})
} catch (e) {
this.setState({
runMin: new Date(),
runMax: new Date()})
}
}
componentDidMount() {
this.getDate();
this.setState({selected: this.state.runMax});
}
render() {
return (<div></div>);
}
}
Actually after axios call, the children doesn't rerender. I separated the call for axios and the component using it, because the Form component do a single call for multiple children (not displayed here), and they read the parent's state to render.
Firstly, you should not access the parents state using super and instead pass the required value as props
Secondly, componentDidMount lifecycle is executed on initial mount and hence the logic within it won't execute when the parent state updates.
The correct way to handle your case would be
class Form extends React.Component {
state = {
RunMin: new Date(),
RunMax: new Date()
}
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState({RunMin: res.data.RunMin, RunMax: res.data.RunMax})
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker runMin={this.state.RunMin} runMax={this.state.RunMax}/>
</form>
</div>
)
}
}
class DatePicker extends React.Component {
render() {
console.log(this.props.runMin, this.props.runMax);
return (<div></div>);
}
}
The way you are setting the state is incorrect
Change
this.setState(res.data);
To
this.setState({data: res.data});
You need to set the response to a state field you have in component and make sure you pass the data to the child component

React: fire render after promise completed

Please note, that I a fetching data from AWS DynamoDB.
...
class Test extends Component {
constructor(props) {
super(props);
this.state = {
contactList: []
}
}
componentDidMount() {
var getItemsPromise = db.scan({ TableName: "tester" }).promise();
getItemsPromise.then((data) => this.setState({ contactList: data.Items }));
}
render() {
return (
<div>{this.state.contactList[0].link.S}</div>
);
}
}
export default Test;
I am trying to render the returned value, but can't. If I set
render() {
console.log(this.state.contactList[0].link.S);
return (
<div>test</div>
);
}
it works. Why is that? Why is it not working when I set it straight inline?
this.state.contactList[0] is undefined before the promise is resolved, so this.state.contactList[0].link will give rise to an error.
You could e.g. return null from the render method until the array has been filled with your objects:
class Test extends Component {
// ...
render() {
if (this.state.contactList.length === 0) {
return null;
}
return <div>{this.state.contactList[0].link.S}</div>;
}
}

Resources