return promise for value from hook - javascript

so I'm trying to be a clever-a$$ and return a promise from a hook (so I can await the value instead of waiting for the hook to give me the value after its resolved and the hook reruns). I'm attempting something like this, and everything is working until the resolve part. The .then doesnt ever seem to run, which tells me that the resolve I set isn't firing correctly. Here's the code:
function App() {
const { valPromise } = useSomeHook();
const [state, setState] = React.useState();
React.useEffect(() => {
valPromise.then(r => {
setState(r);
});
}, []);
if (!state) return 'not resolved yet';
return 'resolved: ' + state;
}
function useSomeHook() {
const [state, setState] = React.useState();
const resolve = React.useRef();
const valPromise = React.useRef(new Promise((res) => {
resolve.current = res;
}));
React.useEffect(() => {
getValTimeout({ setState });
}, []);
React.useEffect(() => {
if (!state) return;
resolve.current(state);
}, [state]);
return { valPromise: valPromise.current, state };
}
function getValTimeout({ setState }) {
setTimeout(() => {
setState('the val');
}, 1000);
}
and a working jsfiddle: https://jsfiddle.net/8a4oxse5/
I tried something similar (re-assigning the 'resolve' part of the promise constructor) with plain functions, and it seems to work:
let resolve;
function initPromise() {
return new Promise((res) => {
resolve = res;
});
}
function actionWithTimeout() {
setTimeout(() => {
resolve('the val');
}, 2000);
}
const promise = initPromise();
actionWithTimeout();
promise.then(console.log);
jsfiddle: https://jsfiddle.net/pa1xL025/
which makes me think something is happening with the useRef or with rendering.
** update **
so it looks like the useRefs are working fine. its the final call to 'res' (or resolve) that doesn't seem to fulfill the promise (promise stays pending). not sure if a reference (the one being returned from the hook) is breaking between renders or something

If you use this code the problem is gone:
const valPromise = React.useRef();
if (!valPromise.current) {
valPromise.current = new Promise((res) => {
resolve.current = res;
})
}
Normally you shouldn't write to ref during render but this case is ok.
Explanation
When you had this initially:
const valPromise = React.useRef(new Promise((res) => {
resolve.current = res;
}));
the promise here is actually recreated on each render and only the result from first render is used.
From the docs:
const playerRef = useRef(new VideoPlayer());
Although the result of new VideoPlayer() is only used for the initial
render, you’re still calling this function on every render. This can
be wasteful if it’s creating expensive objects.
So in your case that meant the resolve.current would be updated on each render.
But the valPromise remains the initial one.
Also since the expression passed to useRef runs during rendering one shouldn't do there anything that you would not do during rendering, including side effects - which writing to resolve.current was.

