martins
martins

Reputation: 10009

How do I call a Redux Saga action in a `onClick` event?

I just started a new project using infinitered/ignite. I've added my getUserToken function to the APITestScreen So I know that the function works as expected, but I'm not able to hook the method up with the onPress function to the button I added to the LaunchScreen.

I have imported it to the view, but nothing happens when I click the button. I have added an alert and a console.log, but they are not triggered. What should I do to get the fetchUserToken to run when I click the button?

The entire project posted posted at Github.

my view

 import getUserToken from '../Sagas/AuthSagas.js';
 <RoundedButton text="Fetch token" onPress={ getUserToken }  />

App/Redux/AuthRedux.js

import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'

/* ------------- Types and Action Creators ------------- */

const { Types, Creators } = createActions({
  tokenRequest: ['username'],
  tokenSuccess: ['token'],
  tokenFailure: null
})

export const AuthTypes = Types
export default Creators

/* ------------- Initial State ------------- */

export const INITIAL_STATE = Immutable({
  token: null,
  fetching: null,
  error: null,
  username: null
})

/* ------------- Reducers ------------- */

// request the token for a user
export const request = (state, { username }) =>
  state.merge({ fetching: true, username, token: null })

// successful token lookup
export const success = (state, action) => {
  const { token } = action
  return state.merge({ fetching: false, error: null, token })
}

// failed to get the token
export const failure = (state) =>
  state.merge({ fetching: false, error: true, token: null })

/* ------------- Hookup Reducers To Types ------------- */

export const reducer = createReducer(INITIAL_STATE, {
  [Types.TOKEN_REQUEST]: request,
  [Types.TOKEN_SUCCESS]: success,
  [Types.TOKEN_FAILURE]: failure
})

App/Sagas/AuthSagas.js

import { call, put } from 'redux-saga/effects'
import { path } from 'ramda'
import AuthActions from '../Redux/AuthRedux'

export function * getUserToken (api, action) {
  console.tron.log('Hello, from getUserToken');
  alert('in getUserToken');
  const { username } = action
  // make the call to the api
  const response = yield call(api.getUser, username)

  if (response.ok) {
    const firstUser = path(['data', 'items'], response)[0]
    const avatar = firstUser.avatar_url
    // do data conversion here if needed
    yield put(AuthActions.userSuccess(avatar))
  } else {
    yield put(AuthActions.userFailure())
  }
}

Sagas/index.js

export default function * root () {
  yield all([
    // some sagas only receive an action
    takeLatest(StartupTypes.STARTUP, startup),

    // some sagas receive extra parameters in addition to an action
    takeLatest(GithubTypes.USER_REQUEST, getUserAvatar, api),

    // Auth sagas
    takeLatest(AuthTypes.TOKEN_REQUEST, getUserToken, api)
  ])
}

Upvotes: 4

Views: 6131

Answers (2)

alechill
alechill

Reputation: 4502

Sagas are great because they allow long running processes to control application flow in a completely decoupled manner, and can be sequenced via actions, allowing you to parallelise/cancel/fork/reconcile sagas to orchestrate your application logic in a centralised place (ie think of it as being able to link together actions, incorporating side effects along the way)

By importing your generator function and calling it directly like a normal function won't work and if it did would be bypassing saga functionality, for example if you press a second time or third time on that button, it will always execute the entire generator again from start to finish, which as they involve async operations could result in you say trying to store or use a token that is then immediately invalidated by a subsequent saga

Better practice would be to have your saga always listening for specific actions to trigger further worker sagas, keeping them decoupled, and allowing them to control their own flow.

In this case you would dispatch an action onPress, and have a long running parent saga listening for that action which then hands off to your current one to do the actual work. This listening saga would then have control over cancelation of previous invocations using takeLatest would cancel the previous saga invocation, so that a subsequent button press while the previous was still in flight would always take precedence, and your token cannot accidentally go stale

// AuthActions.js

// add a new action (or more probably adapt fetchUserToken to suit)...
export const GET_USER_TOKEN = 'auth/get-user-token'
export const getUserToken = (username) => ({
  type: GET_USER_TOKEN, 
  payload: username
})

// view

import {getUserToken} from './AuthActions'

// this now dispatches action (assumes username is captured elsewhere)
// also assumes store.dispatch but that would more likely be done via `connect` elsewhere
<RoundedButton text="Fetch token" onPress={ () => store.dispatch(getUserToken(this.username)) }  />

// AuthSagas.js

import api from 'someapi'
import actions from 'someactions'
import {path} from 'ramda'
import {put, call, takeLatest} from 'redux-saga/effects'
import AuthActions from '../Redux/AuthRedux'

// this will be our long running saga
export function* watchRequestUserToken() {
  // listens for the latest `GET_USER_TOKEN` action, 
  // `takeLatest` cancels any currently executing `getUserToken` so that is always up to date
  yield takeLatest(AuthActions.GET_USER_TOKEN, getUserToken)
}

// child generator is orchestrated by the parent saga
// no need to export (unless for tests) as it should not be called by anything outside of the sagas
function* getUserToken (action) { // the actual action is passed in as arg
  const username = action.payload
  // make the call to the api
  const response = yield call(api.getUser, username)

  if (response.ok) {
    const firstUser = path(['data', 'items'], response)[0]
    const avatar = firstUser.avatar_url
    // do data conversion here if needed
    yield put(AuthActions.userSuccess(avatar))
  } else {
    yield put(AuthActions.userFailure())
  }
}

// main.js (example taken from https://redux-saga.js.org/) adapted to suite

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import {reducer} from './AuthRedux'
import {watchRequestUserToken} from './AuthSagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
export const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// then run the saga
sagaMiddleware.run(watchRequestUserToken)

Upvotes: 5

bluehipy
bluehipy

Reputation: 2294

On button you are calling fetchUserTocken but in script you define getUserToken.

Upvotes: 0

Related Questions