Redux thunk: wait for async function to dispatch - javascript

I'm creating a React Native application and using redux and redux-thunk to implement my API requests. I would like to know how I can wait for my action to be dispatched and make sure that my state has been updated in an async thunk logic. If I understand correctly, await will wait for the end of the thunk but the action is not dispatched yet. Although, as you can see in my usage, I need the state to be modified to proceed the rest of the code accordingly.
actions/user.js
export const tryLogin = (
email: string,
password: string,
sessionToken: string = ''
): Function => async (dispatch: Function) => {
const logUser = () => ({ type: LOG_USER })
const logUserSuccess = (data: any, infos: any) => ({
type: LOG_USER_SUCCESS,
data,
infos,
})
const logUserError = (signinErrorMsg: string) => ({
type: LOG_USER_ERROR,
signinErrorMsg,
})
dispatch(logUser())
try {
{ /* Some API requests via axios */ }
dispatch(logUserSuccess(responseJson, infos))
return true
} catch (error) {
{ /* Error handling code */ }
dispatch(logUserError(error.response.data.error))
return false
}
reducers/user.js
case LOG_USER:
return {
...state,
isLoggingIn: true,
}
case LOG_USER_SUCCESS:
return {
...state,
isLoggingIn: false,
data: action.data,
infos: action.infos,
error: false,
signinErrorMsg: '',
}
case LOG_USER_ERROR:
return {
...state,
isLoggingIn: false,
error: true,
signinErrorMsg: action.signinErrorMsg,
}
RegisterScreen.js
if (await trySignup(
emailValue,
firstNameValue,
lastNameValue,
passwordValue,
birthdateValue,
genderValue
)
) {
if (userReducer.data) {
navigation.navigate('Secured')
}

In Redux,
When an Action is dispatched to the store, it will update the state of the UI automatically with new props.
Instead of watching the dispatched action, You can add a flag in the reducer signUpSuccess similar to isLoggingIn flag and listen to the changes in componentDidUpdate lifecycle method.
trySignup can be called separately (like on an event, formSubmit, button click, etc.)
RegisterScreen.js
class RegisterScreen extends React.Component{
...
componentDidUpdate(prevProps) {
if (prevProps.signUpSuccess !== this.props.signUpSuccess){
if (this.props.signUpSuccess) {
navigation.navigate('Secured')
}
}
}
...
}
const mapStateToProps = (state) => ({
signUpSuccess: state.userReducer.signUpSuccess,
});
export default connect(mapStateToProps)(RegisterScreen);

If I understand correctly, await will wait for the end of the thunk
but the action is not dispatched yet.
Render can be update if any changes happens in props, so extract props in your render method and update UX as per change in props.
I would suggest use React native debugger to check your actions
and current saved state.

Related

How to control a boolean in redux?

It seems like I can't dispatch boolean values. Can someone tell me what I am doing wrong?
This is my state:
const initState = {
allSongs: songData(),
currentSong: { ...songData()[0] },
isPlaying: false,
};
How can I control isPlaying with the dispatch function? When I dispatch it like this, it doesn't work:
dispatch(playAndPause(true));
With this being my action creator:
export const playAndPause = (isPlayingBool) => (dispatch) => {
return {
type: PLAY_AND_PAUSE,
payload: {
isPlaying: isPlayingBool,
},
};
};
Reducer:
case PLAY_AND_PAUSE:
return {
...state,
isPlaying: action.payload.isPlaying,
};
What you have there playAndPause is no normal action creator, but a thunk action creator.
That innermost function would be called and allowed you to do asynchronous actions before actually dispatching. If you wanted to go that route, the correct way would be
export const playAndPause = (isPlayingBool) => (dispatch) => {
// do some stuff here
dispatch({
type: PLAY_AND_PAUSE,
payload: {
isPlaying: isPlayingBool,
},
});
};
But really, you probably want a normal action creator:
export const playAndPause = (isPlayingBool) => ({
type: PLAY_AND_PAUSE,
payload: {
isPlaying: isPlayingBool,
},
});
Also, please note that you are writing a pretty old style of redux here. If you are learning redux right now, please stop whatever tutorial you are following and follow the the official tutorials instead.
In the end, you will write maybe 1/4 of the code you will be writing with the current style you are using.
Modern redux does not require you to write any action creators or action type constants for example. It also has no switch...case reducers or immutable logic in reducers.

Redirect is happening before fetch is finished

I have little problem with redirecting back to previous page - it happens too soon.
Basically I have functional component like that:
export const CreateRecordingControls = ({ recording, handleCreateRecording }) => {
const history = useHistory();
const handleCreate = async () => {
const promise = Promise.resolve(handleCreateRecording(recording));
await Promise.all([promise]).then(function () {
history.push('/home');
});
}
return (
<Fragment>
<button onClick={() => handleCreate()}>Create</button>
</Fragment>
);
}
const mapDispatchToProps = dispatch => ({
handleCreateRecording: recording => {
dispatch(handleCreateRecording(recording));
}
});
const CreateRecordingControlsView = withRouter(connect(null, mapDispatchToProps)(CreateRecordingControls));
export default CreateRecordingControlsView;
When user clicks "Create" it will call createRecording action:
export const handleCreateRecording = (recording) => {
return dispatch => {
dispatch(handleCreateRecordingBegin());
fetch(`${config.get('api')}/api/recordings/create`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recording, replacer)
}).then(data => {
dispatch(handleCreateRecordingSuccess(data));
}).catch(error =>
dispatch(handleCreateRecordingFailure(error))
);
}
}
Which goes to reducer:
export default (state = [], action) => {
switch (action.type) {
case 'CREATE_RECORDING_BEGIN':
return {
...state,
loading: true,
error: null,
finished: false
}
case 'CREATE_RECORDING_SUCCESS':
return {
data: action.payload,
loading: false,
finished: true
}
default:
return state;
}
}
But in that case, it still shows that component on '/home' route will mount before handle success is called. Home component contains list of just added recordings. It seems currently it's a race who gets there first. So far newly created recording is in list, but I think this is still wrong behavior.
Any ideas how to wait success case to finish before redirecting?
The dispatch function does not return a promise, so you're not waiting for anything. With redux-thunk if you want to wait for the result of a dispatch, you must do so within your action creator: where you're dispatching your handleCreateRecordingSuccess.
The alternative is to observe the state value finished and push to your history when it gets the value that you're interested in.
The fetch promise doesn't seem to be returned at the moment, so the Promise.resolve will just happen immediately.
I would try returning the actual promise from the fetch, i.e. awaiting the results of the handleCreateRecording fetch then returning that when you dispatch your action. The promise resolve and promise.all shouldn't be necessary. If you want to catch any fetch errors, then you could just wrap the await in a try/catch block.

