Are there any examples of using redux-loop to handle complex ajax workflows? The official repo is very minimalistic. From the one example in the wild I was able to find (https://hackernoon.com/why-i-wrote-a-redux-async-outerware-277d450dba74#.2ocaloc58), it seems that redux-loop is very similar to redux-thunk.
Here are some examples of complex ajax workflows:
Ajax Workflow1. User selects two different filters on a results table. Each filter initiates an ajax request, which then resolve out of order. The results table should show the correct filter selection. Errors should not update the results table.
Ajax Workflow 2
User starts a report generation (which is a long running process).
User switches to another report. It should either cancel or ignore the pending “wait for report” action.
More complex workflow (based on an old redux-saga example)
User presses log-in button, which starts an ajax request to get an auth token
Either
User immediately presses log-out button, which should cancel/ignore the pending auth action
OR it should store the auth token when it resolves
Should clear the auth token after logout or if login error occurs
I will give a shot at the second workflow (login).
Before going into the code, it's worth noting redux-loop is a lot simpler and offers less than redux-saga in terms of async control flow. But in the spirit of Elm, the focus is on data flow - not surprisingly is typically achieved by data types. Therefore it is helpful to think from the perspective of a statically typed language. In Haskell or Elm, it's probably beneficial to model the problem by data type, which itself encodes a state machine:
data LoginStatus data err =
LoggedOut |
, LoggedIn data |
, LoginError err |
, Pending
Where data and err are type variables represent login data type (tokens) and login errors. JavaScript being dynamically typed, does not have the benefit expressing the same idea - but there are a lot of dynamic tricks one can use to mimic tagged union types like LoginStatus. Without further ago, here's the code:
import {match} from "single-key";
export default function reducer(state, action) {
return match(state, {
LoggedOut : () => loggedOutReducer(state, action),
LoggedIn : () => loggedInReducer(state, action),
Pending : () => pendingReducer(state, action),
LoginError : () => loginErrorReducer(state, action)
});
}
Here I will use a simple and lesser known library singe-key to achieve very basic run time union types. A "single-key" object as it's name suggest, is an object with just a key and a value, such as { a: 1 } ("a" is the key, and 1 is the value). We shall model the state with single-key objects - different keys represent different variants of LoginStatus. A few example states:
{
LoggedOut : true
}
{
LoggedIn : {
token : 1235,
user : { firstName: "John" }
}
}
{
Pending : true
}
With that cleared up, here are the sub-reducers used in the main reducer:
// state :: { LoggedIn: {/* some data * } }
function loggedInReducer(state, action) {
if (action.type === LOGOUT) {
return {
LoggedOut : true
};
}
return state;
}
// state :: { Pending : true }
function pendingReducer(state, action) {
if (action.type === LOGIN_SUCCESS) {
return {
LoggedIn : {
token : action.payload.token,
user : action.payload.user
}
};
}
if (action.type === LOGIN_ERROR) {
return {
LoginError : action.payload;
};
}
if (action.type === LOGOUT) {
return {
LoggedOut : true
};
}
return state;
}
// state :: { LoggedOut : true }
function loggedOutReducer(state, action) {
if (action.type === LOGIN) {
return loop({ Pending: true }, Effects.promise(loginRequest));
}
return state;
}
// state :: { LoginError : error }
function loginErrorReducer(state, action) {
if (action.type === LOGIN) {
return loop({ Pending: true }, Effects.promise(loginRequest));
}
return { LoggedOut : true };
}
These are like transitions in a finite state machine, except sometimes with data attached to the state. Each individual reducers are fairly simple and handles very few action types. Only two reducers return effects:
return loop({ Pending: true }, Effects.promise(loginRequest));
This transitions the state from LoggedOut/LoginError to Pending and specify some side effects - which will be scheduled by redux-loop. You may even merge the two variants into one: { LoggedOut : error | null }, but I feel having a seperate LoginError state is beneficial in the long run.
With some notion of data types, this problem can be easier to reason about than it first appears; you can do the same thing with reducer structured roughly the same and use just redux-thunk.
Related
I have a situation where I should get a song item by id to get the path for that song, and then navigate to that song on button click.
Is there any specific hook that can be used to navigate on data arrival, useEffect will be called any time that state changes but the problem is that first needs to be dispatched the action to get the song, check if it returns any item and then navigate. Typically if it is has been published on the list, it should exist on the db, but the problem might be at the API side, so that check results.length > 0 is why that check is necessary.
useEffect(() => {
const handleClick = (myId: string) => {
dispatch(SongActions.searchSong(myId));
if (results.length > 0) {
if (Object.keys(results[0]).length > 0) {
// navigate(`/songs/${results[0].myPath}`);
}
}
}
}, [dispatch, results])
When user clicks on list item which has a song title, it should call the function handleClick(id) with id of the song as parameter, that is to get the metadata of the song, src path etc.
<Typography onClick={() => handleClick(songItem.songId)} sx={styles.songListItemText}>{songItem.Title}</Typography>
Edit
And this is how I have setup the searchSong action:
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch({
payload: { isLoading: true },
type: SearchActionType.REQUEST,
});
try {
const response = await SearchApi.searchSongAsync(obj);
if (response.length === 0) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch({
type: SearchActionType.RECEIVED_SONG,
payload: { results: response },
});
} catch (e) {
console.error("Error: ", e);
}
}
You appear to be mixing up the purpose of the useEffect hook and asynchronous event handlers like button element's onClick handlers. The useEffect hook is to meant to issue intentional side-effects in response to some dependency value updating and is tied to the React component lifecycle, while onClick handlers/etc are meant to respond to asynchronous events, i.e. a user clicking a button. They don't mix.
Assuming SongActions.searchSong is an asynchronous action, you've correctly setup Redux middleware to handle them (i.e. Thunks), and the action returns the fetched response data, then the dispatched action returns a Promise that the callback can wait for.
Example:
const navigate = useNavigate();
const dispatch = useDispatch();
const handleClick = async (myId: string) => {
const results = await dispatch(SongActions.searchSong(myId));
if (results.length > 0 && Object.keys(results[0]).length > 0) {
navigate(`/songs/${results[0].myPath}`);
}
};
...
<Typography
onClick={() => handleClick(songItem.songId)}
sx={styles.songListItemText}
>
{songItem.Title}
</Typography>
The searchSong action creator should return a resolved value for consumers to await for.
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch(startRequest());
try {
const results = await SearchApi.searchSongAsync(obj);
if (!results.length) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch(receivedSong({ results }));
return results; // <-- return resolved value here
} catch (e) {
console.error("Error: ", e);
} finally {
dispatch(completeRequest());
}
}
You can create a state such as const [isDataPresent, setIsDataPresent] = useState(false) to keep track of if the data has arrived or not. And as David has mentioned in the comments you cannot call the function inside the useEffect on handleClick. Instead what you can do is create that function outside the useEffect hook and inside the same function you fetch the data and check if the data is at all present, if present then you can set the above boolean state to true and then redirect from that function itself.
Since you are already fetching the data from the same API and different endpoint, what you can do is -
Create a new component.
Since you are mapping over the data send the data to this component by rendering it inside the map function. It'd allow the data to be passed and components to be rendered one by one.
Create a state in the new component.
Use useEffect hook to fetch the data for a single song since when you are passing the data from the previous component to this one you would also get access to the ID and store it inside the state. This would be occurring inside the newly created component.
I am sending a bulk data request to Shopify graphql to get all the products in the store, which may take a while depending on how many products the store has, so instead of using useQuery, Shopify recommends to send a bulk request using useMutation more on that here , anyway here is my code.
const BULK_INIT_MUTATION = gql`
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
images{
edges{
node{
id
originalSrc
altText
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
`;
const Index = () => {
const [createBulkRequest, { data }] = useMutation(BULK_INIT_MUTATION);
createBulkRequest();
return <Page>Hi Index</Page>;
};
Above should initiate a bulk request to shopify and as per Shopify docs it will take them anything between couple seconds to couples minutes to have the data ready for me to fetch using useQuery with below query
query {
currentBulkOperation {
id
status
errorCode
createdAt
completedAt
objectCount
fileSize
url
partialDataUrl
}
}
When everything is all good above query will return something like this
{
"data": {
"currentBulkOperation": {
"id": "gid:\/\/shopify\/BulkOperation\/720918",
"status": "COMPLETED",
"errorCode": null,
"createdAt": "2019-08-29T17:16:35Z",
"completedAt": "2019-08-29T17:23:25Z",
"objectCount": "57",
"fileSize": "358",
"url": "https:\/\/storage.googleapis.com\/shopify\/dyfkl3g72empyyoenvmtidlm9o4g?<params>",
"partialDataUrl": null
}
},
...
}
now all I am interested at is the url value, which should link to an address where I can get the actual data.
Now the issue is unless the Shopify Server had all the time it needed to process my request, it will always return the url value as undefined, what I need is a way to maybe constantly request the data after x time until it finally returns the url hence the operation status's completed and then re-render.
I am not sure what's the best way to approach this is, I am thinking something like setInterval, but I'm really not sure. so any suggestion would be quite helpful as I've been stuck with this for couple days now?
I have already tried useEffect like below
const Index = () => {
const [response, setData] = useState({});
const [createBulkRequest, { data }] = useMutation(BULK_INIT_MUTATION);
createBulkRequest();
useEffect(() => {
const timer = setInterval(() => {
const { loading, error, data } = useQuery(BULK_STATUS_QUERY);
console.log('returned data', data);
setData(data);
}, 5000);
// clearing interval
return () => clearInterval(timer);
});
return <Page>Hi Index</Page>;
};
However doing this throws this error.
Unhandled Runtime Error Error: Invalid hook call. Hooks can only be
called inside of the body of a function component. This could happen
for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug
and fix this problem.
If you need to schedule some task after x amount of time in order to retrieve data and update some state if defined, then you can use the setInterval() method calls a function in a useEffect hook (make sure to clear your interval in the return of that useEffect)
after running mutation using the graphql, if I quickly goback to Previous page,
occur error : Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and
asynchronous tasks in %s.%s, a useEffect cleanup function,
I think it's because I quickly go to another page during the mutation.
If this is not the case, there is no error.
(Even if an error occurs, update succeeds. but I'm worried about errors)
Even if move to another page during mutating, I want to proceed with the update as it is
How can I proceed with the update?
if If there is no way, is there method that How to create a delay during mutating
im so sorry. my english is not good.
const CalendarTodo = ({
month,
day,
data,`enter code here`
isImportWhether,
setIsImportWhether
}) => {
const [value, setValue] = useState("");
const monthDay = `${month + 1}월 ${day}일`;
const [createToDoMutation] = useMutation(CREATE_TODO, {
variables: {
toDoId:
data &&
data.toDos &&
data.toDos.filter(object => object.monthDay === monthDay)[0] &&
data.toDos.filter(object => object.monthDay === monthDay)[0].id,
monthDay: monthDay,
dayToDo: value,
importEvent: isImportWhether
},
update: (proxy, { data: { createToDo } }) => {
const data = proxy.readQuery({ query: SEE_TODO_OF_ME });
data &&
data.toDos &&
data.toDos.filter(object => object.monthDay === monthDay)[0] &&
data.toDos
.filter(object => object.monthDay === monthDay)[0]
.dayToDo.push(createToDo);
proxy.writeQuery({ query: SEE_TODO_OF_ME, data });
},
optimisticResponse: {
createToDo: {
__typename: "DayToDo",
id: Math.random().toString(),
toDoList: value,
importEvent: isImportWhether
}
}
});
return (
<>
);
};
export default CalendarTodo;
As you already guessed the reason is the asynchronous request that keeps on running even after un-mounting the component due to navigating away from it.
There are many ways to solve this. One is to add a check whether or not the component you are calling the async request from is still mounted and only update its state if so, e.g.:
useEffect(() => {
let isMounted = true;
apollo.mutate({query, variables, update: {
if(isMounted) {
// update state or something
}
})
return () => {
isMounted = false;
};
}, []);
This way however the data might be lost. If you want to make sure that you receive and store the return value you should add the request to a higher level component or context hat will not be unmounted on navigation. This way you can trigger the async call but dont have to worry about navigating away.
I am trying to update the redux store but when I try to access both points and sessionId, they come back undefined. I am sure there is a problem with my reducer, but I can't figure it out. Any help would be much appreciated.
Here's my reducer:
import { UPDATE_POINTS, SET_SESSION } from '../path'
const initialState = {
sessionId: null,
points: []
}
export default (state = initialState, action) => {
switch (action.type) {
case UPDATE_POINTS:
return {
points: action.points
}
case SET_SESSION:
return {
sessionId: action.session
}
default:
return state;
}
}
Edit:
Action Creators
export function updatePoints(points){
return {
type: UPDATE_POINTS,
points
}
}
export function setSession(session){
return {
type: SET_SESSION,
session
}
}
Within React Component (for simplicity I took most everything else out of this function)
handleSelect(e) {
this.props.setSession(e);
console.log(this.props.sessionId);
}
This function is used when a menu item is chosen from a drop down menu. On the first selection, the console shows whatever is in the initial state for sessionId. Any further drop down selections result in undefined in the console.
You're super close. A reducer in redux needs to return the a new copy of the entire state. Your reducer is returning only the key it's concerned with, which is going to drop the other key. You need to return a new copy of the state with your key updated. For example:
const initialState = {
sessionId: null,
points: []
}
export default (state = initialState, action = null) => {
// Exit early if you don't have an action (returning old state)
if (!action) return state;
// This function will assign your patch onto the old state, and then
// assign all of that onto a NEW object. For redux to do it's job,
// you can't modulate the old object, you have to return a new one.
const update = patch => Object.assign({}, state, patch);
switch (action.type) {
case UPDATE_POINTS:
return update({
points: action.points
});
case SET_SESSION:
return update({
sessionId: action.session
});
default:
return state;
}
}
And for the record, instead of putting your data payload under a unique key each time in your action creators, if you put the payload under a data key then your action will follow the standard flux action format.
export const updatePoints = (points) => ({
type: UPDATE_POINTS,
data: points
});
export const setSession = (session) => ({
type: SET_SESSION,
data: session
});
There you go. Good luck, and if you get stuck, refer back to the Redux docs (they're really good). Link to Redux Docs
I recently started migrating things from jQ to a more structured framework being VueJS, and I love it!
Conceptually, Vuex has been a bit of a paradigm shift for me, but I'm confident I know what its all about now, and totally get it! But there exist a few little grey areas, mostly from an implementation standpoint.
This one I feel is good by design, but don't know if it contradicts the Vuex cycle of uni-directional data flow.
Basically, is it considered good practice to return a promise(-like) object from an action? I treat these as async wrappers, with states of failure and the like, so seems like a good fit to return a promise. Contrarily mutators just change things, and are the pure structures within a store/module.
actions in Vuex are asynchronous. The only way to let the calling function (initiator of action) to know that an action is complete - is by returning a Promise and resolving it later.
Here is an example: myAction returns a Promise, makes a http call and resolves or rejects the Promise later - all asynchronously
actions: {
myAction(context, data) {
return new Promise((resolve, reject) => {
// Do something here... lets say, a http call using vue-resource
this.$http("/api/something").then(response => {
// http success, call the mutator and change something in state
resolve(response); // Let the calling function know that http is done. You may send some data back
}, error => {
// http failed, let the calling function know that action did not work out
reject(error);
})
})
}
}
Now, when your Vue component initiates myAction, it will get this Promise object and can know whether it succeeded or not. Here is some sample code for the Vue component:
export default {
mounted: function() {
// This component just got created. Lets fetch some data here using an action
this.$store.dispatch("myAction").then(response => {
console.log("Got some data, now lets show something in this component")
}, error => {
console.error("Got nothing from server. Prompt user to check internet connection and try again")
})
}
}
As you can see above, it is highly beneficial for actions to return a Promise. Otherwise there is no way for the action initiator to know what is happening and when things are stable enough to show something on the user interface.
And a last note regarding mutators - as you rightly pointed out, they are synchronous. They change stuff in the state, and are usually called from actions. There is no need to mix Promises with mutators, as the actions handle that part.
Edit: My views on the Vuex cycle of uni-directional data flow:
If you access data like this.$store.state["your data key"] in your components, then the data flow is uni-directional.
The promise from action is only to let the component know that action is complete.
The component may either take data from promise resolve function in the above example (not uni-directional, therefore not recommended), or directly from $store.state["your data key"] which is unidirectional and follows the vuex data lifecycle.
The above paragraph assumes your mutator uses Vue.set(state, "your data key", http_data), once the http call is completed in your action.
Just for an information on a closed topic:
you don’t have to create a promise, axios returns one itself:
Ref: https://forum.vuejs.org/t/how-to-resolve-a-promise-object-in-a-vuex-action-and-redirect-to-another-route/18254/4
Example:
export const loginForm = ({ commit }, data) => {
return axios
.post('http://localhost:8000/api/login', data)
.then((response) => {
commit('logUserIn', response.data);
})
.catch((error) => {
commit('unAuthorisedUser', { error:error.response.data });
})
}
Another example:
addEmployee({ commit, state }) {
return insertEmployee(state.employee)
.then(result => {
commit('setEmployee', result.data);
return result.data; // resolve
})
.catch(err => {
throw err.response.data; // reject
})
}
Another example with async-await
async getUser({ commit }) {
try {
const currentUser = await axios.get('/user/current')
commit('setUser', currentUser)
return currentUser
} catch (err) {
commit('setUser', null)
throw 'Unable to fetch current user'
}
},
Actions
ADD_PRODUCT : (context,product) => {
return Axios.post(uri, product).then((response) => {
if (response.status === 'success') {
context.commit('SET_PRODUCT',response.data.data)
}
return response.data
});
});
Component
this.$store.dispatch('ADD_PRODUCT',data).then((res) => {
if (res.status === 'success') {
// write your success actions here....
} else {
// write your error actions here...
}
})
TL:DR; return promises from you actions only when necessary, but DRY chaining the same actions.
For a long time I also though that returning actions contradicts the Vuex cycle of uni-directional data flow.
But, there are EDGE CASES where returning a promise from your actions might be "necessary".
Imagine a situation where an action can be triggered from 2 different components, and each handles the failure case differently.
In that case, one would need to pass the caller component as a parameter to set different flags in the store.
Dumb example
Page where the user can edit the username in navbar and in /profile page (which contains the navbar). Both trigger an action "change username", which is asynchronous.
If the promise fails, the page should only display an error in the component the user was trying to change the username from.
Of course it is a dumb example, but I don't see a way to solve this issue without duplicating code and making the same call in 2 different actions.
actions.js
const axios = require('axios');
const types = require('./types');
export const actions = {
GET_CONTENT({commit}){
axios.get(`${URL}`)
.then(doc =>{
const content = doc.data;
commit(types.SET_CONTENT , content);
setTimeout(() =>{
commit(types.IS_LOADING , false);
} , 1000);
}).catch(err =>{
console.log(err);
});
},
}
home.vue
<script>
import {value , onCreated} from "vue-function-api";
import {useState, useStore} from "#u3u/vue-hooks";
export default {
name: 'home',
setup(){
const store = useStore();
const state = {
...useState(["content" , "isLoading"])
};
onCreated(() =>{
store.value.dispatch("GET_CONTENT" );
});
return{
...state,
}
}
};
</script>