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.
Related
I have a callback which I need to return true or false to trigger some external action, I need to make an API call inside this callback, so I need to get the state and dispatch some actions inside the callback, I don't know if I can use eventChannel because this callback could not be a generator, only a plain function. I need to do something like this.
zafClient.on('ticket.save', () => {
const state = yield select();
yield put(actionWithApiCall())
// I need to wait for the action to finish and then return something
// based on the response from the API
// I know how to block the saga to wait for the action dispatched
// the problem is that I can't use yield here
return somethingfromthestore;
});
Btw, this is zendesk API.
Your not going to be able to pass a generator function to that API. The work around is to dispatch an action directly to the redux store and then write a saga that listens for that action.
zafClient.on('ticket.save', () => reduxStore.dispatch(actionWithApiCall()))
You will have to make the redux store exportable from where you create it. So that you can directly access it here.
One of the challenges is how to yield in the callback. How about importing dispatch? Isn't dispatch the non-generator version of yield?
Should you be using React, doing so seems to be an option.
In the same saga that you use to specify the callback, you can subsequently listen for the action created in the dispatch/action creator callback. That's done by specifying another watchFor function (in my setup, any number of exported watchFor functions get added to the pool of saga watchers).
The paired saga worker can finalize whatever needs to happen to the data returned from the API call before using your final action creator to "document" the workflow by updating the store.
The other option might be to wrap the api call in an eventChannel (see: https://github.com/redux-saga/redux-saga/issues/1178).
- E
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);
continuation-local-storage seems to be used, also in context of express.
Yet the very basic usage does not work for me, since the context is completely lost!
var createNamespace = require('continuation-local-storage').createNamespace;
var session = createNamespace('my session');
async function doAsync() {
console.log('Before getting into the promise: ' + session.get('test'));
await Promise.resolve();
console.log('*in* the promise: ' + session.get('test'));
}
session.run(() => {
session.set('test', 'peekaboo');
doAsync();
});
results in:
$ node server_test.js
Before getting into the promise: peekaboo
*in* the promise: undefined
Have I done something completely wrong or is the CLS simply broken?
Or is the library broken?
If it's not meant to work with promises, are there other concepts that work as a threadLocal storage to implement multi-tenancy in a proper way?
cls-hooked seems to be working fine, though the library (as the previous one) were last updated two years ago...
If someone has some other more robust way to implement a thread-local state for multi-tenancy please share!
You are trying to maintain a shared state across multiple functions which are then executes asynchronously. That is a very common case in JS, and the language itself provides a very simple, yet powerful mechanism for that: You can access variables from an outer function from an inner function, even if the outer function already finished its execution:
(function closured() {
let shared = { /*...*/ };
function inner() {
console.log( shared );
}
setTimeout(inner);
})();
Now while that works, it doesn't scale that well for larger applications: All functions accessing that state have to be inside of one function, and therefore that file gets really blown up. The libraries you are using are trying to resolve that, they also use closures to maintain the state across asynchronous calls: When you register an async callback the state is stored in a function, then the callback itself gets wrapped into a callback that restores the state:
let state;
function setTimeoutWithState(cb, time) {
const closured = state;
setTimeout(() => {
state = closured;
cb();
}, time);
}
state = 1;
setTimeoutWithState(() => console.log(state), 1000);
state = 2;
setTimeoutWithState(() => console.log(state), 500);
Now the library just has to wrap every async callback that way, and then you can maintain the state easily, isn't that great? Well for sure it is, but adding code to every callback does have it's cost (as JS heavily utilizes callbacks).
I am using mobx state tree and mobx for UI Stuff.
Now when I save something to db, after the request is done I want to update the ui(ie my mobx state).
I need to know when the flow is finished.
myFlow: flow(function* () {
// do stuff here.
}),
now I see that a promise is returned, so I thought of just doing
myFlow.then()
which works but I am wondering if this is the property way or if there is another way to do this(async/await? or some internal thing that flow has?)
a flow returns a promise, so any promise waiting mechanism works: .then, await, or yield inside another flow. If you want to render the state of the flow, take a look at mobxUtils.fromPromise(promise).case(....) of the mobx-utils package
Inside generator you can call some another action at the end.
In example below I call thisIsWhatYouNeed function. This function will be called when generator ends.
myFlow: flow(function* () {
try {
const response = yield fetch('your URL here');
const data = yield response.json()
// here you can call another action
self.thisIsWhatYouNeed(data);
} catch (error) {
console.log('error happens');
}
})
thisIsWhatYouNeed(data) {
// here you have your data and know that flow is finished
console.log('generator already finished');
}
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.