As Giorgi said, the useRef runs every render, but all results after the first run are discarded, which can cause the issue I was having above. So for those interested, I made the promise implementation into a standalone hook to abstract away the complexity:
this hook has been published to NPM if interested: https://www.npmjs.com/package/usepromisevalue
export function usePromiseValue() {
const resolve = React.useRef();
const promise = React.useRef(new Promise(_resolve => {
// - useRef is actually called on every render, but the
// subsequent result is discarded
// - however, this can cause the `resolve.current` to be overwritten
// which will make the initial promise unresolvable
// - this condition takes care of ensuring we always resolve the
// first promise
if (!resolve.current) resolve.current = _resolve;
}));
return {
promise: promise.current,
resolve: resolve.current,
};
}
(*** NOTE: see the bottom of this answer for the same hook but with the ability to update the promise/resolve combo so you can resolve multiple promises instead of just 1)
keep in mind, this will only resolve the promise 1 time. if you want the promise to react to state changes and be able to fire off another async operation and resolve a new promise, you'll need a more involved implementation that will return a new promise + resolve combo.
Usage; taking the code from my question above, but tweaking:
function useSomeHook() {
const [state, setState] = React.useState();
const { promise, resolve } = usePromiseValue();
React.useEffect(() => {
getValTimeout({ setState });
}, []);
React.useEffect(() => {
if (!state) return;
resolve(state);
}, [state]);
return {
valPromise: promise,
state,
};
}
*** UPDATE ***
here's the updated promise hook that will handle giving you a new promise/resolve combo when dependecies change:
function usePromiseValue({
deps = [],
promiseUpdateTimeout = 200,
} = {}) {
const [count, setCount] = React.useState(0);
const [mountRender, setMountRender] = React.useState(true);
const resolve = React.useRef();
const promise = React.useRef(new Promise(_resolve => {
// - useRef is actually called on every render, but the
// subsequent result is discarded
// - however, this can cause the `resolve.current` to be overwritten
// which will make the initial promise unresolvable
// - this condition takes care of ensuring we always resolve the
// first promise
if (!resolve.current) resolve.current = _resolve;
}));
React.useEffect(() => {
setMountRender(false);
}, []);
React.useEffect(() => {
// - dont run this hook on mount, otherwise
// the promise will update and not be resolveable
if (mountRender) return;
setTimeout(() => {
setCount(count + 1);
}, promiseUpdateTimeout);
// - dont update the promise/resolve combo right away,
// otherwise the current promise will not resolve
// - instead, wait a short period (100-300 ms after state updates)
// to give the current promise time to resolve before updating
}, [...deps]);
React.useEffect(() => {
// - dont run this hook on mount, otherwise
// the promise will update and not be resolveable
if (mountRender) return;
promise.current = new Promise(r => resolve.current = r);
}, [count]);
return {
promise: promise.current,
resolve: resolve.current,
};
}
https://jsfiddle.net/ahmadabdul3/atgcxhod/5/

Related

Redux: Dispatch actions in sequence

