componentDidMount not called when Route with a different param is called - reactjs

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);
}
}

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

Pass state as prop to child component

As the title says, how can I pass state data to a new component. I have a parent class that loads some data from an IndexedDb.
I will pass the information of "image" as prop to my child component.
class Parent extends Component<Props> {
constructor(props: Props) {
super(props);
// map state to json structure
this.state = {
data: {
image: ''
[...]
};
}
public componentWillMount() {
const dataProvider = new DataProvider(DataProvider.DBNAME);
dataProvider.loadData('somedoc')
.then((data: object) => this.setState({data}));
}
public render() {
const {data}: any = this.stat
return (<Child image={data.image} />);
}}
In my child Component I will use the prop to make a new request for retrieving the image. The problem is that the prop in the componentWillMount method is empty.
interface IImageProps {
image: string;
}
Class Child extends Component<IImageProps> {
constructor(props: IImageProps) {
super(props);
}
public componentWillMount() {
console.log("image", this.props.image); // <-- it's allways empty
// do some async stuff
}
public render() {
console.log("image", this.props.image); // <-- the image information is shown
}}
What did I miss, cause in the render methode I can use the prop? How do I pass the variables correctly?
Thank you for helping. I solved my problem with the componentDidUpdate() method. As describe in the documentation https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/postrender_with_componentdidupdate.html
For now my final solution:
componentDidUpdate() {
const imageName: string = this.props.imageName;
const dataProvider = new DataProvider(DataProvider.DBNAME);
dataProvider.loadImage('somedoc', imageName)
.then((response: any) => {
let {imageDataUrl}: any = this.state;
if (imageDataUrl !== response) {
imageDataUrl = response;
this.setState({imageDataUrl});
}
})
.catch((error: any) => console.log(error));
}
Since your loadData function is asynchronous, data.image will be an empty string when the Child component is mounted.
You could e.g. wait with the rendering of the Child component until the request has finished.
Example
class Parent extends Component<Props> {
constructor(props: Props) {
super(props);
this.state = {
data: {
image: null
}
};
}
public componentDidMount() {
const dataProvider = new DataProvider(DataProvider.DBNAME);
dataProvider
.loadData("somedoc")
.then((data: object) => this.setState({ data }));
}
public render() {
const { data }: any = this.stat;
if (data.image === null) {
return null;
}
return <Child image={data.image} />;
}
}

Cannot access props in Higher Order Component React

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.

Idiomatic React for Blueprintjs Toast component

The Blueprint UI library provides a Toaster component that displays a notice for a user's action. From the documentation, it's used by first calling
const MyToaster = Toaster.create({options}), followed by
MyToaster.show({message: 'some message'}).
I'm having trouble fitting the show method into React's lifecycle - how do I create a reusable toaster component that will display different messages on different button clicks? If it helps, I am using MobX as a data store.
The Toaster is a funny one in this regard because it doesn't care about your React lifecycle. It's meant to be used imperatively to fire off toasts immediately in response to events.
Simply call toaster.show() in the relevant event handler (whether it's a DOM click or a Redux action).
See how we do it in the example itself: toastExample.tsx
My solution with Redux (and redux-actions)...
Action:
export const setToaster = createAction('SET_TOASTER');
Reducer:
const initialState = {
toaster: {
message: ''
}
};
function reducer(state = initialState, action) {
switch(action.type) {
case 'SET_TOASTER':
return {
...state,
toaster: { ...state.toaster, ...action.payload }
};
};
}
Toaster Component:
// not showing imports here...
class MyToaster extends Component {
constructor(props) {
super(props);
this.state = {
// set default toaster props here like intent, position, etc.
// or pass them in as props from redux state
message: '',
show: false
};
}
componentDidMount() {
this.toaster = Toaster.create(this.state);
}
componentWillReceiveProps(nextProps) {
// if we receive a new message prop
// and the toaster isn't visible then show it
if (nextProps.message && !this.state.show) {
this.setState({ show: true });
}
}
componentDidUpdate() {
if (this.state.show) {
this.showToaster();
}
}
resetToaster = () => {
this.setState({ show: false });
// call redux action to set message to empty
this.props.setToaster({ message: '' });
}
showToaster = () => {
const options = { ...this.state, ...this.props };
if (this.toaster) {
this.resetToaster();
this.toaster.show(options);
}
}
render() {
// this component never renders anything
return null;
}
}
App Component:
Or whatever your root level component is...
const App = (props) =>
<div>
<MyToaster {...props.toaster} setToaster={props.actions.setToaster} />
</div>
Some Other Component:
Where you need to invoke the toaster...
class MyOtherComponent extends Component {
handleSomething = () => {
// We need to show a toaster!
this.props.actions.setToaster({
message: 'Hello World!'
});
}
render() { ... }
}

Resources