infinite loop when dispatching in componentWillReceiveProps - reactjs

I have a Profile component that is loaded by react-router (path="profile/:username") and the component itself looks like this:
...
import { fetchUser } from '../actions/user';
class Profile extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const { username } = this.props;
this.fetchUser(username);
}
componentWillReceiveProps(nextProps) {
const { username } = nextProps.params;
this.fetchUser(username);
}
fetchUser(username) {
const { dispatch } = this.props;
dispatch(fetchUser(username));
}
render() {...}
}
export default connect((state, ownProps) => {
return {
username: ownProps.params.username,
isAuthenticated: state.auth.isAuthenticated
};
})(Profile);
And the fetchUser action looks like this (redux-api-middleware):
function fetchUser(id) {
let token = localStorage.getItem('jwt');
return {
[CALL_API]: {
endpoint: `http://localhost:3000/api/users/${id}`,
method: 'GET',
headers: { 'x-access-token': token },
types: [FETCH_USER_REQUEST, FETCH_USER_SUCCESS, FETCH_USER_FAILURE]
}
}
}
The reason I added componentWillReceiveProps function is to react when the URL changes to another :username and to load that users profile info. At a first glance everything seems to work but then I noticed while debugging that componentWillReceiveProps function is called in a infinite loop and I don't know why. If I remove componentWillReceiveProps then the profile doesn't get updated with the new username but then I have no loops problem. Any ideas?

Try adding a condition to compare the props. If your component needs it.
componentWillRecieveProps(nextProps){
if(nextProps.value !== this.props.value)
dispatch(action()) //do dispatch here
}

Your componentWillReceiveProps is in an infinite loop because calling fetchUser will dispatch an action that will update the Props.
Add a comparison to check if the specific prop changes before dispatching the action.
EDIT:
In React 16.3+ componentWillReceiveProps will be slowly deprecated.
It is recommended to use componentDidUpdate in place of componentWillReceiveProps
componentDidUpdate(prevProps) {
if (this.props.params.username !== prevProps.params.username) {
dispatch(fetchUser(username));
}
}
See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change

If you have react routes with some path params like profile/:username,
You can simply compare the props.location.pathname
componentWillReceiveProps(nextProps){
if(nextProps.location.pathname !== this.props.location.pathname){
dispatch()
}
}

Related

React redux current user param gets too late to use