I'm creating a Reddit client on Redux and I have 2 store dispatches firing in the app:
// App()
const dispatch = useDispatch();
useEffect(() => {
const stateMatch = window.location.href.match(/state=([^&]*)/);
const codeMatch = window.location.href.match(/code=([^&]*)/);
if ((stateMatch && codeMatch) || localStorage.getItem("access_token")) {
dispatch(fetchUser());
dispatch(fetchSubs());
}
});
...
However, I want fetchUser() to run and finish before fetchSubs() can begin, as the former currently seems to ruin API calls for the latter while it's running. How can I solve this?
Since you are using createAsyncThunk you can do something like this:
dispatch(fetchUser())
.unwrap()
.then((user) => {
// do anything you want with user, or don't, also dispatch actions
dispatch(fetchSubs());
})
.catch((e) => {
// error in case of rejection inside createAsyncThunk second argument
console.log(e);
});
Explanation
Let's say const thunk = fetchUser()
so basically dispatch(fetchUser()) is the same as dispatch(thunk).
Redux's dispatch function returns whatever its argument (the action) returns.
So in this case, dispatch(thunk) returns whatever thunk returns.
thunk, based on how createAsyncThunk works, returns a promise that either resolves to fulfilled action, or the rejected action. (those actions that you receive in extra reducers).
This is how you can access those actions:
dispatch(thunk).then(fullfilledAction=>...).catch(rejectedAction=>...`
RTK library also provides a method called unwrap. Instead of those action objects I explained above, it lets you use the returned value from the 2nd argument of createAsyncThunk.
export const fetchUser = createAsyncThunk("user/fetchUser", async () => {
const user = await Reddit.getUser().then(val => {
return val;
});
return user; // unwrap lets you use this instead of action objects.
})
Try this with pure react and redux hooks
...
const state = useStore(yourStore) //use your method to read state
const dispatch = useDispatch();
const checkValue = () => {
const stateMatch = window.location.href.match(/state=([^&]*)/);
const codeMatch = window.location.href.match(/code=([^&]*)/);
if ((stateMatch && codeMatch) || localStorage.getItem("access_token")) {
return true;
}
return false;
}
useEffect(() => {
if(checkValue())
dispatch(fetchUser());
}
}, []);
useEffect(() => {
if(checkValue() && state.authState)
dispatch(fetchSubs());
}
}, [state.authState]);
...

React.js useEffect with nested async functions

I have the common warning displaying upon loading of my web app but never again...
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 a useEffect
cleanup function.
EDIT****
It is caused by this chunk of code. I have narrowed it down to one function. It blows up when I try to setMoisture state. I am not sure why.
function getData (){
Axios.get("http://localhost:3001/api/get-value").then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if (loading === true){
setLoading(false);
}
return parseInt(dataValue);
}).then((resp)=>setMoisture(resp))
}
React.useEffect(() => {
if (moisture === "initialState"){
getData();
}
}, []);
Posting the answer here (based from the comments) for completeness.
Basically, use local variables and cleanup function towards the end of useEffect(). Using this as reference:
Similar situation here
You should declare the function inside the useEffect or add it as a dependency. one way to do it's just moving your function inside the hook.
// I assumed that your useState hooks looks something similar.
const [moisture, setMoisture] = React.useState('initialState')
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
function getData() {
Axios.get("http://localhost:3001/api/get-value").then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if(loading === true) {
setLoading(false);
}
return parseInt(dataValue);
}).then((resp => setMoisture(resp)))
}
if (moisture === "initialState"){
getData();
}
}, [])
You also probably want to first set your data to the state and then change your loading state to false, this is gonna prevent some bugs. This is another way to do it and manage the loading state and the promises
React.useEffect(() => {
function getData() {
setLoading(true)
Axios.get("http://localhost:3001/api/get-value")
.then((response) => {
const dataValue = response.data.map((val) => {
return [val.value]
})
// This is going to pass 0 when can't parse the data
setMoisture(parseInt(dataValue) || 0)
setLoading(false)
})
}
getData()
}, [])

Recursive function in Redux

Trying to pass a reference to the recursive function to check if Redux action data fetch is complete, but getting function reference errors
const fetchAccountComplete = (state, accountNo) => { //state here is function reference
return new Promise(resolve => {
(function waitForFetchComplete(state, accountNo) {
const {isFetching, receivedAt} = state().account[accountNo] // getting state not a function here
if (!isFetching) return resolve()
setTimeout(waitForFetchComplete, 100)
})()
})
}
Is there a better way to return a promise to the caller function in Redux dispatch actions so that once the data is fetched, i need to do some other logic in other action.
Update 1:
should have been more clearer. There are two callers of this Request, Recieve actions on say Account data. First caller is directed similar to the above comment by you so waits until complete, second caller would not be doing the async call again and need to check if data fetch is complete so trying to see if recursive function with check on state so that promise can be resolved is being done
You could take advantage of promising chaining.
Example:
Have three actions like: IS_FETCHING, FETCH_SUCCESS, FETCH_ERROR.
IS_FETCHING:
Will simply set your state as pending (may be useful for showing a loading animation, for example).
FETCH_SUCCESS:
Will contain the result of the fetch to update the state. Will also clear the isUpdating flag
FETCH_ERROR:
Will contain any possible error due to the fetch (application or network error). Will also clear the isUpdating flag
Then, what you could do at application level is:
dispatch({type: IS_FETCHING, payload: data});
fetch(`https://MY-SERVER.com/?data=${data}`)
.then(response => response.json())
.then(json =>
dispatch({
type: isError(json) ? FETCH_RESULT : FETCH_ERROR,
payload: json
})
);
You could even benefit of action creators for the job.
Here is a good guide for that: https://redux.js.org/advanced/async-actions
If you have a function that returns a promise that is called multiple times with the same arguments then you can group that in a way so that the function is not called when it still has an unresolved promise and something tries to call it again with the same arguments.
Here is an example:
//group promise returning function
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.done(key); //tell cache promise is done
return r;
},
(e) => {
cache.done(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//creates a cache that will remove cached value when
// Promise is done (resolved or rejected)
const createCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
done: (key) => cache.delete(key),
};
};
//function that retuns a promise
const later = (time, value) => {
console.log('executing later with values', time, value);
return new Promise((r) =>
setTimeout(() => r(value), time)
);
};
//create group function with a cache that will remove
// cache key when promise is resolved or rejected
const groupAndRemoveCacheOnDone = createGroup(
createCache()
);
//grouped version of the later function
const groupedLater = groupAndRemoveCacheOnDone(later);
//testing the groped later
groupedLater(100, 8); //first call causes console.log
groupedLater(100, 8); //same arguments will not call later
groupedLater(100, 8); //will not call later
//will call later because arguments are not the same
// as the other calls
groupedLater(100, 'XX');
groupedLater(100, 8) //will not call later
.then((value) => {
console.log('resolved with:', value);
//this will call later because cache value is removed
// after promise is resolved
return groupedLater(100, 8);
})
.then(() => {
//testing with fetchAccountComplete
console.log(
'***** how many times is fetchAccountComplete called *****'
);
const fetchAccountComplete = (state, accountNo) => {
console.log(
'fetchAccountComplete called with',
accountNo
);
return new Promise((resolve) => {
(function waitForFetchComplete(state, accountNo) {
const {
isFetching,
receivedAt,
} = state().account[accountNo]; // getting state not a function here
if (!isFetching) return resolve();
setTimeout(
() => waitForFetchComplete(state, accountNo),
100
);
})(state, accountNo);
});
};
const data = {
account: [{ isFetching: true }],
};
const state = () => data;
const groupedFetchAccountComplete = groupAndRemoveCacheOnDone(
fetchAccountComplete
);
groupedFetchAccountComplete(state, 0);
groupedFetchAccountComplete(state, 0);
groupedFetchAccountComplete(state, 0);
groupedFetchAccountComplete(state, 0).then((resolve) =>
console.log('resolved')
);
data.account[0].isFetching = false;
});

