Cannot access props in Higher Order Component React - reactjs

I have a simple class as follows that for the sake of this example just renders out the length of a list loaded from Firebase.
class Companies extends Component {
constructor() {
super();
this.state = {
companies: [],
loading: true
};
this.firebase = new FirebaseList('companies');
}
componentDidMount() {
const previousCompanies = this.state.companies;
this.firebase.databaseSnapshot('companies').then((snap) => {
if (snap.val() === null) {
this.setState({loading: false})
}
});
this.firebase.database.on('child_added', snap => {
previousCompanies.push({
id: snap.key,
...snap.val()
});
this.setState({
companies: previousCompanies,
loading: false
})
});
this.firebase.database.on('child_changed', snap => {
const updatedCompanies = updatedItems(this.state.companies, this.state.currentCompany);
this.setState({
companies: updatedCompanies
})
});
this.firebase.database.on('child_removed', snap => {
const updatedCompanies = removeItem(previousCompanies, snap.key);
this.setState({
companies: updatedCompanies
})
})
}
render() {
return (
<div>
{this.state.companies.length}
</div>
);
}
}
export default WithLoader('companies')(Companies);
This is a pattern I frequently repeat, so I want to build a Loader into a Higher Order Component, to show a Loader animation when the data is being fetched from the database.
I'm using the following code for this:
const WithLoader = (propName) => (WrappedComponent) => {
return class WithLoader extends Component {
componentDidMount() {
console.log(this.props)
}
isEmpty(prop) {
return (
prop === null ||
prop === undefined ||
(prop.hasOwnProperty('length') && prop.length === 0) ||
(prop.constructor === Object && Object.keys(prop).length === 0)
)
}
render() {
return this.isEmpty(this.props[propName]) ? <Spinner /> : <WrappedComponent {...this.props}/>
}
}
};
export default WithLoader;
I'm trying to access the companies from the state of the Companies component in my Higher Order Component. However, when I console.log(this.props) in my Higher Order Component, I only get the history, match and location props.
What am I doing wrong?

since withLoader is the HOC so first withLoader will be rendered before the Companies component, because of which you are getting only routes and match as props.
since withLoader is wrapping the companies component so it will access the props of its parent where it is rendered not the props of its wrapped component.
according to your query there are two ways
either you fetch all the data in HOC and then render the Companies Component
or
pass the fetched data from parent to the Companies component.
<Companies companylist={this.state.company} />
what you are trying to do is that you are trying to pass the props of its child which is not yet rendered.

Related

Unable to pass data from a Parent'state to a Child's state in React

I am trying to pass an Array of values from a Parent module to a Child module, and set them in the Child's state in order to display a chart.
Here is my Parent module:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this.getRates();
}
getRates = () => {
fetch(
"https://api.exchangerate.host/timeseries?start_date=2022-07-01&end_date=2022-07-05&base=USD&symbols=EUR"
)
.then((res) => res.json())
.then((timeseries) => {
const rates = Object.values(timeseries.rates);
this.setState({
data: rates,
});
});
};
render() {
const data = this.state;
return (
<>
<Child data={data} />
</>
);
}
}
And here is the Child module:
class Child extends Component {
constructor(props) {
super(props);
this.state = {
items: [],
};
}
componentDidMount() {
this.setState({
items: this.props.data,
});
}
render() {
const { items } = this.state;
console.log("Child data from state: ", items);
console.log("Child data from props: ", this.props.data);
return (
<>
<ReactApexChart options={items} />
</>
);
}
}
Here is what I am getting from the console.log():
Child data from state: []
Child data from props: (30) [95.9182, 95.7676, 94.8036, ..., 95.2308, 95.2906]
Why am I unable to set the Child's state with this data?
Your Child component does not get its state updated because the lifecycle function you are using does not get called when the component gets an updated set of props from the Parent.
Please check the Updating heading on https://www.w3schools.com/react/react_lifecycle.asp
You will not find the componentDidMount lifecycle in there because it does not get called on a prop update.
What you need to use is something like getDerivedStateFromProps
static getDerivedStateFromProps(props) {
return {items: props.data};
}
This makes sure that every time the Parent sends an updated value in the props, the Child uses it to update the state and then re-render accordingly.

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

Correct approach for using flux and component lifecycle