The problem lays in the code below
class GroupsPage extends React.Component {
constructor(props){
super(props);
this.state = {
groups: [],
}
}
async fetchGroups (){
fetchGroupsFirebase().then((res) => {this.setState({groups:res})})
};
async componentDidMount() {
await this.fetchGroups();
}
render(){}
const mapStateToProps = createStructuredSelector({
user: selectCurrentUser
})
export default connect(mapStateToProps)(GroupsPage);
As you see , i call fetchGroups to get some data from firebase, it works allright but i want to get specific data for my current user, my problem is that i can't send de currentUser id as a param to the fetchGroupsFirebase functions, because at the time of the call, this.props.user is still null , and it gets the value from mapStateToProps only after the component mounted.
I hope that i am clear enough, i know it is messy
TLDR: I need the user id but when i get it it's too late
what you would need to do is to first check if the user prop is available right after the mount - if not skip the api call and wait untill the user prop gets updated using componentDidUpdate lifecycle method.
This way, your api call will be made as soon as the user prop gets injected to the component.
class GroupsPage extends React.Component {
constructor(props) {
super(props);
this.state = {
groups: [],
};
}
async fetchGroups(id) {
fetchGroupsFirebase(id).then((res) => {
this.setState({ groups: res });
});
}
async componentDidMount() {
if (this.props.user) {
await this.fetchGroups(this.props.user);
}
}
async componentDidUpdate(prevProps) {
if (this.props.user && this.props.user !== prevProps.user) {
await this.fetchGroups(this.props.user);
}
}
render() {}
}
const mapStateToProps = createStructuredSelector({
user: selectCurrentUser,
});
export default connect(mapStateToProps)(GroupsPage);

How middleware in react life cycle works?

I am new in react js. I have started doing a small product with react-redux. I am using saga middle-ware.
What i have done is as under.
This is the component
//all import work
import { activateAuthLayout, onLoad } from '../../../store/actions';
class EcommerceProductEdit extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
unselected_lists: [],
main_checked: false
}
//here I get the products props always null
console.log(this.props);
}
componentDidMount() {
this.props.activateAuthLayout();
//dispatching an action to fetch data from api, done in midddleware
if (this.props.user !== null && this.props.user.shop_id)
this.props.onLoad({
payload: this.props.user
});
}
render() {
//here I get the products props
console.log(this.props);
return (
//jsx work
);
}
}
const mapStatetoProps = state => {
const { user, is_logged_in } = state.Common;
const { products, is_loading } = state.Products;
return { user, is_logged_in, products, is_loading };
}
export default withRouter(connect(mapStatetoProps, { activateAuthLayout, onLoad })(EcommerceProductEdit));
Action is
import { FETCH_PRODUCT, FETCH_PRODUCT_SUCCESS } from './actionTypes';
export const onLoad = (action) => {
return {
type: FETCH_PRODUCT,
payload: action.payload
}
}
export const productFetched = (action) => {
return {
type: FETCH_PRODUCT_SUCCESS,
payload: action.payload
}
}
Reducer is
import { FETCH_PRODUCT_SUCCESS } from './actionTypes';
const initialState = {
products: null,
is_loading: true
}
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PRODUCT_SUCCESS:
state = {
...state,
products: action.payload,
is_loading: false
}
break;
default:
state = { ...state };
break;
}
return state;
}
And saga is
import { takeEvery, put, call } from 'redux-saga/effects';
import { FETCH_PRODUCT } from './actionTypes';
import { productFetched } from './actions';
import agent from '../../agent';
function* fetchProduct(action) {
try {
let response = yield call(agent.Products.get, action.payload);
yield put(productFetched({ payload: response }));
} catch (error) {
if (error.message) {
console.log(error);
} else if (error.response.text === 'Unauthorized') {
console.log(error)
}
}
}
function* productSaga() {
yield takeEvery(FETCH_PRODUCT, fetchProduct)
}
export default productSaga;
I am being able to get the products props only in render function. How would i be able to get it it in constructor ?
I would be really grateful if anyone explained me about react life cycle a little bit more.
Thanks.
updated
a constructor is called during object instantiation. According to the docs "The constructor for a React component is called before it is mounted". So if the props passed to the component are being changed after the component has been mounted you can use componentWillReceiveProps life cycle methods.
componentWillReceiveProps is deprecated so you can use componentDidUpdate instead. Example from the docs.
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
// update your component state from here.
this.fetchData(this.props.userID);
}
}
MiddleWare: Middleware just comes in between the flow after the action has been dispatched and before it reaches the reducers, like in your case once you fire onLoad action and before it reaches the reducers, its caught in Saga middleware which executes it according to code written in it
Lifecycle in your case goes the following way:
In your compoenentDidMount method, you dispatch an action of onLoad. The action type in such a case becomes "FETCH_PRODUCT" and same action is now caught in Saga.
Since this is async call, the code in your component continues executing while the Saga perform its action in parallel. It calls API through this line of code: yield call(agent.Products.get, action.payload); . Once API call is completed, it dispatches an action 'productfetched' through this line of code yield put(productFetched({ payload: response }));.
Now this action reaches reducer and modify the state of "products". Since the product state in your redux is modified, your component EcommerceProductEdit re-renders and you get your product list in render method. The point to be noted is that the flow must have already finished executing inside componentDidMount method by this time, so no chance of having products their
Solution to your problem:
Once an action is dispatched and which has become async due to Saga, you won't be able to get value in constructor, if you use Saga. You can just directly call upon the API using axios/fetch library in componentDidMount and await their (Making it synchronous). Once you get response, you may proceed further
In case you have functional component, then you may use Effect hook and bind the dependency to products state. You can write your code in this block, what you want to be executed after API call is made and product list modifies.
React.useEffect(
() => {
// You code goes here
},
[products]
);
You just have to console props rather than doing this.props. You should not reference props with this inside the constructor.
Do this instead:
console.log(props)
Middleware is not related to react lifecycle at all, other than it updates and connected components "react" to props updating.
Check the constructor docs
https://reactjs.org/docs/react-component.html#constructor
Question: why are you trying to log props in the constructor anyway? If you want to know what the props are, use one of the lifecycle functions, componentDidMount/componentDidUpdate, don't use the render function to do side-effects like make asynchronous calls or console log.
componentDidMount() {
console.log(this.props);
}
If you must log props in the constructor though, access the props object that was passed as the component won't have a this.props populated yet.
constructor(props) {
super(props);
...
console.log(props);
}

How to fetch updated state in Component while using Redux

