dispatch an action in componentDidMount which receives a redux props as payload - reactjs

What is the best way to trigger an action inside componentDidMount () using a redux props? ex:
import { fetchUser } from '../actions'
class Example extends Component {
ComponentDidMount(){
this.props.fetchUser(this.props.id)
} ...
mapDispatchToProps = dispatch => ({
fetchUser: (payload) => dispatch(fetchUser(payload))
})
mapStateToProps = state => ({
id: state.user.id
})
The problem is that ComponentDidMount () is mounted before the class even receives props from the store. That way my this.props.id is = 'undefined' inside the method.
One solution I found was to run as follows but I do not know if it's the best way:
import { fetchUser } from '../actions'
class Example extends Component {
fetchUser = () => {
this.props.fetchUser(this.props.id)
}
render(){
if(this.props.id !== undefined) this.fetchUser()
} ...
}
mapDispatchToProps = dispatch => ({
fetchUser: (payload) => dispatch(fetchUser(payload))
})
mapStateToProps = state => ({
id: state.user.id
})
That way I get the requisition, but I do not think it's the best way. Any suggestion?

Have you tried using async/await?
async ComponentDidMount(){
await this.props.fetchUser(this.props.id)
} ...

You have to understand the lifecycle of react components. When the component gets mounted, it can fetch data, but your component at that point needs something to render. If the data hasn't been loaded yet, you should either return null to tell react that it's not rendering anything at that point, or perhaps a loading indicator to show that it's fetching data?
import { fetchUser } from '../actions'
class Example extends Component {
componentDidMount() {
this.props.fetchUser();
}
render(){
const { loading, error, user } = this.props;
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <div>Oh noes, we have an error: {error}</div>;
}
// Render your component normally
return <div>{user.name}</div>;
}
}
Your reducer should have loading set to true by default, and when your fetch completes, set loading to false, and either set the user or error depending on if the fetch fails/completes.

Related

React-Redux: Using action creators in React components

