Does React batch props updates in some cases? - reactjs

I'm curious whether React batches updates to props in some rare cases? There is no mention of this in the docs, but I couldn't come up with any other explanation of the following situation.
I have an equivalent to the following code:
// Connected component
class MyComponent extends React.Component {
state = {
shouldDisplayError: false,
};
componentDidUpdate(prevProps) {
console.log("componentDidUpdate: " + this.props.dataState);
if (
prevProps.dataState === "FETCHING" &&
this.props.dataState === "FETCH_FAILED"
) {
this.setState(() => ({ shouldDisplayError: true }));
}
}
render() {
return this.state.shouldDisplayError && <p>Awesome error message!</p>;
}
}
const mapStateToProps = (state) => {
const dataState = getMyDataStateFromState(state);
// dataState can be "NOT_INITIALIZED" (default), "FETCHING", "FETCH_SUCCEEDED" or "FETCH_FAILED"
console.log("mapStateToProps: " + dataState);
return {
dataState,
};
};
export default connect(mapStateToProps)(MyComponent);
// A thunk triggered by a click in another component:
export async const myThunk = () => (dispatch) => {
dispatch({ type: "FETCHING_DATA" });
let result;
try {
result = await API.getData(); // an error thrown immediately inside of here
} catch (error) {
dispatch({ type: "FETCHING_DATA_FAILED" });
return;
}
dispatch({type: "FETCHING_DATA_SUCCEEDED", data: result.data});
}
// Let's say this is the API:
export const API = {
getData: () => {
console.log("> api call here <");
throw "Some error"; // in a real API module, there's a check that would throw in some cases - this is the equivalent for the unhappy path observed
// here would be the fetch call
},
}
What I would expect to see in the console after triggering the API call (which immediately fails), is the following:
mapStateToProps: FETCHING
componentDidUpdate: FETCHING
> api call here <
mapStateToProps: FETCH_FAILED
componentDidUpdate: FETCH_FAILED
However, I can see the following instead:
mapStateToProps: FETCHING
> api call here <
mapStateToProps: FETCH_FAILED
componentDidUpdate: FETCH_FAILED
So the MyComponent component never received the "FETCHING" dataState, although it has been seen in the mapStateToProps function. And thus never displayed the error message. Why? Is it because such fast updates to a component's props are batched by React (like calls to this.setState() in some cases)???
Basically, the question is: If I dispatch two actions, really quickly after each other, triggering a component's props updates, does React batch them, effectively ignoring the first one?

The first time, a component is rendered, componentDidUpdate is NOT called. Instead, componentDidMount is called. Log to console in componentDidMount as well to see the message.

Related

React-Redux: how to set the state?

