David Escalera
David Escalera

Reputation: 521

A pattern to manage intial async actions with redux sagas for fetching initial data?

Until now I've been using redux-thunk for async actions. On application startup I use to have to load some data from some server. So what I do is to create async actions and then use async/await in order to know when they finished. While async actions are fetching I render a splashscreen. When they finish then I start the application.

Now I'm switching to redux sagas and I don't know how to do it with them. I cannot use async/await. What I thought is to have a boolean var in every object of the store that needs to fetch data. However I would like to know if there is any pattern to manage it in a clean way. Does anybody know any pattern for this purpose?

// example with thunks

import { someAsyncAction, someAsyncAction2 } from './actions';

const initialDispatches = async (store) => {
  await store.dispatch(someAsyncAction());
  await store.dispatch(someAsyncAction2());
};

export default initialDispatches;

Upvotes: 0

Views: 1839

Answers (2)

Raza Jamil
Raza Jamil

Reputation: 274

I wrote about creating a structure on top of redux-saga to facilitate async operations by providing an initial action and then loading/success/error states based on the result of the operation. It's in 2 parts, first sync and then async.

It basically lets you write your reducers declaratively, like an object. You only have to call the initial action and the saga takes care of the rest and your UI can respond to the results when loading/success/error actions are triggered. Below is what the reducer looks like.

const counterAsync = {
  initialState: {
    incrementAsync_result: null,
    incrementAsync_loading: false,
    incrementAsync_success: false,
    incrementAsync_error: false,
  },

  incrementAsync: {
    asyncOperation: incrementAPI,
    action: ({number}) => {
      type: ACTION_INCREMENT_ASYNC,
      payload: {
        number: number
      }
    }
    loading: {
      action: (payload) => {
        return {
          type: ACTION_INCREMENT_ASYNC,
          payload: { ...payload }
        }
      },
      reducer: (state, action) => {
        state.incrementAsync_loading = true
        state.incrementAsync_success = false
        state.incrementAsync_error = false
      }
    },
    success: {
      action: (payload) => {
        return {
          type: ACTION_INCREMENT_ASYNC,
          payload: { ...payload }
        }
      },
      reducer: (state, action) => {
        state.incrementAsync_result = action.payload
        state.incrementAsync_loading = false
        state.incrementAsync_success = true
        state.incrementAsync_error = false
      }
    },
    fail: {
      action: (payload) => {
        return {
          type: ACTION_INCREMENT_ASYNC,
          payload: { ...payload }
        }
      },
      reducer: (state, action) => {
        state.incrementAsync_result = action.payload
        state.incrementAsync_loading = false
        state.incrementAsync_success = false
        state.incrementAsync_error = true
      }
    }
  },
}

We use a slightly more heavy weight version of this pattern at work and it's much better than vanilla redux/saga.

Let me know if you have any questions!

https://medium.com/@m.razajamil/declarative-redux-part-1-49a9c1b43805 https://medium.com/@m.razajamil/declarative-redux-part-2-a0ed084e4e31

Upvotes: 0

MorKadosh
MorKadosh

Reputation: 6006

In my opinion there's no right/wrong pattern in this kind of cases.

Iv'e put up an example for you of how your goal could be achieved using saga.

The basic idea: have a separate saga for each resource (for instance, I used to split into feature sagas), and a saga for the initialization. Then the main root saga will run them all parallelly, and you will be able to trigger the initialization saga somewhere in your app and let it all happen:

Note: this example is super naive and simple, you should find a better way for organizing everything up, I just tried to keep it simple.

const {Provider, connect} = ReactRedux;
const {createStore, applyMiddleware} = Redux;
const createSagaMiddleware = ReduxSaga.default;
const {takeEvery, takeLatest} = ReduxSaga;
const {put, call, all, fork} = ReduxSaga.effects;

const initialState = {
    fruits: [],
  vegtables: []
};

const reducer = (state = initialState, action) => {
    switch (action.type) {
    case 'SET_FRUITS':
        return {
        ...state,
        fruits: [
            ...action.payload.fruits
        ]
      }
    case 'SET_VEGTABLES':
        return {
        ...state,
        vegtables: [
            ...action.payload.vegtables
        ]
      }
  }
    return state;
};

//====== VEGTABLES ====== //
async function fetchVegtables() {
    return await new Promise((res) => {
    setTimeout(() => res([
        'Cuecumber',
      'Carrot',
      'LEttuce'
    ]), 3000)
  });
}

function* getVegtables() {
    const vegtables = yield call(fetchVegtables);
  yield put({ type: 'SET_VEGTABLES', payload: { vegtables } })
} 

function* vegtablesSaga() {
    yield takeEvery('GET_VEGTABLES', getVegtables);
}
//====== VEGTABLES ====== //

//====== FRUITS ====== //
async function fetchFruits() {
    return await new Promise((res) => {
    setTimeout(() => res([
        'Banana',
      'Apple',
      'Peach'
    ]), 2000)
  });
}

function* getFruits() {
    const fruits = yield call(fetchFruits);
  console.log(fruits)
  yield put({ type: 'SET_FRUITS', payload: { fruits } })
} 

function* fruitsSaga() {
    yield takeEvery('GET_FRUITS', getFruits);
}
//====== FRUITS ====== //

//====== INIT ====== //
function* initData() {
    yield all([
    put({ type: 'GET_FRUITS' }),
    put({ type: 'GET_VEGTABLES' })
  ]);
}

function* initSaga() {
    yield takeLatest('INIT', initData);
}
//====== INIT ====== //

// Sagas
function* rootSaga() {
  yield all([
    yield fork(initSaga),
    yield fork(fruitsSaga),
    yield fork(vegtablesSaga),
  ]);
}

// Component
class App extends React.Component {
    componentDidMount() {
    this.props.dispatch({ type: 'INIT' });
  }
  render () {
    return (
            <div>
              <div>fruits: {this.props.fruits.join()}</div>
        <div>vegtables: {this.props.vegtables.join()}</div>
            </div>
    );
  }
}

// Store
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

const ConnectedApp = connect((state) => ({
    fruits: state.fruits,
  vegtables: state.vegtables
}))(App);

// Container component
ReactDOM.render(
  <Provider store={store}>
    <ConnectedApp />
  </Provider>,
  document.getElementById('root')
);

As you can see, I have two resources: fruits and vegetables. Each resource has it's own saga, which is responsible for watching for GET actions dispatched somewhere. Each of them using basic saga effects such as call, put etc to fetch the resources asyncly, and then they dispatch it to the store (and then the reducer handles them).

In addition, Iv'e set up an initSaga which uses the all effect to trigger all of the resource fetching sagas in a parallel way.

You can see the whole example running here:

https://jsfiddle.net/kadoshms/xwepoh5u/17/

Upvotes: 3

Related Questions