Dispatching an action from another action - reactjs

Let's say I've got these Flux actions;
{
"type": "USER_LOADED",
"payload": { ... }
}
{
"type": "USER_LOAD_FAILED",
"payload": { ... }
}
{
"type": "SHOW_ERROR_MODAL",
"payload": { ... }
}
{
"type": "HIDE_ERROR_MODAL"
}
I've got a UserStore (which listens to USER_LOADED and USER_LOAD_FAILED and updates accordingly) and a ModalStore (which listens to SHOW_ERROR_MODAL and updates accordingly).
I have a Modal component which is always present on the page, which renders content from the ModalStore.
What's the best way of showing an error modal when USER_LOAD_FAILED occurs? Should by ModalStore listen to it? I am going to end up with a lot of different types of *_LOAD_FAILED actions, so is this a good idea?
I can't dispatch from the UserStore in response to USER_LOAD_FAILED, as you can't dispatch during a dispatch.
I could dispatch from some "Controller" component, which does something along these lines;
class UserController extends PureComponent {
constructor(...args) {
super(...args);
this.state = { error: null, notified: false };
}
componentDidMount = () => this.props.flux.store('UserStore').on('change', this.onUserChange)
componentDidUpdate = () => {
if (this.state.error && !this.state.notified) {
this.props.flux.actions.showErrorModal(this.state.error);
this.setState({ notified: true });
}
}
componentWillUnmount = () => this.props.flux.store('UserStore').off('change', this.onUserChange)
onUserChange = () => {
const userStore = this.props.flux.store('UserStore');
// UserStore#getError() returns the most recent error which occurred (from USER_LOAD_FAILED).
const error = userStore.getError();
this.setState({ error, notified: error !== this.state.error });
}
render = () => ...
}
But I feel like this is just a workaround as opposed to an actual solution.
One last way I thought of was to just dispatch a SHOW_ERROR_MODAL inside the action creator which originally dispatched USER_LOAD_FAILED, but I still don't know if this is the "advised" way, as you could end up putting lots of logic in there for other cases.

If you are using Promises to make API calls, if you dispatch actions in your resolve or reject functions, it will not be triggered during the current dispatch.
Same is the case with callbacks. By the time your resolve/reject/callback functions execute, your original dispatch will have completed.
Having said that, I'm not sure why you chose to have your modal component be driven by a store. Are you making any API calls or doing some other processing before showing the modal? If not, perhaps you can consider showing/hiding your modal based on the state of your component.

Related

how to work reusable state in class components?

do class components support Reusable State in React 18? I wrote an example
componentDidMount() {
console.log("componentDidMount");
if (!this.mounted) {
this.fetchData();
this.mounted = true;
}
}
fetchData() {
fetch().then((data) => {
this.setState((statePrev) => {
console.log("prev data", statePrev); // always null after editing the file of this component and triggering Fast Refresh, how to work reusable state in class components?
return {
data: [
...(Array.isArray(statePrev?.data) ? statePrev?.data : []),
...data
]
};
});
});
}
I expected that when I edit and save the component file and React Refresh is triggered then statePrev will contain the previous state but it is always null
second problem is when React Refresh is triggered, the this.mounted property also does not work to determine that the mounting has occurred and you do not need to run fetch again

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

Does React batch props updates in some cases?

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.

Should Promises be avoided in React components?

I've recently came across this error in React:
warning.js:36 Warning: setState(...): Can only update a mounted or
mounting component. This usually means you called setState() on an
unmounted component. This is a no-op. Please check the code for the
BillingDetails component.
After digging I found out that this is caused because I do setState in unmounted component like this:
componentWillMount() {
this.fetchBillings(this.props.userType);
}
componentWillReceiveProps({ userType }) {
if (this.props.userType !== userType) {
this.fetchBillings(userType);
}
}
fetchBillings = userType => {
switch (userType) {
case USER_TYPE.BRAND:
this.props.fetchBrandBillings()
.then(() => this.setState({ isLoading: false }));
return;
default:
}
};
fetchBillings is a redux-axios action creator which returns a promise
export const fetchBrandBillings = () => ({
type: FETCH_BRAND_BILLINGS,
payload: {
request: {
method: 'GET',
url: Endpoints.FETCH_BRAND_BILLINGS,
},
},
});
The problem is that when user moves fast on site, component can be unmounted at the time promise resolves.
I found out lot of places around the project where I do something like this:
componentWillMount() {
const { router, getOrder, params } = this.props;
getOrder(params.orderId).then(action => {
if (action.type.endsWith('FAILURE')) {
router.push(`/dashboard/campaign/${params.campaignId}`);
}
})
}
and now I begin to think that using Promises in components could be anti-pattern as component can be unmounted at any time...
The problem is that when user moves fast on site, component can be unmounted at the time promise resolves.
Since native promises are not interruptible, this is completely natural and should be expected at all times. You can overcome this in various ways, but you will ultimately need to track whether the component is still mounted, one way or another, and just don't do anything when the promise resolves/rejects if it's not.
Also, from the docs regarding componentWillMount:
Avoid introducing any side-effects or subscriptions in this method.
Considering this, I'd suggest using componentDidMount for initiating your fetch instead. Overall:
componentDidMount() {
this._isMounted = true;
this.fetchBillings(this.props.userType);
}
componentWillReceiveProps({ userType }) {
if (this.props.userType !== userType) {
this.fetchBillings(userType);
}
}
componentWillUnmount() {
this._isMounted = false;
}
fetchBillings = userType => {
switch (userType) {
case USER_TYPE.BRAND:
this.props.fetchBrandBillings().then(() => {
if (this._isMounted) {
this.setState({ isLoading: false });
}
});
return;
default:
}
};
Additionally, although this is not directly related to your question, you will need to consider that you will have multiple parallel fetch calls running in parallel, leading to a data race. That is, the following is just waiting to happen at any time:
start fetch0
start fetch1
finish fetch1 -> update
...
finish fetch0 -> update
To avoid this, you can track your requests with a timestamp.

