I'd like to know in which different ways can we use async/await in a React Redux (with thunk)
and if there are good conventions, what are they? I think it'll be nice to have:
In the container, not have to use the promise then(), as in this.props.myAction.then(...)
Can we NOT have the async keyword in the class method, but in the method body instead? For example:
async doSomething () {
const data = await this.props.myAction()
console.log(data)
}
// But maybe (it's failing for me, if the action is similar to my working action example *see below)
doSomething () {
const handler = async () => await this.props.myAction() // I guess the await here is redundant, but added to be clear to you
const data = handler()
console.log(data)
}
My working solution at the moment follows:
// MOCK data
const MOCK_DATA = [1, 2, 3, 4, 5]
// Action
export function myAction (payload) {
return async (dispatch) => {
const getter = () => {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({serverResponse: MOCK_DATA})
}, 1200)
})
return promise
}
try {
return await getter()
} catch (error) {
console.log(error)
}
}
}
// Container
class Foobar extends Component {
async doSomething () {
const data = await this.props.myAction()
}
render () {
return (
<div>
<button onClick={this.doSomething}>Do something!</button>
</div>
)
}
}
The problem with "await" is you are Blocking the event loop and with Thunk you have to handle the Promises & dispatcher directly.
There is an alternative to Thunk that is more easy to use. redux-auto
from the documantasion
redux-auto fixed this asynchronous problem simply by allowing you to create an "action" function that returns a promise. To accompany your "default" function action logic.
No need for other Redux async middleware. e.g. thunk, promise-middleware, saga
Easily allows you to pass a promise into redux and have it managed for you
Allows you to co-locate external service calls with where they will be transformed
Naming the file "init.js" will call it once at app start. This is good for loading data from the server at start
The idea is to have each action in a specific file. co-locating the server call in the file with reducer functions for "pending", "fulfilled" and "rejected". This makes handling promises very easy.
You example would look like this:
// MOCK data
const MOCK_DATA = [1, 2, 3, 4, 5]
// data/serverThing.js
export default function (data, payload, stage, result) {
switch(stage){
case 'FULFILLED':
return result.serverResponse;
case 'REJECTED':
const error = result;
console.log(error)
case 'PENDING':
default :
break;
}
return data;
}
export function action (payload) {
return Promise.resolve({serverResponse: MOCK_DATA})
}
// Container
import React from "react"
import actions from 'redux-auto'
import { connect } from 'react-redux'
class Foobar extends Component {
const loading = (true === this.props.data.async.serverThing) ? "loading..." : "";
render () {
return (
<div>
<button onClick={()=>actions.data.serverThing()}>Do something!</button> { loading }
</div>
)
}
}
const mapStateToProps = ( { data }) => {
return { data }
};
export default connect( mapStateToProps )(Foobar);
It also automatically attaches a helper object(called "async") to the prototype of your state, allowing you to track in your UI, requested transitions.
Related
I use React for fetching voting objects of a GraphQL API, provided by AWS Amplify. Therefore I created following function that works with async/await:
import { useState, useEffect } from 'react';
import { API } from 'aws-amplify';
import { getVote } from 'src/graphql/queries';
const asGetVoting = (id) => {
const [vote, setVote] = useState([]);
const fetchVoting = async () => {
try {
const voteData = await API.graphql({
query: getVote, variables: { id }
});
setVote(voteData.data.getVote);
} catch (error) {
console.log('Fetching error: ', error);
}
};
useEffect(() => {
fetchVoting();
}, []);
return vote;
};
export default asGetVoting;
In my component I call the function above and I want to wait until the whole object is fetched - without success:
import asGetVoting from 'src/mixins/asGetVoting';
const Voting = () => {
const fetchVoting = asGetVoting(id);
fetchVoting.then((voting) => {
console.log('Voting completely loaded and ready to do other stuff');
}).catch((error) => {
console.log(error);
});
return (
<div>
some code
</div>
);
};
export default Voting;
Any idea what I am doing wrong? Respectively how can I wait until the object is loaded for querying its content? Or is my fetching function (asGetVoting) built in a wrong way? Do I mix async/await stuff with promises?
Thank you for your appreciated feedback in advance.
I think this is a little more complex than it needs to be. If API is returning a promise, you could set your state using .then to ensure the promise has resolved (I didn't included it but should probably add a catch statement as well). Something like:
const asGetVoting = (id) => {
const [vote, setVote] = useState([]);
useEffect(() => {
API.graphql({
query: getVote, variables: { id }
}).then(result => setVote(result.data.getVote))
}, []);
return (
// Whatever logic you are using to render vote state
<div>{vote}</div>
)
};
I have a mern app and i'm using redux to maintain state of my posts, I want to fetch all data from my api at first run of the app (when the app component loads initially) but I can't achieve it. It only works when I post something and it fetches the post, but it doesn't fetch all the posts from db initially.
After struggling for a day I decided ask here.
This is my component tree:
In my PostsBody, I want to fetch all the posts from the database whenever the app loads initially (this is not happening) and then whenever there is a change in state like create, delete it should fetch the updated posts (this is happening).
This is my PostsBody component:
import React, { useEffect } from "react";
import { useDispatch, useSelector } from 'react-redux';
import Post from "./Post";
import { getPostsAction } from '../actions/posts';
const PostsBody = () => {
const dispatch = useDispatch();
// fetching posts
useEffect(() => {
dispatch(getPostsAction);
}, [dispatch]);
const posts = useSelector((globalState) => globalState.postsReducer);
console.log(posts); // intially empty when the app reloads/renders.
return (
// simply posts.map to display individual posts
);
}
export default PostsBody;
Action:
export const getPostsAction = () => async (dispatch) => {
try {
const { data } = await getAllPosts();
const action = {
type: 'GET_ALL',
payload: data,
}
dispatch(action);
} catch (error) {
console.log(error.message);
}
}
GET CALL:
import axios from 'axios';
const url = "http://localhost:5000/users";
export const getAllPosts = () => axios.get(url);
Reducer:
const postsReducer = (posts=[], action) => {
switch (action.type) {
case 'GET_ALL':
return action.payload;
case 'CREATE':
return [...posts, action.payload];
default: return posts;
}
}
export default postsReducer;
I repeat, the only problem is, it is not fetching all the posts from db initially when the app renders, after that when I create a new post it does fetch that post (not all from db).
Issues
It doesn't appear as though you are invoking the getPostsAction action creator correctly. Also, with only dispatch in the useEffect's dependency array the hook callback will only be invoked once when the component mounts.
Solution
Invoke the getPostsAction action.
useEffect(() => {
dispatch(getPostsAction()); // <-- invoke
}, [dispatch]);
Now this still only solves for fetching posts from the DB when the component mounts, but not when new posts are POST'd to your backend.
I've looked at your actions and state. Normally you would include another variable in the useEffect dependency array to trigger the effect callback to execute again, but I think a simpler way is possible. Instead of POST'ing the new post and dispatching the CREATE action you should POST the new "post" and immediately GET all posts and dispatch the GET_ALL action instead with that data.
export const createPostAction = (newPostData) => async (dispatch) => {
try {
await createPost(newPostData);
getAllPosts()(dispatch);
} catch (error) {
console.log(error.message);
}
}
I've basic familiarity with Thunks, but if the above doesn't work then you may need to duplicate some behavior, or factor it out into some common utility code used by both action creators.
export const createPostAction = (newPostData) => async (dispatch) => {
try {
await createPost(newPostData);
const { data } = await getAllPosts();
const action = {
type: 'GET_ALL',
payload: data,
}
dispatch(action);
} catch (error) {
console.log(error.message);
}
}
My context looks like this:
class AuthStoreClass {
authUser = null
constructor() {
makeAutoObservable(this)
}
login = async (params) => {
const { data: { data: authUser } } = await loginUser(params)
this.authUser = authUser
}
}
const AuthStoreContext = React.createContext(null);
export const authStoreObject = new AuthStoreClass()
export const AuthStoreProvider = ({ children }: any) => {
return <AuthStoreContext.Provider value={authStoreObject}>{children}</AuthStoreContext.Provider>;
};
export const useAuthStore = () => {
return React.useContext(AuthStoreContext);
};
And I am using the context somewhere else in a component:
const LoginPage = observer(() => {
const authStore = useAuthStore()
...
authStore.login(...)
The last line reports the following warning:
[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: AuthStoreClass#1.authUser
Everything works as expected. How can I fix this issue?
Your login function is async and you need to use runInAction inside, or handle result in a separate action, or use some other way of handling async actions:
import { runInAction, makeAutoObservable } from "mobx"
class AuthStoreClass {
authUser = null
constructor() {
makeAutoObservable(this)
}
login = async (params) => {
const { data: { data: authUser } } = await loginUser(params)
// Wrap all changes with runInAction
runInAction(() => {
this.authUser = authUser
})
// or do it in separate function
this.setUser(authUser)
}
// This method will be wrapped into `action` automatically by `makeAutoObservable`
setUser = (user) => {
this.authUser = user
}
}
That is because, citing the docs, every step ("tick") that updates observables in an asynchronous process should be marked as action. And the code before the first await is in a different "tick" than the code after await.
More about async actions (you can even use generators!): https://mobx.js.org/actions.html#asynchronous-actions
In MobX version 6 actions are enforced by default but you can disable warnings with configure method:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
But be careful doing it though, the goal of enforceActions is that you don't forget to wrap event handlers and all mutations in an action. Not doing it might cause extra re-runs of your observers. For example, if you changing two values inside some handler without action then your component might re-render twice instead of once. makeAutoObservable wraps all methods automatically but you still need to handle async methods and Promises manually.
You can also change the function to use the yield syntax, negating the need for runInAction.
*login() {
const { data: { data: authUser } } = yield loginUser(params)
this.authUser = authUser
}
I'm trying to figure out the best way to display a sweetalert message after a successful async action. So I have an ExcursionDetail component that allows you to book the excursion. Here is the simplified component:
class ExcursionDetails extends Component {
bookExcursion() {
const userId = jwt_decode(localStorage.getItem('token')).sub;
this
.props
.actions
.bookExcursion(userId, this.props.match.params.id);
}
render() {
....
<RaisedButton label="Book Excursion" onClick={e => this.bookExcursion()}/>
....
)
}
}
const mapStateToProps = (state, ownProps) => {
return {excursion: state.excursion}
}
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(ExcursionActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ExcursionDetails);
The action creator:
export const bookExcursion = (userId, excursionId) => {
return (dispatch, state) => {
dispatch(requestBookExcursions())
return ExcursionApi
.bookExcursion(userId, excursionId)
.then(resp => {
if (resp.ok) {
return resp
.json()
.then(payload => {
dispatch(bookExcursionsSuccess(payload.data));
})
}
}).catch(err => {
dispatch(bookExcursionsFailed(err));
})
}
}
What would be the best practice to then display the sweet alert notification? The options I thought of were:
Add a bookSuccess property that I can view if true or false in my ExcursionDetails component and if true call the sweetalert function.
Create notification specific actions and reducers and listen for it in my components. Only issue with this is I would need to have some sort of setTimeout after every notification call to clear the notification reducer and this seems a bit hacky.
call the sweet alert function within my reducer
pass a callback to the action creator
redux-thunk returns a promise; however even if the http call fails it will return a successful promise so this option doesn't seem viable.
I would and is using the first option that you mentioned.
I have created a new component and pass the redux store using connect. I check for it if the value is true on componentWillReceiveProps and set the state according and then you can display your sweetalert.
Well you can call it in the action creator.
You can use something like toastr.
Simple and clean.
export const bookExcursion = (userId, excursionId) => {
return (dispatch, state) => {
dispatch(requestBookExcursions())
return ExcursionApi
.bookExcursion(userId, excursionId)
.then(resp => {
if (resp.ok) {
return resp
.json()
.then(payload => {
dispatch(bookExcursionsSuccess(payload.data));
//Here is your notification.
toastr.success('Have fun storming the castle!', 'Miracle Max Says')
})
}
}).catch(err => {
dispatch(bookExcursionsFailed(err));
})
}
}
I am using thunk middleware and I have two asynchronous actions creators like following.
export const fetchObject = () => {
return dispatch => {
let action = fetchObjectRequest();
dispatch(action);
let url = "URL1";
let promise = axios.get(url)
.then(response => dispatch(fetchObjectSuccess(response.data)));
return handlingErrorsPromise(promise,
error => {
console.error(JSON.stringify(error));
dispatch(errorOccurred(action, error))
}
);
}
};
Let's assume I have Object1 and Object 2 endpoints, but the problem is Object1 is required by almost all components and I have to somehow merge all other objects with data from Object1.
Ex: Object2 contains peoples id and I have to attach them names from Object1.
Currently I am mapping my component properties to both objects, and I have if statements in render checking if all object are fetched. Like this:
class Peoples extends Component {
componentWillMount(){
this.props.fetchObject1();
this.props.fetchObject2();
}
render() {
let peoples = this.mergeObjects();
//rendering
}
mergeObjects = () => {
let isFetching = this.props.object1.isFetching ||
this.props.object2.isFetching;
if (isFetching) {
return {
isFetching,
json: []
};
}
let mergedJson = {...};
return {
isFetching,
json: mergedJson
};
};
}
const mapDispatchToProps = dispatch => {
return {
fetchObject1: () => dispatch(fetchObject1()),
fetchObject2: () => dispatch(fetchObject2())
}
};
const mapStateToProps = state => {
return {
object1: state.object1,
object2: state.object2
};
};
export default Peoples = connect(mapStateToProps, mapDispatchToProps)(Peoples);
Is there a more elegant way to merge one asynchronous object with others in my store?
I believe you could use the Promise.all api since axios returns a promise.
Promise.all([this.props.fetchObject1(), this.props.fetchObject2()]).then((data) => {
//do stuff with data
})
Update:
You can trigger an action in your view this.props.action.fetchObjects() that does the Promise.all or yield all[] if you're using redux-saga and then trigger another action that updates your store with both of them at once (merged or not). You can easily merge them in your view or even in the selector function.