I'm migrating the code from what I see on here on CodePen.
Within IssueBox, I am planning to implement a form which an enduser will update setting a state from 'unverified' to 'verified'.
App (ill rename this component) will be my parent and IssueBox would be the child.
So I got through flux => Action -> dispatcher -> udpate db -> update view.
Now that I have the new state and the view should be updated, do I use componentWillRecieveProps() and then setState there, so that in IssueBox I can continue using this.props thus in turn updating it.
import React, { Component } from "react";
import IssueBox from "./issuebox.js";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoaded: false,
email: [],
counter: 0,
title: "Test run"
};
}
componentDidMount() {
fetch(
"https://s3-us-west-2.amazonaws.com/s.cdpn.io/311743/dummy-emails.json"
)
.then(res => res.json())
.then(result => {
const emails = result.data;
console.log("resutl state: ", emails);
let id = 0;
for (const email of emails) {
email.id = id++;
email.verified = 'False'
}
this.setState({
isLoaded: true,
emails: emails
});
});
}
render() {
//console.log(this.state.email);
return (
<div className="App">
<div>
<IssueBox emails={this.state.email} />
</div>
</div>
);
}
}
//issuebox.js
import React, { Component } from "react";
class IssueBox extends Component {
constructor(args) {
super(args);
const emails = this.props.emails;
console.log("inner props: ", emails);
let id = 0;
for (const email of emails) {
email.id = id++;
}
this.state = {
selectedEmailId: 0,
currentSection: "inbox",
emails
};
}
//...copy and pase from codepen
setSidebarSection(section) {
let selectedEmailId = this.state.selectedEmailId;
if (section !== this.state.currentSection) {
selectedEmailId = "";
}
this.setState({
currentSection: section,
selectedEmailId
});
}
componentWillReceiveProps(newProps) {
// Assign unique IDs to the emails
this.setState({ emails: newProps.data });
}
render() {
const currentEmail = this.state.emails.find(
x => x.id === this.state.selectedEmailId
);
return (
<div>
<Sidebar
emails={this.props.emails}
setSidebarSection={section => {
this.setSidebarSection(section);
}}
/>
)}
///.....copy and pase from codepen
The error is being caused by this line in componentWillReceiveProps():
this.setState({ emails: newProps.data });
The emails are coming in on a property called emails so that line should be:
this.setState({ emails: newProps.emails });
That being said, componentWillReceiveProps() gets called more frequently than you might expect. I recommend that you add the id's to the emails within componentDidMount() of App so they come into IssueBox ready to use. This means that App is keeping the emails in its state and simply passing them to IssueBox as props, so you can remove emails from the state in IssueBox and just use the emails that come in through the props everywhere within IssueBox (similar to how the other components use emails coming in on their props and don't keep them in their own local state).

componentDidMount not called when Route with a different param is called