Invariant Violation: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch

I am using ALT for my ReactJS project. I am getting the cannot 'dispatch' error if the ajax call is not yet done and I switch to another page.
Mostly, this is how my project is setup. I have action, store and component. I querying on the server on the componentDidMount lifecycle.
Action:
import alt from '../altInstance'
import request from 'superagent'
import config from '../config'
import Session from '../services/Session'
class EventActions {
findNear(where) {
if (!Session.isLoggedIn()) return
let user = Session.currentUser();
request
.get(config.api.baseURL + config.api.eventPath)
.query(where)
.set('Authorization', 'Token token=' + user.auth_token)
.end((err, res) => {
if (res.body.success) {
this.dispatch(res.body.data.events)
}
});
}
}
export default alt.createActions(EventActions)
Store
import alt from '../altInstance'
import EventActions from '../actions/EventActions'
class EventStore {
constructor() {
this.events = {};
this.rsvp = {};
this.bindListeners({
findNear: EventActions.findNear
});
}
findNear(events) {
this.events = events
}
}
export default alt.createStore(EventStore, 'EventStore')
Component
import React from 'react';
import EventActions from '../../actions/EventActions';
import EventStore from '../../stores/EventStore';
import EventTable from './tables/EventTable'
export default class EventsPage extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
events: [],
page: 1,
per: 50
}
}
componentDidMount() {
EventStore.listen(this._onChange.bind(this));
EventActions.findNear({page: this.state.page, per: this.state.per});
}
componentWillUnmount() {
EventStore.unlisten(this._onChange);
}
_onChange(state) {
if (state.events) {
this.state.loading = false;
this.setState(state);
}
}
render() {
if (this.state.loading) {
return <div className="progress">
<div className="indeterminate"></div>
</div>
} else {
return <div className="row">
<div className="col m12">
<h3 className="section-title">Events</h3>
<UserEventTable events={this.state.events}/>
</div>
</div>
}
}
}
componentDidMount() {
EventStore.listen(this._onChange.bind(this));
EventActions.findNear({page: this.state.page, per: this.state.per});
}
This would be my will guess. You are binding onChange which will trigger setState in _onChange, and also an action will be fired from findNear (due to dispatch). So there might be a moment where both are updating at the same moment.
First of all, findNear in my opinion should be as first in componentDidMount.
And also try to seperate it in 2 differnet views (dumb and logic one, where first would display data only, while the other one would do a fetching for example). Also good idea is also to use AltContainer to actually avoid _onChange action which is pretty useless due to the fact that AltContainer has similar stuff "inside".
constructor() {
this.events = {};
this.rsvp = {};
this.bindListeners({
findNear: EventActions.findNear
});
}
findNear(events) {
this.events = events
}
Also I would refactor this one in
constructor() {
this.events = {};
this.rsvp = {};
}
onFindNear(events) {
this.events = events
}
Alt has pretty nice stuff like auto resolvers that will look for the action name + on, so if you have action called findNear, it would search for onFindNear.
I can't quite see why you'd be getting that error because the code you've provided only shows a single action.
My guess however would be that your component has been mounted as a result of some other action in your system. If so, the error would then be caused by the action being triggered in componentDidMount.
Maybe try using Alt's action.defer:
componentDidMount() {
EventStore.listen(this._onChange.bind(this));
EventActions.findNear.defer({page: this.state.page, per: this.state.per});
}
I believe it's because you're calling an action, and the dispatch for that action only occurs when after the request is complete.
I would suggest splitting the findNear action into three actions, findNear, findNearSuccess and findNearFail.
When the component calls findNear, it should dispatch immediately, before even submitting the reuqest so that the relevant components will be updated that a request in progress (e.g. display a loading sign if you like)
and inside the same action, it should call the other action findNearSuccess.
The 'Fetching Data' article should be particularly helpful.

Categories

Resources