I am new to redux, I am getting confused about how to get State from Redux Store when we have multiple Reducer.
let Say this is my combineReducer
import apiCallInProgress from './ApiStattusReducer';
import login from './loginReducer';
const rootReducer = combineReducers({
login,
apiCallInProgress
});
export default rootReducer;
// below is my Login Reducer
const initialState = {
isAuthenticated: false,
user: {}
};
export function loginReducer(state = initialState, action = {}) {
console.log(action.user);
switch (action.type) {
case types.SET_CURRENT_USER:
return {
...state,
isAuthenticated: true,
user: action.user
}
default:
return state
}
}
export default loginReducer;
// now I want to access user from my component,
// I wrote as
function mapStateToProps(state) {
return {
login: state.login
}
}
componentDidMount() {
const { login } = this.props
console.log("set user didmount:" + login.user)
}
componentDidUpdate() {
const { login } = this.props
console.log("set user didupdate:" + login.user)
}
I am not able to get the state of user in the component but when I am pressing login button console.log(action.user) showing proper output in console .
The variable names we mentioned inside combineReducer, the same name do I need to use inside mapStateToProps func to fetch the state. I am very much confused. Someone, please explain.
I think you're doing everything right till you get to the component (although difficult to fully determine without actually testing the code). The main issue is, as far as I can see, you're not actually dispatching the action so login.user is never being set.
The connect method of react-redux has two function parameters - mapStateToProps, which you're using correctly, and mapDispatchToProps, which it doesn't look like you're using.
mapDispatchToProps is a method to pass Redux actions into props, which when invoked, will fire the action, which will in turn be picked up by the reducer, which will return a new state object. Create a new directory called actions in the root of your app. Inside it, create a file called loginActions.js, and within that, put something like the following:
export function setCurrentUser(user) {
return (dispatch) => {
dispatch({ type: 'SET_CURRENT_USER', user)
}
}
import this function into your component, then in your connect function, add mapDispatchToProps
import { connect } from 'react-redux'
import { setCurrentUser } from './actions/loginActions'
// component code
const mapDispatchToProps = {
setCurrentUser: setCurrentUser
}
const mapStateToProps = (state) => {
return {
login: state.login
}
}
connect(mapStateToProps, mapDispatchToProps)(YourComponentName)
In your componentDidMount method, you can now call this action:
componentDidMount() {
this.props.setCurrentUser('myUser')
}
in componentDidUpdate, the user should now be available:
componentDidUpdate(prevProps) {
if (prevProps.login !== this.props.login) {
console.log(this.props.login)
}
}
I hope this helps. Let me know if you need any more help.

React Mobx componentDidUpdate is not updating when observable changes

I'm new to Mobx and reactjs in general, I have knowledge in Redux and react native, and in Redux when I used to call an action and the props get updated, the componentDidUpdate life cycle method is triggered.
The scenario I'm having now is login. so the user fills the form, clicks submit, and the submit calls a Mobx action (asynchronous), and when the server responds, an observable is updated, and then it navigates to a main page (navigation happens in the component).
Here is my store code.
import { autorun, observable, action, runInAction, computed, useStrict } from 'mobx';
useStrict(true);
class LoginStore {
#observable authenticated = false;
#observable token = '';
#computed get isAuthenticated() { return this.authenticated; }
#action login = async (credentials) => {
const res = await window.swaggerClient.Auth.login(credentials)l
// checking response for erros
runInAction(() => {
this.token = res.obj.token;
this.authenticated = true;
});
}
}
const store = new LoginStore();
export default store;
export { LoginStore };
and this handler is in my component.
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
this.props.store.login(values);
}
});
}
componentDidUpdate() {
if (this.props.store.isAuthenticated) {
const cookies = new Cookies();
cookies.set('_cookie_name', this.props.store.token);
this.props.history.push('main');
}
}
It's not the ideal code, I'm just experimenting, but I'm not quite getting it.
Also, if I use the computed value (isAuthenticated) in the render life cycle method, the componentDidUpdate is triggered, but if I didn't use it in the render method, the componentDidUpdate is not triggered.
For example, if I do this
render() {
if (this.props.store.isAuthenticated) return null
// .... rest of the code here
}
the above will trigger the componentDidUpdate.
Am I missing something? is there a better way to do it with Mobx?
Thanks
Observer component will only react to observables referred in its render method. MobX documentation covers this.
I would recommend you to use when to solve the problem.
componentDidMount() {
when(
() => this.props.store.isAuthenticated,
() => {
// put your navigation logic here
}
);
}
Mobx suggest the following solutions for such a case:
when
autorun
reaction
See the examples below, and don't forget to dispose:
componentDidMount() {
this.disposers.push(
// option with autorun:
autorun(() => {
this.runYourLogicHere();
})
// another option with reaction:
reaction(
() => this.yourModelOrProps.something,
() => {
this.runYourLogicHere();
}
)
)
}
...
componentWillUnmount() {
this.disposers.forEach(disposer => {
disposer();
});
}
And see the answer of #Dominik Serafin in parallel thread as a reference.

Initialize state with async data in Redux

I want to populate two tokens properties via AJAX whenever the state is created. It seems that Redux doesn't have much documentation on this. I can't use componentWillMount to do this because the way I have my containers set up it just won't work.
const Auth = Record({
token: '',
yelpToken: '',
});
Is there someway run a function that will happen before createStore is invoked?
You can replace your index with this:
class EntryPoint extends Components {
constructor(){
this.state = {
//can be replaced with store state..
finishedFetching: false
}
}
componentDidMount() {
//you can chain dispatches with redux thunk
dispatch(fetchAsyncData())
.then(() => this.setState({finishedFetching: true}))
}
render() {
return this.state.finishedFetching ? <App/> : null;
}
}

Resources