Async/Await seems to have different behaviour in class methods and function expressions (React Hooks, Redux-Thunk)

I'm migrating a class-based react system to hooks, and I'm facing some challenges which I can't understand.
Take a look at the snippet below:
async onSearchforOptions(elementId) {
await this.props.onFetchOperatingSystems()
//(3) [{…}, {…}, {…}]
console.log(this.props.operatingSystems)
}
In this method, I am dispatching an action to update the redux state, and right after this I'm logging the result to make sure the information was fetched and updated in the redux state.
The problem is that in an application which uses functional components, the result doesn't seem the same. Instead of updating the redux state and recovering the info right after, it simply doesn't seem to update the state, even if I'm using "await" and the very same actions and reducers the class component is using:
const onSearchforOptions = async (elementId) => {
await props.onFetchOperatingSystems()
//[]
console.log(props.operatingSystems)
}
My connection for both components (the class component and the functional component):
const mapStateToProps = state => {
return {
operatingSystems: state.operatingSystemReducer.operatingSystems
}
}
const mapDispathToProps = dispatch => {
return {
onFetchOperatingSystems: () => dispatch(actions.fetchOperatingSystems())
}
}
export default connect(mapStateToProps, mapDispathToProps)(productsForm)
My actions:
export const fetchOperatingSystemsStart = () => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_START
}
}
export const fetchOperatingSystemsFail = (error) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_FAIL,
error: error
}
}
export const fetchOperatingSystemsSuccess = (operatingSystems) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS,
operatingSystems: operatingSystems
}
}
export const fetchOperatingSystems = () => {
return dispatch => {
dispatch(fetchOperatingSystemsStart())
return axios.get(url)
.then(response => {
const fetchedData = []
for (let key in response.data) {
fetchedData.push({
...response.data[key],
id: response.data[key].id
})
}
dispatch(fetchOperatingSystemsSuccess(fetchedData))
})
.catch(error => {
if (error.response !== undefined) dispatch(fetchOperatingSystemsFail(error.response.data))
else dispatch(fetchOperatingSystemsFail(error))
})
}
}
My Reducer:
const initialState = {
operatingSystems: [],
loading: false
}
const fetchOperatingSystemsStart = (state) => {
return updateObject(state, { loading: true })
}
const fetchOperatingSystemsSuccess = (state, action) => {
return updateObject(state, { operatingSystems: action.operatingSystems, loading: false })
}
const fetchOperatingSystemsFail = (state) => {
return updateObject(state, { loading: false })
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_OPERATING_SYSTEMS_START: return fetchOperatingSystemsStart(state)
case actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS: return fetchOperatingSystemsSuccess(state, action)
case actionTypes.FETCH_OPERATING_SYSTEMS_FAIL: return fetchOperatingSystemsFail(state)
default: return state
}
}
export default reducer
updateObject function:
export const updateObject = (oldObject, updatedProperties) => {
const element = {
// The values of the object oldObject are being spread, at the same time the values of
// updatedProperties are (I'm taking out the attributes of both objects with the spread operator).
// In this case, since the names of the attributes are the same,
// the attributes (which were spread) of the first object will have their values replaced
// by the values of the second object's attributes.
...oldObject,
...updatedProperties
}
return element
}
My Goal:
Accoding to the snippet below, my goal is to dynamically search for options and update it in my form, which is in the component state.
const onSearchforOptions = async (elementId) => {
let elementUpdated
switch (elementId) {
case 'operatingSystem': {
await props.onFetchOperatingSystems()
console.log(props.operatingSystems)
elementUpdated = {
'operatingSystem': updateObject(productsForm['operatingSystem'], {
selectValue: {
value: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.id : undefined,
label: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.name : undefined
},
elementConfig: updateObject(productsForm['operatingSystem'].elementConfig, {
options: props.operatingSystems
})
})
}
break
}
case 'productType': {
await props.onFetchProductTypes()
elementUpdated = {
'productType': updateObject(productsForm['productType'], {
selectValue: {
value: props.selectedElement.productType ? props.selectedElement.productType.id : undefined,
label: props.selectedElement.productType ? props.selectedElement.productType.name : undefined
},
elementConfig: updateObject(productsForm['productType'].elementConfig, {
options: props.productTypes
})
})
}
break
}
default: break
}
const productsFormUpdated = updateObject(productsForm, elementUpdated)
setProductsForm(productsFormUpdated)
}
The props object passed to the render function initially is not going to be mutated; rather the props passed to your component on its next render will be updated. This is more in keeping with the flux architecture. You fire-and-forget an action, the reducer runs, and then your component is re-rendered with new props.
Before, this same thing was happening, but the new props were being assigned to this.props again. Since there's no meaningful "this" anymore, you can't use this pattern. Besides, depending on this behavior is not idiomatically the React way of doing things.
Update:
I think this is like a great number of cases I've also encountered where the React team seemed to overcorrect for a lot of use cases of people handling derived state poorly (see You Probably Don't Need Derived State). I've seen plenty of cases, like yours, where the now-deprecated componentWillReceiveProps lifecycle method solved this problem for class-based components very nicely.
Thankfully, useEffect now gives you something like a replacement. Think about it this way: when props.operatingSystems changes, you want to perform the effect of changing the state of your form. It's an unfortunate double update issue, but you had that before. Here's how you could go about writing that:
const [productsForm, setProductsForm] = useState(...);
useEffect(() => {
// Handle the case where props.operatingSystems isn't initialized?
if (!props.operatingSystems || !props.selectedElement.operatingSystem)
return;
setProductsForm({
...productsForm,
operatingSystem: {
...productsForm.operatingSystem,
selectValue: {
value: props.selectedElement.operatingSystem.id,
label: props.selectedElement.operatingSystem.name
},
elementConfig: {
...productsForm.operatingSystem.elementConfig,
options: props.operatingSystems
}
}
});
}, [props.operatingSystems]);
The way this works is that your effect code is only kicked off whenever your props.operatingSystems value changes since the last render. You can do a similar sort of effect for product types.
Another option which is maybe less elegant is for your async function that kicked off the redux actions to also resolve to a value which you can then use in your state setting code:
const operatingSystems = await props.onFetchOperatingSystems();
// ...now set your state
i usually implements thunks in a functional component like:
`export default connect(mapStateToProps, {fetchOperatingSystems})(productsForm)`
can you try this and comment back.

