I'm learning redux-saga and I'm trying to integrate it into a project that uses an API that is generated with openapi-generator which produces output like the following:
async loginUser(body: Login): Promise<LoginResponse> {
debugger;
const response = await this.loginUserRaw({ body: body });
return await response.value();
}
And loginUserRaw is a function that performs the actual login. Then, I have the following saga:
function* login(action:Login) {
try{
const response = yield call(User.loginUser, action);
yield result(LOGIN_SUCCESS, response)
}catch(e){
yield result(LOGIN_FAILURE, e)
}
}
When this runs, I get an error in my API method's await this.loginUserRaw({ body: body }); line:
TypeError: Cannot read property 'loginUserRaw' of null
I debug it and see that this is null. When I bind the function explicitly in the saga:
const response = yield call(User.loginUser.bind(User), action);
It works, but I don't wanna bind it every time I call a function. How can I get saga to work without explicitly binding the function? (I can't change the generated code and remove this either)
Context in Javascript is dynamic based on the way you've written your code
const loginUser = User.loginUser
loginUser() // context lost, because there is no longer `User.` in front of it
Same applies when you pass a function as an parameter. This means that you have to provide the call effect context in some way. The bind method is one option, however the effect itself supports multiple ways of providing context. You can find them in the official docs https://redux-saga.js.org/docs/api/#callcontext-fn-args, here is the short version of it:
Possible ways to pass context:
call([context, fn], ...args)
call([context, fnName], ...args)
call({context, fn}, ...args)
So for example you can do the following:
const response = yield call([User, User.loginUser], action);
Related
The bounty expires in 19 hours. Answers to this question are eligible for a +100 reputation bounty.
David Callanan wants to draw more attention to this question.
Synchronous function call context
In JavaScript, it's easy to associate some context with a synchronous function call by using a stack in a global scope.
// Context management
let contextStack = [];
let context;
const withContext = (ctx, func) {
contextStack.push(ctx);
context = ctx;
try {
return func();
} finally {
context = contextStack.pop();
}
};
// Example
const foo = (message) => {
console.log(message);
console.log(context);
};
const bar = () => {
withContext("calling from bar", () => foo("hello"));
};
This allows us to write context-specific code without having to pass around a context object everywhere and have every function we use depend on this context object.
This is possible in JavaScript because of the guarantee of sequential code execution, that is, these synchronous functions are run to completion before any other code can modify the global state.
Generator function call context
We can achieve something similar with generator functions. Generator functions give us an opportunity to take control just before conceptual execution of the generator function resumes. This means that even if execution is suspended for a few seconds (that is, the function is not run to completion before any other code runs), we can still ensure that there is an accurate context attached to its execution.
const iterWithContext = function* (ctx, generator) {
// not a perfect implementation
let iter = generator();
let reply;
while (true) {
const { done, value } = withContext(ctx, () => iter.next(reply));
if (done) {
return;
}
reply = yield value;
}
};
Question: Async function call context?
It would also be very useful to attach some context to the execution of an async function.
const timeout = (ms) => new Promise(res => setTimeout(res, ms));
const foo = async () => {
await timeout(1000);
console.log(context);
};
const bar = async () => {
await asyncWithContext("calling from bar", foo);
};
The problem is, to the best of my knowledge, there is no way of intercepting the moment before an async function resumes execution, or the moment after the async function suspends execution, in order to provide this context.
Is there any way of achieving this?
My best option right now is to not use async functions, but to use generator functions that behave like async functions. But this is not very practical as it requires the entire codebase to be written like this.
Background / Motivation
Using context like this is incredibly valuable because the context is available deep down the call-stack. This is especially useful if a library needs to call an external handler such that if the handler calls back to the library, the library will have the appropriate context. For example, I'd imagine React hooks and Solid.js extensively use context in this way under-the-hood. If not done this way, the programmer would have to pass a context object around everywhere and use it when calling back to the library, which is both messy and error-prone. Context is a way to neatly "curry" or abstract away a context object from function calls, based on where we are in the call stack. Whether it is good practice or not is debatable, but I think we can agree that it's something library authors have chosen to do. I would like to extend the use of context to asynchronous functions, which are supposed to conceptually behave like synchronous functions when it comes to the execution flow.
As far as I know ECMA has no specification for "contexts" (regardless if it's a normal or async function).
Therefore the solution you posted for normal functions is already a hack.
As per ECMA standard, there is no JavaScript based API to hook await in order to do a generator like trick. So you have to rely on (environment based) hacks.
These hacks may highly depend on the environment you're using.
JavaScript Only (requires async stack traces)
A solution which is purly based async stack traces is the following one.
Since nearly every JavaScript interpreter is based on V8 this works on nearly every use case.
const kContextIdFunctionPrefix = "__context_id__";
const kContextIdRegex = new RegExp(`${kContextIdFunctionPrefix}([0-9]+)`);
let contextIdOffset = 0;
function runWithContextId(target, ...args) {
const contextId = ++contextIdOffset;
let proxy;
eval(`proxy = async function ${kContextIdFunctionPrefix}${contextId}(target, ...args){ return await target.call(this, ...args); }`);
return proxy.call(this, target, ...args);
}
function getContextId() {
const stack = new Error().stack.split("\n");
for(const frame of stack) {
const match = frame.match(kContextIdRegex);
if(!match) {
continue;
}
const id = parseInt(match[1]);
if(isNaN(id)) {
console.warn(`Context id regex matched, but failed to parse context id from ${match[1]}`);
continue;
}
return id;
}
console.log(new Error().stack)
throw new Error("getContextId() called without providing a context (runWithContextId(...))");
}
A simple demo:
async function main() {
const target = async () => {
const contextId = getContextId();
console.log(`Context Id: ${contextId}`);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Context Id (After await): ${getContextId()} (before: ${contextId})`);
return contextId;
};
const contextIdA = runWithContextId(target);
const contextIdB = runWithContextId(target);
// Note: We're first awaiting the second call!
console.log(`Invoke #2 context id: ${await contextIdB}`);
console.log(`Invoke #1 context id: ${await contextIdA}`);
}
main();
This solution leverages stack traces in order to identify a context id. Traversing the (sync and async) stack trace and using dynamically generated functions with special names allows to pass a special value (a number in this instance).
NodeJS (AsyncLocalStorage)
NodeJS offers a way for Asynchronous context tracking:
https://nodejs.org/api/async_context.html#class-asynclocalstorage
It should be possible to build an async context by using AsyncLocalStorage.
Using a transpiler
You might want to use a transpiler (like babel or typescript) which convert async functions to generator functions on the fly.
Using a transpiler allows you to even write a plugin for implementing async contexts based on generator functions.
I am trying to clear all items stored in localStorage in Redux Saga method. But it's not working as expected.
In theory, If I want to call a function in Saga, we need to write it without brackets with call keyword.
So, I tried to write it with yield call(localStorage.clear); but it's not clearing items from the LocalStorage. If I added brackets () or without yeild & call, it's working and clearing items in LocalStorage as expected.
export function* logoutUserSaga() {
try {
const accessToken = yield call(AuthService.getAccessToken);
yield call(AuthService.logoutUser, accessToken);
yield put(logoutUser.success());
yield call(localStorage.clear); // not working
//yield call(localStorage.clear()); // working
//localStorage.clear(); // working
yield put({ type: RESET_ALL_STATE });
}
catch (error) {
yield put(logoutUser.failure({ errorMessage: error.statusText }));
}
}
export default function* watcherSaga() {
yield takeLatest(authenticateUser.TRIGGER, authenticateUserSaga);
yield takeLatest(logoutUser.TRIGGER, logoutUserSaga);
yield takeLatest(getAccessToken.TRIGGER, getAccessTokenSaga);
}
I would like to know why the call function without brackets () is not working.
Is it because the function being called is void and not returning any value?
Do we always have to add brackets if we want to call void methods?
The reason it's not working is that the localStorage.clear function expects this to be equal to localStorage. That happens automatically when using the notation localStorage.clear, but if you just have a reference to the function and call it without context, you get an Illegal invocation error. This is not directly related to sagas, and can be reproduced like this:
const clear = localStorage.clear;
clear(); // throws an exception
This does have an indirect relationship to sagas though, which is the way call works. If you don't tell call what context it should use when calling the function, then it has no choice but to call it in a global context, causing this exception. call does have a few variations on its parameters that let you specify what this should equal. For example, you could do this:
yield call([localStorage, localStorage.clear]);
You can see other variations of the parameters that call accepts here: https://redux-saga.js.org/docs/api/
Another option is to just not use call. Using call has benefits when trying to test a saga, and has the benefit that it works with both sagas and normal functions, but you can still call normal functions yourself if you want.
When introducing the tf.data.Dataset API, the Deep Learning with JavaScript book says:
Large applications require technology for accessing data from a remote source, piece by piece, on demand.
But the documentation I've read about generators says a generator can't produce values via callbacks. But how else can one access remote sources? I don't see how one can use tf.data.generator in such cases. MDN documentation on yield states:
yield can only be called directly from the generator function that contains it. It can't be called from nested functions or from callbacks.
You can pass an async function (or a function returning a Promise) to the generator. It is then okay to use await inside the function (even inside a loop) to handle any asynchronous tasks.
Code Sample
const dataset = tf.data.generator(async function* () {
const dataToDownload = await fetch(/* ... */);
while (/* ... */) {
const moreData = await fetch(/* ... */);
yield otherData;
}
});
This example uses node-fetch, of course any other method of downloading data also works fine.
Async Generators
Regarding the MDN documentation, generators can be defined as async, but this changes the way they work. Instead of returning the value right away, they will return a Promise that you have to await for. So, instead of calling iterator.next(), you have to call await iterator.next() to read the value.
Code Sample
async function* foo(index) {
while (true) {
yield index++;
}
}
(async () => {
const iterator = foo(0);
console.log((await iterator.next()).value); // 0
console.log((await iterator.next()).value); // 1
})();
Luckily, Tensorflow.js is able to handle async functions/Promises in generators.
I'm using React Starter Kit to create my first React app, and I'm struggling with Ajax calls. I saw that this Kit embeds a way to perform Ajax calls (which is by the way used internally for app routing):
import fetch from '../../core/fetch';
I've added this in my component, and then try to perform an Ajax call when the component loads. Here is my code:
componentDidMount() {
var moduleManager = 'https://my_domain.com/my_endpoint';
async function getModules (url) {
const response = await fetch(url);
const content = await response.json();
return content;
};
this.state.modulesList = getModules(moduleManager);
console.log(this.state.modulesList);
}
I'm also using the state value in my render function:
render() {
var rows = [];
for (var i = 0; i < this.state.modulesList.length; i++) {
rows.push(
<li>{this.state.modulesList[i]}<li/>
);
}
This code put together logs this in my console:
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Then the Ajax call is performed (successfully) and my console is now showing this:
Promise
__proto__:Promise
[[PromiseStatus]]:"resolved"
[[PromiseValue]]:Array[16]
The desired behaviour is of course to update my view: when the ajax calls is performed, display one line per array member.
What am I missing?
Thanks
What I suggest doing:
constructor() {
...
// Bind your async function to your stateful component's class
this.getModules = this.getModules.bind(this);
}
async getModules(url) {
try {
// Perform the ajax call
const response = await fetch(url);
// Convert respone to json
const content = await response.json();
// Process your content (if needed)
...
// Call setState() here
this.setState({someContent: content});
} catch(e) {
console.error(e);
}
}
componentDidMount() {
this.getModules(`${URL}`);
}
You can't actually return the fetched/parsed data from an async function. According to this MDN link, async function returns a promise, and not the actual data you'd expect to get after parsing it.
What happened in your case, was that you were actually trying to receive a returned value from an async function, inside a regular(sync) function (componentDidMount). You can either do what I suggested, or use .then() to use setState after resolving and parsing the promise, in the actual componentDidMount function.
I suggest reading about async functions and Promise before continuing.
Best of luck!
Without testing your code, one problem is that you're incorrectly modifying state directly. That doesn't trigger a render and therefore your view is not updated. Try setState() instead, like so:
<li>{this.setState({modulesList[i]})}<li/>
I am following redux-saga documentation on helpers, and so far it seems pretty straight forward, however I stumbled upon an issue when it comes to performing an api call (as you will see link to the docs points to such example)
There is a part Api.fetchUser that is not explained, thus I don't quiet understand if that is something we need to handle with libraries like axios or superagent? or is that something else. And are saga effects like call, put etc.. equivalents of get, post? if so, why are they named that way? Essentially I am trying to figure out a correct way to perform a simple post call to my api at url example.com/sessions and pass it data like { email: 'email', password: 'password' }
Api.fetchUser is a function, where should be performed api ajax call and it should return promise.
In your case, this promise should resolve user data variable.
For example:
// services/api.js
export function fetchUser(userId) {
// `axios` function returns promise, you can use any ajax lib, which can
// return promise, or wrap in promise ajax call
return axios.get('/api/user/' + userId);
};
Then is sagas:
function* fetchUserSaga(action) {
// `call` function accepts rest arguments, which will be passed to `api.fetchUser` function.
// Instructing middleware to call promise, it resolved value will be assigned to `userData` variable
const userData = yield call(api.fetchUser, action.userId);
// Instructing middleware to dispatch corresponding action.
yield put({
type: 'FETCH_USER_SUCCESS',
userData
});
}
call, put are effects creators functions. They not have something familiar with GET or POST requests.
call function is used to create effect description, which instructs middleware to call the promise.
put function creates effect, in which instructs middleware to dispatch an action to the store.
Things like call, put, take, race are effects creator functions. The Api.fetchUser is a placeholder for your own function that handles API requests.
Here’s a full example of a loginSaga:
export function* loginUserSaga() {
while (true) {
const watcher = yield race({
loginUser: take(USER_LOGIN),
stop: take(LOCATION_CHANGE),
});
if (watcher.stop) break;
const {loginUser} = watcher || {};
const {username, password} = loginUser || {};
const data = {username, password};
const login = yield call(SessionService.login, data);
if (login.err === undefined || login.err === null && login.response) {
yield put(loginSuccess(login.response));
} else {
yield put(loginError({message: 'Invalid credentials. Please try again.'}));
}
}
}
In this snippet, the SessionService is a class that implements a login method which handles the HTTP request to the API. The redux-saga call will call this method and apply the data parameter to it. In the snippet above, we can then evaluate the result of the call and dispatch loginSuccess or loginError actions accordingly using put.
A side note: The snippet above is a loginSaga that continuously listens for the USER_LOGIN event, but breaks when a LOCATION_CHANGE happens. This is thanks to the race effect creator.