dispatching actions within nested functions in redux-saga - reactjs

When a user submits a form I want them to be prompted with multiple modals.
Basic structure:
My app is set up so that to show a modal all you have to do is dispatch an action with the modal body as the payload fo the action.
dispatch({type: SHOW_MODAL, payload: <MyModal />})
When the user submits the form an action is dispatched that is picked up by a saga, so now we are in saga land. What I'd like to do is have the user be shown several modals sequentially before the form is actually submitted to the backend.
// mySaga.js
function* submitForm() {
// show a modal
// then show another modal
// then submit the form
}
What is the best way of doing this? What makes the most sense to me is to use promises.
// mySaga.js
function* submitForm() {
yield call(() => {
new Promise( resolve => {
yield put({type: SHOW_MODAL, payload: <MyModal onClick={resolve} />})
})
})
...
// add as many more modals as I'd like
...
yield call(myApiCall)
}
The problem with the above is that you can't use a yield inside of that promise function because it isn't a generator. All I need is a way to do a normal dispatch of an action inside of a saga, but in looking all over the internet that does not seem trivial at all.
Am I missing something about sagas? What is the best way to do this?

I suggest to change your program a little.
It is not a good idea to dispatch <Modal/> component to store. Although you can store component inside store, but it will be difficult to pass correct props to component.
I suggest to have a variable in store, like firstModalOpened which will control if modal is showing. You can set this variable in saga and await for an action to change this variable.
// mySaga.js
function* submitForm() {
yield put({type: SHOW_MODAL, firstModalOpened: true});
take('FIRST_MODAL_CLOSED'); // Await for modal close action
...
// add as many more modals as I'd like
...
yield call(myApiCall)
}
In React the <Modal/> compoennt can be used as follows
<Modal open={props.firstModalOpened} onClose={() => dispatch({type: 'FIRST_MODAL_CLOSED'})}/>
If you have several modals which will be opened simultaneously you can call put several times and then await for all close actions to arrive before proceeding to yield call(myApiCall)
yield put({type: SHOW_MODAL, firstModalOpened: true});
yield put({type: SHOW_MODAL, secondModalOpened: true});
yield all([
take('FIRST_MODAL_CLOSED')
take('SECOND_MODAL_CLOSED')
]);

Related

Use an Saga action as condition for an if statement

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.

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.

Is it idiomatic use of redux-saga to call a worker saga directly from a React component?