Failed fetch causes infinite component life-cycle loop

With a connected container, I have a reducer that is wrapped by a higher order reducer (show below) to catch and handle errors. When calling a fetch request during componentDidMount and it fails, the connected container will unmount itself componentWillUnmount. This causes an infinite loop in the container as it will mount again, fetch will fail, and container will unmount itself.
Any ideas why having the higher order reducer in the connect component is causing this?
Error handling higher order reducer:
export const errorHandler = (reducer: (state: any, action: { type: string }, initialState: any) => {}) => {
const errorState = fromJS({
error: {
hasError: false,
message: "",
},
});
const initialState = errorState.merge(reducer(undefined, { type: undefined }, undefined));
return (state = initialState, action) => {
switch (action.type) {
case ACTIONS.SET_ERROR:
return state.setIn(["error", "hasError"], true)
.setIn(["error", "message"], action.message);
case ACTIONS.CLEAR_ERROR:
return state.set("error", errorState.get("error"));
default:
return reducer(state, action, initialState);
}
};
};
Example Container:
class Page extends Component {
componentDidMount() {
this.props.fetch(....);
}
componentWillUnmount() {
this.props.clearData();
this.props.cancelRequests();
}
}
export default connect(
(state) => ({
error: state.data.get("error", ""),
}),
{
clearError,
clearData,
cancelRequests,
},
)(Page);
Example Reducer:
export fetch = () => ({
type: ACTIONS.FETCH
});
export default errorHandler((state, action) => {
switch(action.type) {
default:
return state;
}
}));
Epic:
export const fetch = (action$: any, store: any, api: API) => {
return action$.ofType(ACTIONS.FETCH)
.mergeMap((action: any) =>
fromPromise(api.fetch(action.data))
.pluck("Data")
.map(data) =>
fetchFulfilled(data),
)
.catch((response) => {
const toPromise = typeof response.json === "function" ? response.json() : new Promise((resolve) => resolve(response));
return fromPromise(toPromise)
.pluck("Message")
.map((Message: string) =>
setError(Message));
})
.takeUntil(action$.ofType(ACTIONS.CANCEL_REQUESTS)));
};
Based on our conversation in the comments:
Typically components unmount because their parent no longer renders them. What does the parent of look like? It is likely where you would look to find why your component is unmounting.
I'm not aware of any situation where a component can unmount itself (without hacks)
I think you just need catch the error instead of letting the exception be caught by the React mounting code.
try {
this.props.fetch(....);
}
catch (e) {
//Do whatever is appropriate to handle the fetch failure. Maybe you want...
this.setState({ error: {hasError: true, message: '' + e});
}
I think the setState() call above is not right for your intended reducer implementation, but that is a separate problem you can solve (or ask more questions about). The main part of your problem seemed to be stopping the unmount/remount behavior.

Accessing a part of reducer state from one reducer within another reducer

I do not know how to access a boolean isLoading flag from reducerForm.js reducer in reducerRegister.js. I have used combineReducers() and I use isLoading to disable a button during form submit.
It's initial state is false, after clicking submit, it changes to true. After the form submission is successful, isLoading is reset to false again. Below is the relevant code for this issue:
actionRegister.js
let _registerUserFailure = (payload) => {
return {
type: types.SAVE_USER_FAILURE,
payload
};
};
let _registerUserSuccess = (payload) => {
return {
type: types.SAVE_USER_SUCCESS,
payload,
is_Active: 0,
isLoading:true
};
};
let _hideNotification = (payload) => {
return {
type: types.HIDE_NOTIFICATION,
payload: ''
};
};
// asynchronous helpers
export function registerUser({ // use redux-thunk for asynchronous dispatch
timezone,
password,
passwordConfirmation,
email,
name
}) {
return dispatch => {
axios.all([axios.post('/auth/signup', {
timezone,
password,
passwordConfirmation,
email,
name,
is_Active: 0
})
// axios.post('/send', {email})
])
.then(axios.spread(res => {
dispatch(_registerUserSuccess(res.data.message));
dispatch(formReset());
setTimeout(() => {
dispatch(_hideNotification(res.data.message));
}, 10000);
}))
.catch(res => {
// BE validation and passport error message
dispatch(_registerUserFailure(res.data.message));
setTimeout(() => {
dispatch(_hideNotification(res.data.message));
}, 10000);
});
};
}
actionForm.js
export function formUpdate(name, value) {
return {
type: types.FORM_UPDATE_VALUE,
name, //shorthand from name:name introduced in ES2016
value
};
}
export function formReset() {
return {
type: types.FORM_RESET
};
}
reducerRegister.js
const INITIAL_STATE = {
error:{},
is_Active:false,
isLoading:false
};
const reducerSignup = (state = INITIAL_STATE , action) => {
switch(action.type) {
case types.SAVE_USER_SUCCESS:
return { ...state, is_Active:false, isLoading: true, error: { register: action.payload }};
case types.SAVE_USER_FAILURE:
return { ...state, error: { register: action.payload }};
case types.HIDE_NOTIFICATION:
return { ...state , error:{} };
}
return state;
};
export default reducerSignup;
reducerForm.js
const INITIAL_STATE = {
values: {}
};
const reducerUpdate = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.FORM_UPDATE_VALUE:
return Object.assign({}, state, {
values: Object.assign({}, state.values, {
[action.name]: action.value,
})
});
case types.FORM_RESET:
return INITIAL_STATE;
// here I need isLoading value from reducerRegister.js
}
return state;
};
export default reducerUpdate;
reducerCombined.js
import { combineReducers } from 'redux';
import reducerRegister from './reducerRegister';
import reducerLogin from './reducerLogin';
import reducerForm from './reducerForm';
const rootReducer = combineReducers({
signup:reducerRegister,
signin: reducerLogin,
form: reducerForm
});
export default rootReducer;
This is where I use isLoading:
let isLoading = this.props.isLoading;
<FormGroup>
<Col smOffset={4} sm={8}>
<Button type="submit" disabled={isLoading}
onClick={!isLoading ? isLoading : null}
>
{ isLoading ? 'Creating...' : 'Create New Account'}
</Button>
</Col>
</FormGroup>
Mapping state to props within the same component
function mapStateToProps(state) {
return {
errorMessage: state.signup.error,
isLoading: state.signup.isLoading,
values: state.form.values
};
}
This is covered in the Redux FAQ at https://redux.js.org/faq/reducers#how-do-i-share-state-between-two-reducers-do-i-have-to-use-combinereducers:
Many users later want to try to share data between two reducers, but find that combineReducers does not allow them to do so. There are several approaches that can be used:
If a reducer needs to know data from another slice of state, the state tree shape may need to be reorganized so that a single reducer is handling more of the data.
You may need to write some custom functions for handling some of these actions. This may require replacing combineReducers with your own top-level reducer function. You can also use a utility such as reduce-reducers to run combineReducers to handle most actions, but also run a more specialized reducer for specific actions that cross state slices.
Async action creators such as redux-thunk have access to the entire state through getState(). An action creator can retrieve additional data from the state and put it in an action, so that each reducer has enough information to update its own state slice.
A reducer cannot access another reducer's state, but if you're using redux-thunk you can do so from within an action creator. As an example, you can define an action creator like this:
export const someAction = () =>
(dispatch, getState) => {
const someVal = getState().someReducer.someVal;
dispatch({ type: types.SOME_ACTION, valFromOtherReducer: someVal });
};
React Redux works on unidirectional data flow.
Action ---> Reducer /store ---> Reducer
Reducer works on small subset of store, you can not access store inside reducer which is not part of Reducer. you can either need to fire new action from the component based on reducer state return.

Categories