Use an Saga action as condition for an if statement - reactjs

I'm actually trying to implement redux-saga into my app. To keep it easy in the beginning, I tried to add saga into my login component first.
But I'm struggling rn by adding an if statement which redirects the UI to an page if my login action loginSuccess() is dispatched. I would like to implement smth like the commented statement below:
const onSubmitForm = (e?: React.FormEvent<HTMLFormElement>) => {
if (e) {
e.preventDefault();
}
dispatch(actions.submitLogin());
// if (actions.loginSuccess() && !actions.loginError) {
// history.push('/');
// }
};
How the actions are passed:
// After the API was fetched:
if (response.message === 'login') {
yield put(actions.loginSuccess());
} else {
yield put(actions.loginError('Username or password is incorrect'));
}

You would handle the "loginSuccess" action in your reducer to change your state, or possibly by another saga listening for the loginSuccess action to perform various other actions when the user logs in. e.g. fetching user data, redirecting the location, etc.

Related

How to access redux state after dispatch is finished fully?

I am making a Create Operation. When the user clicks on onSubmit I am dispatching an action that creates a resource in the database and updates the store with the information that is returned from API call which contains information like status(indicating the status of operation) and error or message.
const initialState = {
loading: true,
status: false,
error: false
}
Above is my initial state of redux store
const {loading,status} = useSelector(state=>state.newUser)
I am consuming the state using useSelector
const addUser = (values) => async (dispatch) => {
const response = // Makes an API call return information
dispatch({type:"ADD_USER",payload:response.data})
}
onSubmit = (values)=>{
dispatch(addUser(values))
.then(data=>{
console.log(data)
})
if(!status){
}
}
In the above onSubmit function when the user submits the form it dispatches an action and updates the store. Now based on the new state I want to update the UI. After dispatching I am checking whether the status of the operation is true or false. If false I want to display the error. The problem is after dispatching If I try to access status it gives me the status value from initialState. I want to execute the if(!status) after the states get updated.
In Redux everything is declarative, you can't wait for the async action like you do on promise calls. Instead you have to dispatch an action for failure and store the error in the reducer.
Your thunk should look like something below
const addUser = (values) => async (dispatch) => {
dispatch({type:"ADDING_USER"}) // You can use this for showing some loading state
const response = await getDataFromAPI(values);
if(response.status === 200) {
dispatch({type:"ADD_USER_SUCCEEDED", data: response.data});
} else {
dispatch({type:"ADD_USER_FAILED", error: "API Failed" });
}
}
And then in the component you have to listen for all the values that you need and write rendering logic declaratively based on the values.
const {loading,status,error} = useSelector(state=>state.newUser)
if(error)
return <p>API Call failed</p>;
else if(loading)
return <p> Loading </p>;
else
return <p> Data saved </p>
If you need to trigger some logic based on it then you should be doing that in the thunk itself. By any chance if you need to trigger some function from the component when the value in the redux state changes, then you have to use useEffect and pass the value as dependency.
useEffect(() => triggerFunction, [status]);
In Redux:
First of all, it's preferred to update your state to include only status and its value should be changed depending on the status of the request.
status = 'idle' or
status = 'loading' or
status = 'succeded' or
status = 'failed'
Second, in your addUser function you should use the await keyword before the request.
Third, you should check the status of the request if it failed or succeeded, this can be done inside the addUser function and dispatch one of the two actions one for failure and one for success to update the status, or you can do this after dispatching your action `({type:"ADD_USER",payload:response.data}) and check the response data in the reducer to update the status.
In the UI:
You need to access the data through useSelector as you did, but to take an action after a form submission which in turn will update the status in the store. You should do this in useEffect hook.
useEffect(() =>
if (status === 'failed') {
// somecode
} else if (status === 'succeeded') {
// somecode
}
}, [status]);

Redux saga - error binding - appropriate way to bind error to local state

We have a ReactJS app that uses redux-saga.
In redux-saga we perform all the data grabbing (http webrequests).
Moreover: We use React hooks.
Scenario:
We have a button that dispatches an action to redux. Redux-saga handles this action and fires a webrequest.
Now, this webrequest fails (for instance server is down).
What we want to do now is to change the text of the button.
So in fact, we want to get back to the "scope" where the action has been dispatched.
Question:
What is the appropriate way to get back from the redux-saga to the button ?
I have found this:
https://github.com/ricardocanelas/redux-saga-promise-example
Is this appropriate to use with hooks in year 2021 ?
That example you posted sounds needlessly convoluted. Error handling with redux-sagas can be relatively straightforward. We'll consider 3 parts of the application - the UI, the store, and the actions/saga chain.
The UI:
const SomeUIThing = () => {
const callError = useSelector(state => state.somewhere.error);
const dispatch = useDispatch();
return (
<button onClick={
dispatch({
type: "API_CALL_REQUEST"
})
}>
{callError ? 'There was an error' : 'Click Me!'}
</button>
)
}
You can see that the UI is reading from the store. When clicked, it will dispatch and API_CALL_REQUEST action to the store. If there is an error logged in the store, it will conditionally render the button of the text, which is what it sounds like you wanted.
The Store
You'll need some actions and reducers to be able to create or clear an error in the store. So the initial store might look like this:
const initialState = {
somewhere: {
error: undefined,
data: undefined
}
}
function reducer(state = initialState, action){
switch(action.type){
case "API_CALL_SUCCESS":
return {
...state,
somewhere: {
...state.somewhere,
data: action.payload
}
}
case "API_CALL_FAILURE":
return {
...state,
somewhere: {
...state.somewhere,
error: action.payload
}
}
case "CLEAR":
return {
...state,
somewhere: initialState.somewhere
}
default:
return state;
}
}
Now your reducer and your store are equipped to handle some basic api call responses, for both failures and successes. Now you let the sagas handle the api call logic flow
Sagas
function* handleApiCall(){
try {
// Make your api call:
const response = yield call(fetch, "your/api/route");
// If it succeeds, put the response data in the store
yield put({ type: "API_CALL_SUCCESS", payload: response });
} catch (e) {
// If there are any failures, put them in the store as an error
yield put({ type: "API_CALL_ERROR", payload: e })
}
}
function* watchHandleApiCall(){
yield takeEvery("API_CALL_REQUEST", handleApiCall)
}
This last section is where the api call is handled. When you click the button in the UI, watchHandleApiCall listens for the API_CALL_REQUEST that is dispatched from the button click. It then fires the handleApiCall saga, which makes the call. If the call succeeds, a success action is fired off the to store. If it fails, or if there are any errors, an error action is fired off to the store. The UI can then read any error values from the store and react accordingly.
So as you see, with a try/catch block, handling errors within sagas can be pretty straightforward, so long as you've set up your store to hold errors.

How to catch changes in redux store after dispatching async action to redux-saga

In my Login component i try to login on button click, corresponding function 'handleLogin' is then getting called. In this function i dispatch an async function with my user credentials as a payload. In my saga, i am making a request putting an error to store if the response got such field, else i set a user in there. By default the error field in store is 'false'. In my component where i dispatch an action, i want to know the state of error field in store right after the successfull/unsuccessful response. When i try to login in with wrong credentials and log the state of error into the console, i first get the 'old' (initial) value of error: false, instead of error: true. Only after second login try, the error get set to true. Is there a way to know the actual state of error in store, right after dispatching async action?
Reducer:
const initialState = {
userData: [],
error: false
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case SET_USER:
return {
...state,
userData: action.payload
}
case SET_ERROR:
return {
...state,
error: action.payload
}
default:
return state;
}
};
SAGAS:
function* handleUserLoad(payload) {
try {
const user = yield call(
loginUser,
payload
);
if(user.data.errors) {
yield put(setError(true))
} else {
yield put(setError(false))
yield put(setUser(user.data.data.loginUser));
}
} catch (error) {
console.log(error);
}
}
export default function* userSaga() {
while (1) {
const {payload} = yield take(GET_USER);
yield call(handleUserLoad,payload);
}
}
PART OF LOGIN COMPONENT:
const handleLogin = async() => {
setShouldValidate(true);
if (allFieldsSet) {
try {
loginUser(user)
// LOGIN ERROR HERE DISPLAYS THE PREVIOUS STATE 'false'
// INSTEAD OF ACTUAL TRUE AFTER loginUser
// IF I LOG WITH WRONG CREDENTIALS
console.log(loginError);
} catch (e) {
console.error(e);
}
}
};
loginUser(user) in your component is an async function, so you can't expect console.log(loginError); to log the value of loginError correctly in next line.
Solution:
First:
You can bind your login component to error state in redux. Once the state is modified by Saga based upon successful/unsuccesful response, your login component will re-render. So, in render if you will console.log then you may see the value of loginError coming correctly. So, in render only you can put the component what you want on the basis of "loginError" to be shown.
Second:
You can skip using Saga and directly await for your login API response(call API using fetch/axios from here itself) in Login Component at the current place itself. In that case you will be able to get the correct login response just after the await calls gets completed and than you can continue executing your code
I have made slight changes to default value of error in store. Now it is 'null' instead of false. And thus i can check if it was set to false/true in useEffect and do the other logic.

async/await in react with redux

Hi guys I am quite new to programming and trying to understand one thing with redux and await/async functions. Basically I have this function:
//nh functions
const onSubmit = async data => {
try{
await dispatch(Login(data))
if (auth.logged != false){
addToast(content, { appearance: 'success', autoDismiss: true, })
history.push('/')
} else if (auth.logged == false){
addToast(content2, { appearance: 'error', autoDismiss: true, })
}
}finally{
console.log('Tada')
}
}
which should first authenticate an account and then push a notification. However, the await is not working at all, and it proceeds immediately to the if statement. Any tips?
Wht dave said is true. If you want to do something like that, you should dispatch your data and get the result in a props. Then you can use a useEffect to listen to this prop and do your things. Somethink like:
useEffect(() => {
// Do your things after your dispatch here
}, [yourProp]);
The "normal" pattern is to write asynchronous pseudo-actions: async function dispatching classical synchronous actions :
// reducer
// define 3 synchronous actions : loginActionBegin, loginActionSuccess,
// loginActionFailure which update your state at your convenience, for example setting a boolean flag to inform components that request is
// flying, or add user infos in store when loginActionSuccess is dispatched...
// async pseudo action
export const loginActionAsync = (loginInfos: any): any => {
return (dispatch: Dispatch, getState: any): any => {
dispatch(loginActionBegin());
return loginService.login(loginInfos)
.then(result: any): any => {
// request succeeded, add toast, user feedback...
dispatch(loginActionSuccess(result));
})
.catch((error: any) => {
// request failed, add toast, user feedback...
dispatch(loginActionFailure(error));
});
};
}
Then in a component:
// grab infos from store
const user = useSelector(state => state.user)
// on login form submit
dispatch(loginActionAsync({username:..., password:...}));
You will need async middleware to do so like https://github.com/reduxjs/redux-thunk
See :
https://redux.js.org/advanced/async-actions
https://www.digitalocean.com/community/tutorials/redux-redux-thunk
https://redux-toolkit.js.org/usage/usage-with-typescript#createasyncthunk
I'm assuming that auth is a prop that is mapped from the redux state to the component?
If so you could get around having to create an async functionality for the submit button and handling the redirect and/or state change when the component is updated with a new value from the store.
I would recommend this as you should rather be handling any error in the action itself, that way the component can be kept simple and mainly focuses on what to display.
useEffect(() => {
if (auth.logged != false){
addToast(content, { appearance: 'success', autoDismiss: true, })
history.push('/')
}
else if (auth.logged == false){
addToast(content2, { appearance: 'error', autoDismiss: true, })
}
}, [auth.logged]);
const onSubmit = data => dispatch(Login(data))

Call two different action types from one redux-saga

I've been able to get by with basic sagas implementation for now but my app is getting a little more complex. I chose sagas for the asynchronous capabilities but seem to have misunderstood how things work.
I have a global search input within my application that needs to make two different api calls (different data objects), but the search input also has it's own loading states based on the search/ status of api calls. Based on this information this is the flow of the application:
Search happens (dispatches the action GLOBAL_SEARCH_REQUEST)
The saga watcher for GLOBAL_SEARCH_REQUEST kicks off (sets loading to true for the input)
In that saga - make a call to get all users / subscriptions that match the search query
On success, set loading for the input to false
On failure, set error
the global search request saga
function* globalSearchRequestSaga(action) {
const { query } = action
console.log(`searching subscriptions and users for : ${query}`)
try {
yield put(fetchUsersRequest(query))
// call for the subscriptions (leaving it out for simplicity in this example)
yield put(globalSearchSuccess(query))
} catch (error) {
console.log(`error: ${error}`)
yield put(globalSearchFailure(error.message))
}
}
where the fetch users saga looks like
export function* fetchUsersRequestSaga(action) {
const { query } = action
const path = `${root}/users`
try {
const users = yield axios.get(path, { crossDomain: true })
yield put(fetchUsersSuccess(query, users.data))
} catch (error) {
console.log(`error : ${error}`)
yield put(fetchUsersFailure(query, error.message))
}
}
(very basic)
If I do things this way, there is an issue where the the GLOBAL_SEARCH_SUCCESS action is executed before the completion of the request for users ( and I imagine the same thing if I added in subscriptions api call as well). One solution I found is if I change the line
yield put(fetchUsersRequest(query))
to
yield call(fetchUsersRequestSaga, fetchUsersRequest(query))
where fetchUsersRequestSaga is the saga from above, and fetchUsersRequest(query) is the action creator for fetching users. This causes the asnyc functionality to work, and GLOBAL_SEARCH_SUCCESS waits for the return of the users (correct behavior).
The only issue with this is that the FETCH_USERS_REQUEST action is no longer logged to the store.
I am wondering if there is a way to either get this to properly log to the store, or return to my previous implementation with proper blocking on the put(fetchUsersRequest(query))
The put function is a non-blocking action. It won't wait till the promise/api request resolves.
I would suggest you to just call sagas directly instead of dispatching actions.
try {
yield call(fetchUsersRequestSaga, query);
yield call(globalSearchSaga, query); // or whatever its called
}
call is a blocking action. It will wait until the request finishes, so both if your calls will execute in proper order.
It's been a while since I worked with sagas but here is some code that will give you a general idea how to wait for a dispatched action.
The way it works is that when you fetch and want to wait for it to fail or succeed you give the fetch action an id, then you can pass that to the waitFor function while simultaneously dispatch the action.
If you don't want or need to wait for it then you can just dispatch the action without an id and it'll still work:
const addId = (id => fn => (...args) => ({
...fn(...args),
id: id++,
}))(0);
const withId = ({ id }, action) => ({ action, id });
function* waitFor(id) {
const action = yield take('*');
if (action.id === id) {
return action;
}
return waitFor(id);
}
function* globalSearchRequestSaga(action) {
const { query } = action;
console.log(
`searching subscriptions and users for : ${query}`
);
try {
//add id to action (id is unique)
const action = addId(fetchUsersRequest, query);
//dispatch the action and then wait for resulting action
// with the same id
yield put(action);
const result = yield waitFor(action.id);
// call for the subscriptions (leaving it out for simplicity in this example)
yield put(globalSearchSuccess(query));
} catch (error) {
console.log(`error: ${error}`);
yield put(globalSearchFailure(error.message));
}
}
export function* fetchUsersRequestSaga(action) {
const { query } = action;
const path = `${root}/users`;
try {
const users = yield axios.get(path, {
crossDomain: true,
});
yield put(//add original id to success action
withId(action, fetchUsersSuccess(query, users.data))
);
} catch (error) {
console.log(`error : ${error}`);
yield put(
withId(//add original id to fail action
action,
fetchUsersFailure(query, error.message)
)
);
}
}

Resources