i'm rendering "Details" component in a callback in my UsersListContainer like this:
class UsersListContainer extends Component {
goToUserById(id) {
if (!id) { return false; }
this.props.history.push(`/users/${id}`);
}
render() {
return (
<UserList
goToUser={(id) => this.goToUserById(id)}/>
);
}
}
My "Details" container:
class UserDetailsContainer extends Component {
componentDidMount() {
this.props.getUserDetails(this.props.match.params.id);
}
render() {
return (
<UserDetails user={this.props.selectedUser}/>
);
}
}
const mapDispatchToProps = dispatch => {
return {
getUserDetails: id => dispatch(getUser(id))
};
};
const mapStateToProps = (state) => ({
selectedUser: state.user.selectedUser
});
And in my presentational "User" component I display a set of data from redux store like this:
class UserDetails extends Component {
constructor(props) {
super(props);
this.state = {
name: this.props.user.name,
address: this.props.user.name,
workingHours: this.props.user.workingHours,
phone: this.props.user.phone
};
}
I'm not displaying component props directly and I use state because they are meant to be edited. This works, but the problem is that all these props are not updating simultaneously with component load which means when I select user for the first time it displays the right info, then I switch back to "/users" to choose another, and his props remain the same as props of the previous user. I tried componentWillUnmount to clear the data but it didn't work
Solved this by using lodash lib
in my presentational component I compare if objects are equal
constructor(props) {
super(props);
this.state = {
name: "",
address: ""
};
}
componentWillReceiveProps(nextProps) {
_.isEqual(this.props.user, nextProps.user) ? (
this.setState({
name: this.props.user.name,
address: this.props.user.address
})
) : (
this.setState({
name: nextProps.user.name,
address: nextProps.user.address,
})
)
}
When you implement a Route like /users/:id, and if you change the id to something else, the entire component is not re-mounted and hence the componentDidMount is not called, rather only the props change and hence you need to implement componentWillReceiveProps function also
componentDidMount() {
this.props.getUserDetails(this.props.match.params.id);
}
componentWillReceiveProps(nextProps) {
if(this.props.match.params.id !== nextProps.match.params.id) {
this.props.getUserDetails(nextProps.match.params.id);
}
}

save react component and load later

I have react component in react native app and this will return Smth like this:
constructor(){
...
this.Comp1 = <Component1 ..... >
this.Comp2 = <Component2 ..... >
}
render(){
let Show = null
if(X) Show = this.Comp1
else Show = this.Comp1
return(
{X}
)
}
and both of my Components have an API request inside it ,
so my problem is when condition is changed and this toggle between Components , each time the Components sent a request to to that API to get same result ,
I wanna know how to save constructed Component which they wont send request each time
One of the ways do that is to handle the hide and show inside each of the child component comp1 and comp2
So you will still render both comp1 and comp2 from the parent component but you will pass a prop to each one of them to tell them if they need to show or hide inner content, if show then render the correct component content, else just render empty <Text></Text>
This means both child components exist in parent, and they never get removed, but you control which one should show its own content by the parent component.
So your data is fetched only once.
Check Working example in react js: https://codesandbox.io/s/84p302ryp9
If you checked the console log you will find that fetching is done once for comp1 and comp2.
Also check the same example in react native below:
class Parent extends Component {
constructor(props)
{
super(props);
this.state={
show1 : true //by default comp1 will show
}
}
toggleChild= ()=>{
this.setState({
show1 : !this.state.show1
});
}
render(){
return (
<View >
<Button onPress={this.toggleChild} title="Toggle Child" />
<Comp1 show={this.state.show1} />
<Comp2 show={!this.state.show1} />
</View>
)
}
}
Comp1:
class Comp1 extends Component
{
constructor(props) {
super(props);
this.state={
myData : ""
}
}
componentWillMount(){
console.log("fetching data comp1 once");
this.setState({
myData : "comp 1"
})
}
render(){
return (
this.props.show ? <Text>Actual implementation of Comp1</Text> : <Text></Text>
)
}
}
Comp2:
class Comp2 extends Component {
constructor(props) {
super(props);
this.state = {
myData2: ""
}
}
componentWillMount() {
console.log("fetching data in comp2 once");
this.setState({
myData2: "comp 2"
});
}
render() {
return (
this.props.show ? <Text>Actual implementation of Comp2</Text> : <Text></Text>
)
}
}
I think, you should move all your logic to the main component (fetching and saving data, so you component1 and component2 are simple dumb components. In component1 and component2 you can check "does component have some data?", if there isn't any data, you can trigger request for that data in parent component.
Full working example here: https://codesandbox.io/s/7m8qvwr760
class Articles extends React.Component {
componentDidMount() {
const { fetchData, data } = this.props;
if (data && data.length) return;
fetchData && fetchData();
}
render() {
const { data } = this.props;
return (
<div>
{data && data.map((item, key) => <div key={key}>{item.title}</div>)}
</div>
)
}
}
class App extends React.Component{
constructor(props){
super(props);
this.state = {
news: [],
articles: [],
isNews: false
}
}
fetchArticles = () => {
const self = this;
setTimeout( () => {
console.log('articles requested');
self.setState({
articles: [{title: 'article 1'}, {title: 'articles 2'}]
})
}, 1000)
}
fetchNews = () => {
const self = this;
setTimeout(() => {
console.log('news requested');
self.setState({
news: [{ title: 'news 1' }, { title: 'news 2' }]
})
}, 1000)
}
handleToggle = (e) => {
e.preventDefault();
this.setState({
isNews: !this.state.isNews
})
}
render(){
const { news, articles, isNews} = this.state;
return (
<div>
<a href="#" onClick={this.handleToggle}>Toggle</a>
{isNews? (
<News data={news} fetchData={this.fetchNews} />
): (
<Articles data={articles} fetchData={this.fetchArticles} />
)}
</div>
)
}
}

Resources