I am new to React/Redux, and appreciate your help. I am taking a Udemy course on this topic. The course instructor creates a component like this.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUser } from '../actions';
class User extends Component {
componentDidMount(){
this.props.fetchUser(this.props.userId);
}
render(){
const { user } = this.props;
if(!user) return null;
return(
<div className="header"> User Info: {user.name}</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return { user: state.users.find( user => user.id === ownProps.userId)};
};
export default connect(mapStateToProps, { fetchUser })(User)
my question: why inside the componentDidMount() he is prefixing fetchUsers() with this.props?
it is not the case that he is passing fetchUsers() as props from the parent component. This is how the parent is using this component <User userId={post.userId}/>
Note: this code works
It is because of this line :
export default connect(mapStateToProps, { fetchUser })(User)
the second parameter to connect is called mapDispatchToProps, It adds the actions to props
From the docs :
connect can accept an argument called mapDispatchToProps, which lets
you create functions that dispatch when called, and pass those
functions as props to your component.
const mapDispatchToProps = dispatch => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' })
}
}
Your code is using the “object shorthand” form.
The way the mapDispatchToProps in the example is shorthanded. It might be easier to tell what is going if it was written like so:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUser } from '../actions';
class User extends Component {
componentDidMount(){
this.props.fetchUser(this.props.userId);
}
render(){
const { user } = this.props;
if(!user) return null;
return(
<div className="header"> User Info: {user.name}</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return { user: state.users.find( user => user.id === ownProps.userId)};
};
const mapDispatchToProps = () => ({
fetchUser
});
export default connect(mapStateToProps, mapDispatchToProps)(User)
Maybe this shows it more clearly, but the dispatch function (fetchUser) is being mapped to the components properties. Just like the state value (user) is being mapped to the properties of the component. I think you just got confused because of the shorthand that was used.

How to access redux-store from within react's componentDIdMount()

In the following code I am trying to pass the state.userData.userDetails from the redux-store to getleftpaneProductCatalogue(), but state.userData.userDetails is unaccessible to componentDidMount(). I tried assigning the state.userData.userDetails to this.prop.userProfile, but still this.prop.userProfile is an empty value. How to access the prop within componentDidMount?
import React,{Component} from 'react';
import { connect } from 'react-redux';
import {Row, Col } from 'react-materialize';
import {getleftpaneProductCatalogue} from '../actions/leftpane-actions';
import ProductCatalogueLeftPaneComp from '../components/pages/product-catalogue-leftpane';
class ProductCatalogueLeftPane extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
console.log('this.props^', JSON.stringify(this.props));
this.props.getleftpaneProductCatalogue().then((data) => {
console.log('productdata', data);
})
}
render() {
return (
<div>
{JSON.stringify(this.props.userProfile)}
</div>
)
}
}
const mapStateToProps = (state) => {
console.log('state^', JSON.stringify(state));
return {leftpaneProductCatalogue: state.leftpaneProductCatalogue, userProfile: state.userData.userDetails};
};
const mapDispatchToProps = (dispatch) => {
return {
getleftpaneProductCatalogue: () => dispatch(getleftpaneProductCatalogue()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ProductCatalogueLeftPane);
You can access the state directly in mapDispatchToProps and pass it to getleftpaneProductCatalogue:
componentDidMount() {
const { dispatch, getleftpaneProductCatalogue }
dispatch(getleftpaneProductCatalogue())
}
const mapDispatchToProps = dispatch => {
return {
getleftpaneProductCatalogue: () => (dispatch, getState) => {
const state = getState()
const details = state.userData.userDetails
return dispatch(getleftpaneProductCatalogue(details))
},
dispatch
}
}
However, the way you're doing it, passing the state via mapStateToProps is still valid, but more verbose. Therefore the problem would be somewhere else.
Here's my bet. I guess you're getting the userData somewhere in your code with async API call and it's not being fetched yet. If that's the case - then you should wait for data being fetched firstly, then you can access it in your component ProductCatalogueLeftPane.

How can I do ajax call each time when store gets updated?

How can I do an ajax call each time the store gets updated?
Basically, I want to fetch products with new API params, let's say there is a drop-down for items per page. It is working fine on load, i.e on call of method componentWillMount
But I'm not sure how to do a fetch again on when the store changes.
import React, { Component } from 'react';
import ProductsList from '../components/ProductsList'
import { connect } from 'react-redux'
// Action Creators
import doFetchProducts from '../actions/doFetchProducts'
import queryString from 'query-string'
class Products extends Component {
constructor (props) {
super(props);
}
componentWillMount () {
this.fetch()
}
fetch () {
let q = queryString.stringify({
categories: 'rings',
'attributes.Style': 'Classic',
limit: this.props.applyItemsPerPage,
page: 1,
status: 'Active'
})
this.props.dispatch(
doFetchProducts(q)
)
}
render() {
return (
<section className="content">
<ProductsList {...this.props} />
</section>
);
}
}
const mapStateToProps = state => {
return {
products: state.applyFetchProducts.products,
isLoading: state.applyFetchProducts.isLoading,
itemsPerPage: state.applyItemsPerPage
}
}
export default connect(
mapStateToProps
)(Products);
Thanks in advance.
You can use Redux's store.subscribe to listen for changes.
componentWillMount() {
this.unsubscribeStore = store.subscribe(this.onStoreChange);
this.setState({
currentState: store.getState()
});
}
componentWillUnmount() {
this.unsubscribeStore();
}
onStoreChange = () => {
const newState = store.getState();
// Check the relevant part of store for changes.
// Depends on your store implementation
if (newState.reducer.prop !== this.state.currentState.reducer.prop) {
this.fetch();
}
}
this.onStoreChange will get called on each dispatch, so be careful not to create infinite loops. That is also the reason why you have to manually check for changes when the callback function is executed.

Geolocation - can't get latitude and longitude in React/Redux app

I have following action creator:
export const getLocation = () => {
const geolocation = navigator.geolocation;
const location = new Promise((resolve, reject) => {
if (!geolocation) {
reject(new Error('Not Supported'));
}
geolocation.getCurrentPosition((position) => {
resolve(position);
}, () => {
reject (new Error('Permission denied'));
});
});
return {
type: GET_LOCATION,
payload: location
}
};
And following reducer for GET_LOCATION type:
case GET_LOCATION: {
return {
...state,
location: {
latitude: action.location.coords.latitude,
longitude: action.location.coords.latitude,
}
}
}
I try to use this data in my component like so:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getLocation } from '../actions';
import { bindActionCreators } from 'redux';
class UserLocation extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.getLocation();
}
render() {
return (
<div>
<div><span>latitude:</span>{this.props.location.latitude}
</div>
<div><span>longitude:</span>{this.props.location.longitude} </div>
</div>
);
}
}
const mapDispatchToProps = dispatch => {
return bindActionCreators( { getLocation }, dispatch )
}
export default connect(null, mapDispatchToProps)(UserLocation);
But everytime when I load this component I get TypeError: Cannot read property 'latitude' of undefined
Can you please point me where I was wrong?
The geolocation data will be resolved asynchronously, and is not available the first time your component is rendered. You need to correctly handle the case where that data is not yet available, and there's several ways you can do that. Please see the article Watch Out for Undefined State for descriptions of how to handle data that is not available.
You should use componentWillReceiveProps to receive the returned data from dispatched action.
componentWillReceiveProps = (nextProps) => {
if (this.props.location.latitude !== nextProps.location.latitude) {
// You will have location object here that is returned from recuder
}
}
Check this alternative way to call geocoding to fetch lat and long in react with redux
https://medium.com/#eesh.t/auto-detect-location-react-component-using-google-geocoding-and-reverse-geocoding-in-autocomplete-66a269c59315
You're doing the request in componentWillMount() that it's before mount the component, but this doesn't not mean that the component will wait for the request end. Therefore you should put some validation in the render method like if(!this.props.location) return <div>Loading...</div>
I think I used too the example from the same Codepen as you, in it the author uses a very simplified version of a React Promise Middleware.
But suggest that you use a redux-promise-middleware so you can control how the promise will be resolved.
So you should import the middleware and include it in applyMiddleware when creating the Redux store:
import promiseMiddleware from 'redux-promise-middleware'
composeStoreWithMiddleware = applyMiddleware(
promiseMiddleware(),
)(createStore)
The promise that you're dispatching in your action creator must be handled in the reducer with a FULFILED promise suffix (there are another ways to do the same but you should be ok with this one, also I'm not a native English speaker, so watchout with typos in _FULFILLED).
case `${GET_LOCATION}_FULFILLED`:
return {
...state,
location: {
latitude: action.location.coords.latitude,
longitude: action.location.coords.latitude,
}
}
I use the location object from the render() method so you can use const mapStateToProps so you can use it from there.
const mapStateToProps = (state) => {
return {location: state.location};
};
Then at the render method of your component you can destructure your location object from the props:
render() {
const { location: { latitude, longitude } } = this.props;
return (
<div>
<div><span>latitude:</span>{latitude}</div>
<div><span>longitude:</span>{longitude}</div>
</div>
);

confusing about load data based on router params in react component

I want to load data from api based on router params in component,
The channel page behave as expected when I first open the page, but if I go to other channel page by clicking, ChannelPage component didn't call componentDidMount, but reducer received FETCH_MESSAGES action, the Sidebar component also have the problem. redux-devtools can only received LOCATION_CHANGE action when other channel page get clicked. it's too weird!
What's the best practice for loading data based on params in react component?
Sidebar
class Sidebar extends React.Component {
componentDidMount() {
this.props.fetchChannels(1);
}
openChannelPage = (e, url) => {
e.preventDefault();
console.log('open channel page');
this.props.changeRoute(url);
};
render() {
let channelsContent = null;
if (this.props.channels !== false) {
channelsContent = this.props.channels.map((item, index) => (
<ChannelItem
routeParams={this.props.params} item={item} key={`item-${index}`} href={item.url}
handleRoute={(e) => this.openChannelPage(e, item.url)}
/>
), this);
}
return (
<div id="direct_messages">
<h2 className={styles.channels_header}>messages</h2>
<ul>
{channelsContent}
</ul>
</div>
);
}
}
const mapStateToProps = createStructuredSelector({
channels: selectChannels(),
});
const mapDispatchToProps = (dispatch) => ({
changeRoute: (url) => dispatch(push(url)),
fetchChannels: () => dispatch(fetchChannels()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);
ChannelPage
class ChannelPage extends React.Component {
componentDidMount() {
console.log('##Fetch history messages##');
this.props.fetchMessages(this.props.channel);
}
render() {
return (
{this.props.channel.name}
);
}
}
const mapStateToProps = createStructuredSelector({
channel: selectChannel(),
});
export default connect(mapStateToProps, {
fetchMessages,
})(ChannelPage);
appReducer
function appReducer(state = initialState, action) {
switch (action.type) {
case RECEIVE_CHANNELS:
console.log('received channels');
return state;
case FETCH_CHANNELS:
console.log('fetching channels');
return state;
case FETCH_MESSAGES:
console.log('fetching history messages---WTF');
return state;
default:
return state;
}
}
export default appReducer;
reducer received unexpected FETCH_MESSAGES action
redux-devtools can only received LOCATION_CHANGE action when go to other channel page
Which one is the right behavior?
There are two questions
What's the best practice for loading data based on params in react component?
You can use componentDidUpdate() to dispatch request based on router params to get data from API
reducer received unexpected FETCH_MESSAGES action when navigate from messages/1 to messages/2
It's an expected behavior, reducer will receive previous action when navigate from messages/1 to messages/2, and the componentDidMount already ran once so it will never be called

Resources