Async/Await inside the Observable

How can I use async/await inside the Observable??
With this code I'm unable to trigger the unsubscribe function within observable thus interval is not cleared.
const { Observable } = require("rxjs");
const test = () => new Observable(async (subscriber) => {
await Promise.resolve();
const a = setInterval(() => {
subscriber.next(Math.random());
console.log("zz");
}, 500);
return () => {
console.log("asdsad");
clearInterval(a);
};
});
const xyz = test().subscribe(console.log);
setTimeout(() => {
xyz.unsubscribe();
}, 3000);
Async/Await inside an observable is not supported. However, it can be done with a behavior subject and an asynchronous nested function.
Create a behavior subject, convert it to an observable (.asObservable()), execute the asynchronous nested function, return the observable. Here's an example.
function getProgress() {
// Change this value with latest details
const value = new BehaviorSubject('10%');
const observable = value.asObservable();
// Create an async function
const observer = async() => {
// Perform all tasks in here
const wait1 = await new Promise(resolve => setTimeout(resolve, 3000));
value.next('66%');
const wait2 = await new Promise(resolve => setTimeout(resolve, 3000));
value.next('100%');
// Complete observable
value.complete();
}
// Call async function & return observable
observer();
return observable;
}
It's very readable and works like a charm.
First of all, subscriber passed to observable contructor cannot be async function. There is no support for that.
If you need to create observable from promise, use from:
import { from } from 'rxjs';
const observable = from(promise);
But considering your scenario.
Because there is no way to cancel native js promise, you cannot realy unsubscribe from such created observable, so:
const obs = from(new Promise(resolve => {
setTimeout(() => {
console.log('gonna resolve');
resolve('foo');
}, 1000);
}));
const sub = obs.subscribe(console.log);
setTimeout(() => sub.unsubscribe(), 500);
will print:
gonna resolve
gonna resolve
gonna resolve
(...)
so yeah: gonna resolve will be printed in the cosole all the time, but nothing more - result passed to resolve will be ignored - just not logged.
From the other hand, if you remove that unsubscribtion (setTimeout(() => sub.unsubscribe(), 500);) this time you will see:
gonna resolve
foo
gonna resolve
gonna resolve
gonna resolve
(...)
There is one way that maybe will help you - defer - but it's not strictly related with your question.
import { defer } from 'rxjs';
defer(async () => {
const a = await Promise.resolve(1);
const b = a + await Promise.resolve(2);
return a + b + await Promise.resolve(3);
}).subscribe(x => console.log(x)) // logs 7

