I'm working in a React + Redux + redux-thunk codebase and I'm seeing some odd behavior. If I attempt to execute TWO actions in componentWillMount, the second action will infinitely loop.
Here's the componentWillMount:
componentWillMount() {
const { actions } = this.props;
// Action #1 (synchronous)
actions.openLoader();
// Action #2 (promise-based fetch)
actions.getListingsPurchased().then(() => {
actions.closeLoader();
})
.catch(error => {
console.error(error);
});
}
The first action, openLoader() is a simple state update. The second action does a fetch to the server. Action file here:
export function openLoader() {
return {
type: TYPES.SET_LOADER_OPEN
};
}
export function getListingsPurchased() {
return dispatch => {
return fetch'URL GOES HERE', { 'credentials': 'include' })
.then(response => {
return response.json();
})
.then(response => {
return dispatch({ type: TYPES.SET_LISTINGS, data: response.data });
});
};
}
If I was to remove the first action openLoader() from componentWillMount the infinite loop does not happen. Otherwise the fetch call will keep repeating endlessly.
Any help would be appreciated, I seem to have hit a wall.
I believe the best place for breaking infinite loop is in Redux reducer. Reducer is the place where you have to decide if you going to update the state of your app -> will trigger re-render of your components -> will trigger fetch action.
So try to put in place some reducer condition where you can recognize that state was already fetched before and you not going to update the state.
Related
For my app's signup form, I am maintaining the form values with local component state, but the currentUser state, error state, and the API calls are in Redux.
What I'd like to happen is when the form is submitted, the button has a loading spinner and the form values remain until a response is returned from the server. If the server responds with an authorized user, redirect to app. If there's an error, the form's values should not be cleared.
The problem seems to be that Redux is clearing my form values when it dispatches any state updating function (whether removing an error or making the API call to authorize the user). Is there any way to avoid this happening?
From my AuthForm.js
const submitData = () => {
setLoading(true);
if (formType === 'reset') {
updatePassword(resetToken, values.password, history)
.then(result => setLoading(false));
} else if (formType === 'forgot') {
forgotPassword(values.email, history);
} else {
console.log(values); // form values still populated
onAuth(formType, values, history)
.then(result => {
console.log('result received'); // values empty
setLoading(false);
if (formType === 'signup') {
history.push('/questionnaire')
} else {
history.push('/app')
}
})
.catch(err => setLoading(false));
}
};
From my redux actions.js file:
export function authUser(type, userData, history) {
return dispatch => {
dispatch(removeError());
console.log('dispatch') // by this time the form values are empty
// unless I comment out the dispatch(removeError()) above,
// in which case we still have values until 'token recevied' below
return apiCall('post', `/users/${type}`, userData)
.then(({ jwt, refresh_token, ...user }) => {
console.log('token received')
localStorage.setItem('jwtToken', jwt);
localStorage.setItem('jwtTokenRefresh', refresh_token);
dispatch(getUser(user.id));
})
.catch(err => {
handleError(dispatch, err);
});
};
}
I'm also logging the values in my AuthForm component. The result is this:
EDIT: It definitely looks like my components are unmounting but it's still not clear why to me, or how to prevent it.
I am trying to memoize the dispatch function but it seems to have no effect.
const dispatch = useDispatch();
const onAuth = useCallback(
(formType, values, history) => {
dispatch(authUser(formType, values, history))
},
[dispatch]
);
Your component is a subscriber to a redux store, thats why it re-renders,
component's state gets initialized to initial states,
Now you need, a way to persist these component states within rendering cycles.
On class component you would tie a value to a instance i.e using this.value == 'value'
so that it remains the same between renders,
On functional component to achieve this you make use of a hook called useRef
which doesnt change its value on re-render unless you manually change it,
Hopely it will help
I have been working in a react application that uses redux to maintain states globally. But in a particular component, I intentionally mixed setState with dispatch calls. I just used setState to hide/show modal container and dispatch for doing a network operation. Here is a sample code:
onButtonClick = () => {
this.setState({ showAddModal: true });
}
onButtonInModalClick = data => {
this.props.dispatchSave(data);
this.setState({ showAddModal: false });
};
Now, the problem is setState is not waiting for the dispatchSave to get completed. I even tried using await this.props.dispatchSave(data);. No luck with that. My question here is, Can't we mix up setState with dispatch when doing an asynchronous operation? If yes why? If no, what is wrong with my approach?
summary of what I am trying:
there is a button available, on clicking it, a modal will be opened using setState.
Inside the modal there is a button available, on clicking it, it saves data via dispatch
after performing the operation, again setState will be used to close the modal.
Code used for async action:
const mapDispatchToProps = dispatch => ({
dispatchSave(data) {
dispatch(actions.save(data));
}
});
//---------- Above is from the component file
save(data) {
return async dispatch => {
await httpSrc.save(data);
dispatch(actions.setData({ data: [] }));
};
},
The main problem is the mapDispatchToProps that doesn't return generated by the async action.
Define mapDispatchToProps as an object. It's a shorter way to bind with dispatch, and it will pass on the promise returned from the thunk:
const mapDispatchToProps = { dispatchSave: actions.save };
Now you can await the dispatch:
onButtonInModalClick = async data => {
await this.props.dispatchSave(data);
this.setState({ showAddModal: false });
};
I would also return the data from the action, so it will be available when awaiting the thunk (if needed):
save(data) {
return async dispatch => {
await httpSrc.save(data);
dispatch(actions.setData({ data: [] }));
return data;
};
},
I've made an application and want to add more components which will use the same json I fetched in "personlist.js", so I don't want to use fetch() in each one, I want to make a separate component that only does fetch, and call it in the other components followed by the mapping function in each of the components, how can make the fetch only component ?
here is my fetch method:
componentDidMount() {
fetch("data.json")
.then(res => res.json())
.then(
result => {
this.setState({
isLoaded: true,
items: result.results
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
error => {
this.setState({
isLoaded: true,
error
});
}
);
}
and here is a sandbox snippet
https://codesandbox.io/s/1437lxk433?fontsize=14&moduleview=1
I'm not seeing why this would need to be a component, vs. just a function that the other components use.
But if you want it to be a component that other components use, have them pass it the mapping function to use as a prop, and then use that in componentDidMount when you get the items back, and render the mapped items in render.
In a comment you've clarified:
I am trying to fetch the json once, & I'm not sure whats the best way to do it.
In that case, I wouldn't use a component. I'd put the call in a module and have the module expose the promise:
export default const dataPromise = fetch("data.json")
.then(res => {
if (!res.ok) {
throw new Error("HTTP status " + res.status);
}
return res.json();
});
Code using the promise would do so like this:
import dataPromise from "./the-module.js";
// ...
componentDidMount() {
dataPromise.then(
data => {
// ...use the data...
},
error => {
// ...set error state...
}
);
}
The data is fetched once, on module load, and then each component can use it. It's important that the modules treat the data as read-only. (You might want to have the module export a function that makes a defensive copy.)
Not sure if this is the answer you're looking for.
fetchDataFunc.js
export default () => fetch("data.json").then(res => res.json())
Component.js
import fetchDataFunc from './fetchDataFunc.'
class Component {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
Component2.js
import fetchDataFunc from './fetchDataFunc.'
class Component2 {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
You could also have a HOC that does fetches the data once and share it across different components.
I have two redux actions which call as follows.
export function action1(params) {
//This line is always called.
return (dispatch) => {
//This line is not called the second time.
return MyApi.call1(params)
.then(response => {
// some logic
return dispatch(someFunction1());
})
.catch(error => {
throw(error);
});
};
}
export function action2(params) {
return (dispatch) => {
return MyApi.call2(params)
.then(response => {
// call the first API again
action1();
return dispatch(someFunction2());
})
.catch(error => {
throw(error);
});
};
}
When the view is first loaded, action1 is called within the constructor of the view. Upon performing an action and triggering action2 in the same view, action1 needs to be called on action2's success to get the updated list from the server. Unfortunately, code breaks without any error when action1 is called the second time.
What am I missing here?
You have not dispatched the action1.
dispatch( action1( params ) )
Invoking action1() without dispatch just returns a function. In order to get dispatch in returned function, you should dispatch that function. Then it will be caught by redux-thunk middleware. The middleware will pass dispatch and invoke function.
I'm new to React-Native
I have fetch, through which I get some data. What I want to do is to call another function or update the state, after the request is over and data is ready. Here is my code.
getProducts()
{
return fetch(prodUrl, {method: "GET"})
.then((response) => response.json())
.then((responseData) => {
this.setState({brandList: responseData.products});
console.log("Brands State -> : ",this.state.brandList)
})
.done();
}
I call this getProducts() function in componentWillMount() and trying to use fetched data in render().
After I set the state, I can't see the change when I try to console.log(), most probably because fetch() is async. How can I stop execution of render() function before fetch() is over? Or can you recommend any other request type rather then fetch() which is sync.
It's not because fetch is async, you already have your responseData at that point. It is because setState doesn't change state immediately, so you're console.log is being called before state is being changed. setState has an optional callback as it's second parameter that will be called once set is done being updated, so you can change it like this to see the effect correctly:
getProducts()
{
return fetch(prodUrl, {method: "GET"})
.then((response) => response.json())
.then((responseData) => {
this.setState(
{brandList: responseData.products},
() => console.log("Brands State -> : ",this.state.brandList)
);
});
}
You do not want to "stop" the render() function from being executed. You can, however, apply a check in render if the data is available and render a spinner or something else while it is not.
Very rough sketch of how this could look like:
render() {
let component = this.state.brandList ? <ComponentWithData/> : <Spinner/>;
return component;
}