I am trying to understand someone else their code but have difficulty understand the interaction between Redux and React.
On a React page, I invoke a Redux action called getSubscriptionPlan. Inside that Redux action, I see it is able to load the correct data (point 1 below). This uses a reducer, in which I can again confirm the correct data is there (point 2 below).
Then the logic returns to the React page (point 3 below). I now would expect to be able to find somewhere in the Redux store the previously mentioned data. However, I can't find that data listed anywhere... not in this.state (where I would expect it), nor in this.props. Did the reducer perhaps not update the store state...?
What am I doing wrong and how can I get the data to point 3 below?
React page:
import { connect } from "react-redux";
import { getSubscriptionPlan } from "../../../appRedux/actions/planAction";
async componentDidMount() {
let { planId } = this.state;
await this.props.getSubscriptionPlan(planId);
// 3. I can't find the data anywhere here: not inside this.state and not inside this.props.
this.setState({plan: this.state.plan});
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.payment.paymentData !== this.props.payment.paymentData) {
this.setState({
checkout: this.props.payment.paymentData,
plan: this.props.payment.paymentData.plan,
});
}
}
const mapStateToProps = (state) => {
return {
plan: state.plan,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(
{ getSubscriptionPlan }, dispatch
);
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Checkout)
);
Redux action:
export const getSubscriptionPlan = (id) => {
let token = getAuthToken();
return (dispatch) => {
axios
.get(`${url}/getSubscriptionPlan/${id}`, {
headers: { Authorization: `${token}` },
})
.then((res) => {
if (res.status === 200) {
// 1. From console.log(res.data) I know res.data correctly now contains the data
return dispatch({
type: GET_PLAN_SUCCESS,
payload: res.data,
});
})
};
};
Reducer:
export default function planReducer(state = initial_state, action) {
switch (action.type) {
case GET_PLAN_SUCCESS:
// 2. I know action.payload, at this point contains the correct data.
return { ...state, plan: action.payload };
default:
return state;
}
}
You are getting tripped up on how Redux works.
Redux does not use react component state. It manages state separately, and passes that state to components as props. When you call getSubscriptionPlan, you asynchronously dispatch an event to Redux, which handles the event and updates store state in the reducer. This state is the passed to the connected components mapStateToProps function, mapped to props, and then passed as props to your component. Passing new props triggers a componentDidUpdate and a rerender of the component.
A few key things here.
Redux does not interact with component state unless you explicitly set state with props passed from Redux.
Redux is asynchronous. That means that when you make a change to state via dispatch, the change is not immediately available in the component, but only available when new props are passed. It's event driven, not data binding. As a result, in your code you woun't see the plan prop in componentDidMount because at the time componentDidMount the call to getSubscriptionPlan hasn't happened.
You should see the prop populated in this.props in componentDidUpdate and in render before the didUpdate.
When working with react, it's best to think of components as basically functions of props with some extra lifecycle methods attached.

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

Wait for redux action to finish dispatching when using redux saga

I have a redux saga setup which works fine. One of my dispatches is to create a new order, then once that has been created I want to do things with the updated state.
// this.props.userOrders = []
dispatch(actions.createOrder(object))
doSomethingWith(this.props.userOrders)
Since the createOrder action triggers a redux saga which calls an API, there is a delay, so this.props.userOrders is not updated before my function doSomethingWith is called. I could set a timeout, but that doesn't seem like a sustainable idea.
I have read the similar questions on Stack Overflow, and have tried implementing the methods where relevant, but I can't seem to get it working. I'm hoping with my code below that someone can just add a couple of lines which will do it.
Here are the relevant other files:
actions.js
export const createUserOrder = (data) => ({
type: 'CREATE_USER_ORDER',
data
})
Sagas.js
function * createUserOrder () {
yield takeEvery('CREATE_USER_ORDER', callCreateUserOrder)
}
export function * callCreateUserOrder (newUserOrderAction) {
try {
const data = newUserOrderAction.data
const newUserOrder = yield call(api.createUserOrder, data)
yield put({type: 'CREATE_USER_ORDER_SUCCEEDED', newUserOrder: newUserOrder})
} catch (error) {
yield put({type: 'CREATE_USER_ORDER_FAILED', error})
}
}
Api.js
export const createUserOrder = (data) => new Promise((resolve, reject) => {
api.post('/userOrders/', data, {headers: {'Content-Type': 'application/json'}})
.then((response) => {
if (!response.ok) {
reject(response)
} else {
resolve(data)
}
})
})
orders reducer:
case 'CREATE_USER_ORDER_SUCCEEDED':
if (action.newUserOrder) {
let newArray = state.slice()
newArray.push(action.newUserOrder)
return newArray
} else {
return state
}
This feels like an XY Problem. You shouldn't be "waiting" inside a component's lifecycle function / event handler at any point, but rather make use of the current state of the store.
If I understand correctly, this is your current flow:
You dispatch an action CREATE_USER_ORDER in your React component. This action is consumed by your callCreateUserOrder saga. When your create order saga is complete, it dispatches another "completed" action, which you already have as CREATE_USER_ORDER_SUCCEEDED.
What you should now add is the proper reducer/selector to handle your CREATE_USER_ORDER_SUCCEEDED:
This CREATE_USER_ORDER_SUCCEEDED action should be handled by your reducer to create a new state where some "orders" property in your state is populated. This can be connected directly to your component via a selector, at which point your component will be re-rendered and this.props.userOrders is populated.
Example:
component
class OrderList extends React.PureComponent {
static propTypes = {
userOrders: PropTypes.array.isRequired,
createOrder: PropTypes.func.isRequired,
}
addOrder() {
this.props.createOrder({...})
}
render() {
return (
<Wrapper>
<Button onClick={this.addOrder}>Add Order</Button>
<List>{this.props.userOrders.map(order => <Item>{order.name}</Item>)}</List>
</Wrapper>
)
}
}
const mapStateToProps = state => ({
userOrders: state.get('userOrders'),
})
const mapDispatchToProps = {
createOrder: () => ({ type: 'CREATE_ORDER', payload: {} }),
}
export default connect(mapStateToProps, mapDispatchToProps)(OrderList)
reducer
case 'CREATE_USER_ORDER_SUCCEEDED':
return state.update('userOrders',
orders => orders.concat([payload.newUserOrder])
)
If you really do need side-effects, then add those side-effects to your saga, or create a new saga that takes the SUCCESS action.

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.

React Redux Dispatch

What is the best way to call a dispatch to get initial data on a React component. My understanding is that ComponentWillMount is called before render. So in theory if I call dispatch on ComponentWillMount, by the time I hit render and then ComponentDidMount I should have my data in the component's props, right? I'm not seeing that.
I'm seeing that render gets called twice and that on the first go when the component is being initialized, I cannot access the data in props. It also seems like dispatch does not actually get called until the second render. I'm basically looking to have some light shed on the best way to call a dispatch when initially setting up a component. I'm essentially trying to do something like the following where I use a container component to get my data from dispatch and then pass it to a child component as props. But I also want to initialize some state variables in the ContainerComponent and then pass them to the ChildComponent as props. The thing is that the state variables I want to initialize depend on the data returned from dispatch and ideally I would do the initialization in ComponentWillMount or ComponentDidMount.
import React from 'react';
import axios from 'axios';
import { connect } from 'react-redux';
import ChildComponent from './ChildComponent.js';
import { getTransactionsAll } from '../actions/actions.js';
class ContainerComponent extends React.Component {
constructor() {
super();
this.state = {
acctList:[],
acctChecked:[],
categoryList:[]
}
}
componentWillMount() {
console.log("componentWillMount entered");
this.props.get_data();
console.log(this.props.searchProps.transactions_all);//this is undefined meaning the dispatch has not assigned the data yet...??
}
componentDidMount() {
console.log("componentDidMount entered");
console.log(this.props.searchProps.transactions_all);//this is undefined meaning the dispatch has not assigned the data yet...??
}
render() {
console.log("TransactionManagerContainer render entered");
console.log(this.props.searchProps.transactions_all);//this is undefined the first time around meaning the dispatch has not assigned the data yet...??, but is defined on the second call to render after the dispatch has actually occurred...
return <ChildComponent
data={this.props.searchProps.data}/>;
}
const mapStateToProps = (state) => ({
searchProps: state.searchProps
});
export default connect(mapStateToProps, {getTransactionsAll})(TransactionManagerContainer);
Here is my reducer that assigns the state:
import { combineReducers } from 'redux'
import {GET_TRANSACTIONS } from '../actions/actions.js'
import {GET_TRANSACTIONS_ALL } from '../actions/actions.js'
const INITIAL_STATE = { defaultYear: 2016, transactions: []};
function get_transactions(state = INITIAL_STATE, action) {
// console.log("this is in the reducer: get_transactions");
// console.log(action);
switch(action.type) {
case GET_TRANSACTIONS:
// return { ...state, transactions: action.payload };
return Object.assign({}, state, {
transactions: action.payload,
selectedYear: action.selectedYear
})
default:
return state;
}
}
function get_transactions_all(state = INITIAL_STATE, action) {
console.log("this is the value of action in the reducer: get_transactions_all");
console.log(action);
switch(action.type) {
case GET_TRANSACTIONS_ALL:
// return { ...state, transactions: action.payload };
return Object.assign({}, state, {
transactions_all: action.payload
})
console.log("this is the value of state in the reducer after being set");
console.log(state);
default:
return state;
}
}
const rootReducer = combineReducers({
//stateProps: get_transactions,
searchProps: get_transactions_all
})
export default rootReducer
Here are my actions:
import axios from 'axios';
export const GET_TRANSACTIONS = 'GET_TRANSACTIONS';
export function getTransactions(year) {
return function(dispatch) {
axios.get(`http://localhost:3001/api/transfilter?year=${year}&grouping=2`)
.then(response => {
dispatch({
type: GET_TRANSACTIONS,
payload: response.data,
selectedYear: year
});
})
.catch((error) => {
console.log(error);
})
}
}
export const GET_TRANSACTIONS_ALL = 'GET_TRANSACTIONS_ALL';
export function getTransactionsAll(year) {
return function(dispatch) {
axios.get(`http://localhost:3001/api/trans?limit=20`)
.then(response => {
dispatch({
type: GET_TRANSACTIONS_ALL,
payload: response.data
});
})
.catch((error) => {
console.log(error);
})
}
}
I believe your main question is:
What is the best way to call a dispatch to get initial data on a React component?
Getting initial data requests (or any AJAX requests in general) should go in the componentDidMount lifecycle event.
There are a few reasons for this, here are two important:
Fiber, the next implementation of React’s reconciliation algorithm, will have the ability to start and stop rendering as needed for performance benefits. One of the trade-offs of this is that componentWillMount, the other lifecycle event where it might make sense to make an AJAX request, will be “non-deterministic”. What this means is that React may start calling componentWillMount at various times whenever it feels like it needs to. This would obviously be a bad formula for AJAX requests.
You can’t guarantee the AJAX request won’t resolve before the component mounts. If it did, that would mean that you’d be trying to setState on an unmounted component, which not only won’t work, but React will yell at you for. Doing AJAX in componentDidMount will guarantee that there’s a component to update.
Credits: I learned that from here, there is also a discussion here.
Then, there are a lot of smaller question you've raised and it will be hard for me to answer all, but I'll try to cover most:
After reading the above, you now should understand why your data is undefined in componentWillMount and componentDidMount. That's simply because the data has not arrived yet;
It's normal that your data is undefined during the first render of the component. Initial render happens before data arrival;
It's normal that the data is defined during the second render. The dispatch triggers asynchronous data fetch. Right after data comes, a reducer is hit and component gets re-rendered (that's the second re-render).
If the child components in your main component require the data - check in the parent render method if data exists pass internal components conditionally, only if data is present. Like so:
class ContainerComponent extends React.Component {
// ... omitted for brevity
render() {
return (
{ this.props.searchProps.data ?
<ChildComponent
data={this.props.searchProps.data} />
: <p>Loading</p>
}
);
}
}

Resources