I have a component with some internal state (e.g. isLoading) which has access to redux data. In this component I'd like to dispatch a thunk action (api request) resulting in redux data change. After the thunk is completed I need to change the state of my component. As I see it, there are two ways to do so:
Use the promise return by the thunk and do all I need there, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id).then(() => this.setState({ isLoading: false }));
};
Pass a callback to the thunk and fire it from the thunk itself, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id, this.onSaveSuccess);
};
Which one is the correct way to do so?
The safer way is to use the promise implementation, as you'll be sure that function will only run after the promise has been resolved. The second implementation has no inherent flaws, but if anything in your thunk is async, then it will not work correctly since it'll run once the code is reached, not when the code above it finishes executing. When handling anything that can be async (server requests/loading data/submitting data), it's always safer to use Promise implementations.
Probably the best practice for updating component level state or running a callback function based on changing redux state (or any props/state changes for that matter) is to use componentDidUpdate or useEffect:
componentDidUpdate(prevProps, prevState){
if(prevProps.someReduxState !== this.props.someReduxState && this.state.isLoading){
setState({isLoading:false})
}
}
With useEffect:
useEffect(()=>{
if(!props.someReduxState){
setLoading(true)
} else {
setLoading(false)
}
},[props.someReduxState])
However, I might recommend a different approach (depending on the goal especially on initial data fetching) that manages the loading of state within redux:
Initialize your redux state with a loading value instead:
export default someReduxState = (state = {notLoaded:true}, action) => {
switch (action.type) {
case actions.FETCH_SOME_REDUX_STATE:
return action.payload;
default:
return state;
}
}
then in your component you can check:
if (this.props.someReduxState.notLoaded ){
// do something or return loading components
} else {
// do something else or return loaded components
}
Related
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.
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);
}
I was under the impression that redux was synchronous but it does not seem to be in this case. I have this:
loadData() {
const filterData = {};
Object.values(this.props.filters).map(f => {
filterData[f.filter] = f.selectedValue || '';
});
this.props.fetchData({filters: filterData}); //redux call to load data
}
filterOnSubmit(filter, value, display) {
const newFilter = { filter, selectedValue: value, display };
this.props.updateFilter(newFilter);
this.loadData();
}
But the value of f does not include the selected value that is set BUT when I look at my redux devtools it is there. If I delay the mapping (or delay the loadData via putting it in a setState callback) it is there also.
action and reducer for reference:
case UPDATE_FILTER: {
const newFilter = { ...state[action.payload.filter], ...action.payload };
return { ...state, [action.payload.filter]: newFilter };
}
export function updateFilter(newFilterData) {
return {
type: UPDATE_FILTER,
payload: newFilterData
};
}
Although the redux store update is synchronous, the changes are not reflected in the same React cycle, the updates go though a re-render state, calling update lifecycle method.
After updating the store, you can call the actions that need to be called on store update in lifecycle method like componentDidUpdate (or componentWillReceiveProps which is now soon to be deprecated) or otherwise pass the filters to the loadData function directly
loadData(filters) {
const filterData = {};
Object.values(filters).map(f => {
filterData[f.filter] = f.selectedValue || '';
});
this.props.fetchData({filters: filterData}); //redux call to load data
}
filterOnSubmit(filter, value, display) {
const newFilter = { filter, selectedValue: value, display };
this.props.updateFilter(newFilter);
this.loadData(newFilter);
}
Using componentDidUpdate, you would write it like
componentDidUpdate(prevProps) {
// you have to perform an isEqual check since simply using '===' will not work for comparing arrays
if(!_.isEqual(prevProps.filters, this.props.filters)) {
this.loadData();
}
}
loadData() {
const filterData = {};
Object.values(this.props.filters).map(f => {
filterData[f.filter] = f.selectedValue || '';
});
this.props.fetchData({filters: filterData}); //redux call to load data
}
filterOnSubmit(filter, value, display) {
const newFilter = { filter, selectedValue: value, display };
this.props.updateFilter(newFilter);
}
You're on the right way, use componentDidUpdate to start a new fetch request. A thing you're possibly missing is an if-condition for changes of filter prop.
Docs remind about this:
You may call setState() immediately in componentDidUpdate() but note
that it must be wrapped in a condition like in the example above, or
you’ll cause an infinite loop.
And for your case it might be something like this, depending on the shape of filters:
componentDidUpdate(prevProps) {
if (this.props.filter !== prevProps.filter)
this.loadData();
}
Depending on code outside, you also might consider using a deep comparison inside if. But, of course, the best way would be to stay with === comparison and optimize mapStateToProps from outside (e.g. use meomization).
Do as SAmab said or move this
Object.values(this.props.filters).map(f => {
console.log(f);
filterData[f.filter] = f.selectedValue || '';
});
to your render function.
I am struggling to figure out why a change to an object located in the store handled by a redux reducer is not triggering the componentDidUpdate method inside of my react component. I am using the react developer tools and can see the correct store after the state is reduced, and am also using redux logger and can see the correct after state after the reducer makes the change. But the component still never calls the update method.
action
export const GSAP_ANIMATION = 'GSAP_ANIMATION';
export const animateGsap = (key, next) => {
return {
type: GSAP_ANIMATION,
payload: {
key: key,
next: next
}
}
}
reducer
case GSAP_ANIMATION:
return Object.assign({}, state, {
...state,
gsap: {
...state.gsap,
[payload.key]: {
...state.gsap[payload.key],
next: {
...payload.next
}
}
}
});
component connection
const mapStateToProps = (state, ownProps) => {
return {
component: state.priorities.gsap[ownProps.id]
};
}
const mapDispatchToProps = (dispatch) => {
return {
addGsap: (key) => dispatch(actions.addGsap(key))
};
}
GsapComponent = connect(mapStateToProps, mapDispatchToProps)(GsapComponent);
In the GsapComponent I have the componentDidUpdate method, but this method is never called. However, I can see that the value of this.props.component should be correct when I view the component in the chrome extension.
edit
also doing { JSON.stringify(this.props.component) } correctly shows the updated prop values. Nothing in the react component update lifecycle is every triggered though.
I have also tried to use the immutibility-helper from react like so
return update(state, {
gsap: {
[payload.key]: {
$merge: { next: payload.next }
}
}
});
but it still doesn't call the lifecycle method.
GsapComponent source code.
Check this object assign documentation. Section Examples -> Warning for Deep Clone. I think that your reducer return object is === as state object so react can't detect change. Try json.parse(json.stringify) workaround or use immutable-js.
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>
}
);
}
}