I am using redux-saga having spent some time on core concepts of generators, generators with promises, and redux-saga itself. What I want below is to understand what is idiomatic and recommended, and what isn't.
In one file I define my root saga, watcher saga, and one worker saga (fetchSupplierOrders).
import {
fetchSupplierOrders,
} from './supplierOrders';
import { takeLatest} from 'redux-saga/effects';
function* watchSupplierOrdersSagas() {
yield takeLatest('REQUEST_FETCH_SUPPLIER_ORDERS', fetchSupplierOrders);
}
export default function* rootSaga() {
yield all([watchSupplierOrdersSagas()]);
}
Here is the worker saga:
export function* fetchSupplierOrders() {
try {
const supplierOrders = yield call(getSupplierOrders); // API call is in getSupplierOrders
// normally here I would use redux-saga put to hit my redux reducers
yield supplierOrders.map(({ id }) => id)
} catch (error) {
yield put({ type: 'RECEIVE_ERROR_FETCH_SUPPLIER_ORDERS', error: error.message });
}
}
I have a React component that when I click a button, it executes the worker saga. What I am trying to do here is to not go through the redux-saga watcher saga at all. I will simply execute the generator function myself in the component, and iterate through it. Usually, I would go through the watcher saga, and it would call a worker saga that would generate side effects by modifying redux state.
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state? I want the component to somehow get the results from the worker saga directly.
Here is the click handler for the button in the React component:
const handleFetchSuppliers = event => {
const it = fetchSupplierOrders({ payload: event.target.value });
const res1 = await it.next().value;
console.log('res1', res1);
const res2 = it.next(res1);
console.log('res2', res2);
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Is it common to do this (executing a worker saga from a component to get the results of an API request)? But if I do it this way then I lose the benefit of using call (isn't this useful because it's easier to test?)
Before redux-saga I would simply dispatch a thunk using redux-thunk, which is basically using async/await all the way through.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state?
If redux is not involved, then redux-saga is not the right tool to use. Just use the normal react approach: make the api request (often in componentDidMount), then wait for that promise to complete (with .then or await), then call setState.
If you want to have multiple ways to do the fetch (both via a saga, and via a direct call), then you could put the fetch into a helper function (regular function, not generator). The component and the saga could then both make use of the helper function, each wrapping it with whatever extra work they need to do.
For example:
// helper
async function fetchStuff() {
const response = await fetch('some Url');
if (!response.ok) {
throw response.status;
}
const data = await response.json();
return data.map(({ id }) => id);
}
// In a saga...
function* watchFetch() {
yield takeLatest('doFetch', fetchStuffSaga);
}
function* fetchStuffSaga() {
try {
const data = yield call(fetchStuff);
yield put({ type: 'success', payload: data });
} catch (err) {
yield put({ type: 'error', payload: err });
}
}
// In a component that dispatches an action:
componentDidMount() {
this.props.dispatch({ type: 'doFetch' });
}
// In a component that doesn't want to dispatch an action:
async componentDidMount() {
try {
const data = await fetchStuff();
this.setState({ data });
} catch (err) {
this.setState({ error: err });
}
}
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Sagas are not meant for manual iteration. If you try to manually iterate through a saga, you either need to have specialized knowledge about exactly what the saga will yield in what order, or you basically need to re-implement redux-saga yourself. The former is brittle and tightly coupled, the latter is a waste of effort.
Is it common to do this (executing a worker saga from a component to get the results of an API request)?
No.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
They're both trying to handle the same kinds of things (asynchronous actions). Your codebase will be simpler if you use just one approach, then trying to mix and match both.

Handling AJAX Errors with Redux Form

I'm new to React/Redux so I'm building a simple blog app with Redux Form to help me learn. Right now I'm unclear on how I would handle ajax errors when submitting data from the form to the api in my action. The main issue is that I'm using the onSubmitSuccess config property of Redux Form and it seems to always be called, even when an error occurs. I'm really unclear on what triggers onSubmitSuccess or onSubmitFail. My onSubmitFail function is never executed, but my onSubmitSuccess function always is, regardless of whether an error occurred or not.
I read about SubmissionError in the redux-form docs, but it says that the purpose of it is "to distinguish promise rejection because of validation errors from promise rejection because of AJAX I/O". So, it sounds like that's the opposite of what I need.
I'm using redux-promise as middleware with Redux if that makes any difference.
Here's my code. I'm intentionally throwing an error in my server api to generate the error in my createPost action:
Container with my redux form
PostsNew = reduxForm({
validate,
form: 'PostsNewForm',
onSubmit(values, dispatch, props) {
// calling my createPost action when the form is submitted.
// should I catch the error here?
// if so, what would I do to stop onSubmitSuccess from executing?
props.createPost(values)
}
onSubmitSuccess(result, dispatch, props) {
// this is always called, even when an exeption occurs in createPost()
},
onSubmitFail(errors, dispatch) {
// this function is never called
}
})(PostsNew)
Action called by onSubmit
export async function createPost(values) {
try {
const response = await axios.post('/api/posts', values)
return {
type: CREATE_POST,
payload: response
}
} catch (err) {
// what would I do here that would trigger onSubmitFail(),
// or stop onSubmitSuccess() from executing?
}
}
In your case, redux-form doesn't know whether form submission was succeeded or not, because you are not returning a Promise from onSubmit function.
In your case, it's possible to achieve this, without using redux-promise or any other async handling library:
PostsNew = reduxForm({
validate,
form: 'PostsNewForm',
onSubmit(values, dispatch, props) {
// as axios returns a Promise, we are good here
return axios.post('/api/posts', values);
}
onSubmitSuccess(result, dispatch, props) {
// if request was succeeded(axios will resolve Promise), that function will be called
// and we can dispatch success action
dispatch({
type: CREATE_POST,
payload: response
})
},
onSubmitFail(errors, dispatch) {
// if request was failed(axios will reject Promise), we will reach that function
// and could dispatch failure action
dispatch({
type: CREATE_POST_FAILURE,
payload: errors
})
}
})(PostsNew)
For handling asynchronous actions you should use redux-thunk, redux-saga or an other middleware which makes it possible to run asynchronous code.

How to show a message after Redux dispatch

When a button is clicked I call the following:
this.props.dispatch(addNote(this.state.noteContent))
This in turn calls the following action:
export default function addNote(note){
return dispatch => {
axios.post('http://localhost:2403/notes', {
Body: note
})
.then(function (response) {
dispatch({
type: ADD_NOTE,
payload: response.data
})
})
.catch(function (error) {
console.log(error)
})
}
}
My reducer then updates the state and a new note is added to the list, so it's all working correctly.
However I now want to show a message in the UI which says "Note has been added successfully". Ideally, this would be a part of a wider notifications system, but for now, I just need to figure out how to report success (or failure) on this one action.
How do I achieve this?
You can return a promise from your dispatch using redux-thunk, why not use that? Try this code in your thunk:
export default function addNote(note){
return dispatch => {
return axios.post('http://localhost:2403/notes', {
Body: note
})
... //function continues
Then you can do:
this.props.dispatch(addNote(this.state.noteContent)).then(successFunc, failureFunc);
successFunc = function() {
//Show message on UI
}
You can use a toast component (like this https://www.npmjs.com/package/react-toastify) that sits on top of your routes in your app that renders every time an action in redux is called.
In each of your action functions, you can have a call to a message reducer that updates the state inside message reducer. Each time the state in that reducer is changed, the toast component will re-render and last for a certain amount of time or until a user manually closes the toast component.

Resources