Search code examples
reactjsreact-hooksredux-saga

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 ?


Solution

  • 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.