Does this.setState return promise in react

I made my componentWillMount() async. Now I can using await with the setState.
Here is the sample code:
componentWillMount = async() => {
const { fetchRooms } = this.props
await this.setState({ })
fetchRooms()
}
So question here is this.setState returns promise because I can use await with it?
Edit
When I put await then it runs in a sequence 1, 2, 3 And when I remove await then it runs 1, 3, 2??
componentWillMount = async() => {
const { fetchRooms } = this.props
console.log(1)
await this.setState({ } => {
console.log(2)
})
console.log(3)
fetchRooms()
}
You can promisify this.setState so that you can use the React API as a promise. This is how I got it to work:
class LyricsGrid extends Component {
setAsyncState = (newState) =>
new Promise((resolve) => this.setState(newState, resolve));
Later, I call this.setAsyncState using the standard Promise API:
this.setAsyncState({ lyricsCorpus, matrix, count })
.then(foo1)
.then(foo2)
.catch(err => console.error(err))
setState is usually not used with promises because there's rarely such need. If the method that is called after state update (fetchRooms) relies on updated state (roomId), it could access it in another way, e.g. as a parameter.
setState uses callbacks and doesn't return a promise. Since this is rarely needed, creating a promise that is not used would result in overhead.
In order to return a promise, setState can be promisified, as suggested in this answer.
Posted code works with await because it's a hack. await ... is syntactic sugar for Promise.resolve(...).then(...). await produces one-tick delay that allows to evaluate next line after state update was completed, this allows to evaluate the code in intended order. This is same as:
this.setState({ roomId: room && room.roomId ? room.roomId : 0 }, () => {
console.log(2)
})
setTimeout(() => {
console.log(3)
});
There's no guarantee that the order will stay same under different conditions. Also, first setState callback isn't a proper place to check whether a state was updated, this is what second callback is for.
setState does not return a promise.
setState has a callback.
this.setState({
...this.state,
key: value,
}, () => {
//finished
});
It does not return a promise.
You can slap the await keyword in front of any expression. It has no effect if that expression doesn't evaluate to a promise.
setState accepts a callback.
Don't think setState is returning a Promise but you can always do this
await new Promise( ( resolve ) =>
this.setState( {
data:null,
}, resolve )
)
or you can make some utility function like this
const setStateAsync = ( obj, state ) => {
return new Promise( ( resolve ) =>
obj.setState( state , resolve )
)
}
and use it inside a React.Component like this:
await setStateAsync(this,{some:'any'})
You can simple customize a Promise for setState
componentWillMount = async () => {
console.log(1);
await this.setRooms();
console.log(3);
};
setRooms = () => {
const { fetchRooms } = this.props;
return fetchRooms().then(({ room }) => {
this.setState({ roomId: room && room.roomId ? room.roomId : 0 }, _ =>
console.log(2)
);
});
};
Or
setRooms = async () => {
const { fetchRooms } = this.props;
const { room } = await fetchRooms();
return new Promise(resolve => {
this.setState({ roomId: room && room.roomId ? room.roomId : 0 }, _ =>
resolve()
);
});
};
Hope this help =D
Simpler way to answer this question is we can use promisify function present in pre-installed util library of node.js and then use it with the await keyword.
import {promisify} from 'util';
updateState = promisify(this.setState);
await this.updateState({ image: data });

Categories