Im trying to comprehend the art of redux saga, but faced this situation:
I have useEffect hook that works correctly(works one time when changing url params). This hook dispatches action(created by redux-saga-routines) only one time.
const params = useParams().params;
useEffect(() => {
if (urlTriggers.some(item => item === params)) {
dispatch(setItemsCollection({ itemType: params }));
toggleVisibleMode(true);
} else {
toggleVisibleMode(false);
}
}, [params]);
Saga watcher reacts to the dispatched action
export function* setItemsCollectionWatcher() {
yield takeEvery(setItemsCollection.TRIGGER, setItemsCollectionWorker);
}
And then calls saga worker
function* setItemsCollectionWorker(action) {
const { itemType } = action.payload;
try {
yield put(toggleIsFetching({ isFetching: true }));
const itemsCollection = yield call(() => {
return axios.get(`http://localhost:60671/api/items/${itemType}/?page=1&count=2`).then(response => response.data.items);
});
yield put(setItemsCollection.success({ itemsCollection }));
yield put(toggleIsFetching({ isFetching: false }));
} catch (error) {
console.log(error);
} finally {
yield put(setItemsCollection.fulfill());
}
}
This saga listens all saga watchers
export default function* saga() {
yield all([
setBackgroundWatcher(),
setItemsCollectionWatcher(),
])
}
saga running
sagaMiddleware.run(saga);
export const setItemsCollection = createRoutine('showcase/SET_ITEMS_COLLECTION');
export const toggleIsFetching = createRoutine('showcase/TOGGLE_IS_FETCHING');
const showcase = createReducer(
{
itemsCollection: [],
isFetching: false,
},
{
[setItemsCollection.SUCCESS]: (state, action) => {
state.itemsCollection = action.payload.itemsCollection;
},
[toggleIsFetching.TRIGGER]: (state, action) => {
state.isFetching = action.payload.isFetching;
},
}
);
But I have 2 axios requests instead of just one.
Your dispatch from the client is type setItemsCollection which should be fine (though I generally 'USE_HUGE_OBVIOUS_TEXT_LIKE_THIS'). The response from the promise in your saga is the same: setItemsCollection whereas, depending on what you're trying to render, you may want to call your reducer with something entirely different.
At a glance, I'd suggest changing this line to something else (and matching what the reducer is listening for). I wonder if it's causing a crossed wire somewhere.
Related
I am using Redux for state management and saga as a middleware. For some reason my app is in some infinite loop state of calling API endpoint.
This is my actions:
export const GET_USERS = "GET_USERS";
export const getUsers = () => ({
type: GET_USERS,
});
export const GET_USERS_SUCCESS = `${GET_USERS}_SUCCESS`;
export const getUsersSuccess = (data) => ({
type: GET_USERS_SUCCESS,
payload: data,
});
export const GET_USERS_FAIL = `${GET_USERS}_FAIL`;
export const getUsersFail = (error) => ({
type: GET_USERS_FAIL,
payload: error,
});
This is saga:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.getUsers, getUsers$)]);
}
This is a reducer:
export default (state = initialState(), action) => {
const { type, payload } = action;
switch (type) {
case actions.GET_USERS:
return {
...state,
users: {
...state.users,
inProgress: true,
},
};
case actions.GET_USERS_SUCCESS:
return {
...state,
users: {
inProgress: false,
data: payload,
},
};
case actions.GET_USERS_FAIL:
return {
...state,
users: {
...state.users,
inProgress: false,
error: payload,
},
};
default:
return state;
}
};
And this is a component connected with redux:
const Home = (props) => {
useEffect(() => {
props.getUsers();
console.log('props', props.data);
}, []);
return(
<h1>Title</h1>
);
}
const mapStateToProps = ({
users: {
users: {
data
}
}
}) => ({data})
export default connect(mapStateToProps, {getUsers})(Home);
Why is this happening?
This is due to the fact that you misused the sagas in your example. As with any other effect creator as the first parameter must pass a pattern, which can be read in more detail in the documentation. The first parameter can also be passed a function, but in a slightly different way. View documentation (block take(pattern)).
In your case, you are passing a function there that will return an object
{
type: 'SOME_TYPE',
payload: 'some_payload',
}
Because of this, your worker will react to ALL events that you dispatch.
As a result, you receive data from the server, dispatch a new action to save data from the store. And besides the reducer, your getUsers saga will be called for this action too. And so on ad infinitum.
Solution
To solve this problem, just use the string constant actions.GET_USERS that you defined in your actions.
And your sagas will look like this:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.GET_USERS, getUsers$)]);
}
This should fix your problem.
I'm using redux with React to manage states but when I called two dispatch function from one action creator, it's return state from the first dispatch but unable to get updated state after another dispatch call.
I've tried to call dispatch from different reducers and tried to call after API call.
Here are my actions.
export const setLoader = (loader) => dispatch => {
dispatch({ type: SET_LOADER, payload: loader });
};
export const fetchCategory = (setLoader) => async dispatch => {
setLoader(true);
try {
const instance = axios.create();
instance.defaults.headers.common['Authorization'] = AUTHORIZATION_TOKEN;
const response = await instance.get(API_PATHS.SERVICE_CATEGORY_API);
dispatch({ type: FETCH_CATEGORY, payload: response.data });
} catch (e) {
setLoader(false);
}
};
Here i defined reducers:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_CATEGORY:
return { ...state, categoryList: action.payload };
case SET_LOADER:
return { ...state, isLoading: action.payload };
default:
return state;
}
};
Here my component connected with redux:
const mapStateToProps = state => {
return ({
categoryList: state.locator.categoryList
});
}
export default connect(
mapStateToProps,
{ fetchCategory, setLoader }
)(ServiceLocator);
I expect the output to return updated categoryList, but the actual it returns a blank array.
You are performing an asynchronous task in your action creator, which Redux can't handle without a middleware. I recommend using the middleware redux-thunk. This will allow you to perform asynchronous actions in your action creators and dispatch multiple times.
Hope this helps!
UPDATE:
If you have the redux-think middleware installed and added to Redux (per your comment), then next I would look at setLoader() - it looks like that function is curried and I don't think you want it to be. I would remove the setLoader() step and dispatch that action directly from fetchCategory():
export const fetchCategory = () => async dispatch => {
dispatch({ type: SET_LOADER, payload: true });
try {
const instance = axios.create();
instance.defaults.headers.common['Authorization'] = AUTHORIZATION_TOKEN;
const response = await instance.get(API_PATHS.SERVICE_CATEGORY_API);
dispatch({ type: FETCH_CATEGORY, payload: response.data });
} catch (e) {
dispatch({ type: SET_LOADER, payload: false });
}
};
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;
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/
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
Update: I'm using React-Boilerplate, unmodified from the original except in the containers / components. The reducers run multiple times, sometimes more than twice, when a new action is dispatched for the first time, but not when the same action is dispatched subsequently. The actions themselves are not fired on the repeated reducer calls, but the state is updated and re-renders the component.
For example: If I dispatch action1 which updates reducerCase1, but not action2 which updates reducerCase2, action1 will run once, and reducerCase1 will run twice. action2 and reducerCase2 will not run. If I then dispatch action3 which updates reducerCase3, reducerCase1 will be called multiple times, but action1, action2, and reducerCase2 will not be called.
If I dispatch action2 in the same manner as action1, it will be treated the same way as action1 and reducerCase1, running the reducer multiple times without firing the action.
If after all this I dispatch action3 a second time, reducerCase1 will not be run at all (as should be the case).
Here I'm interested GET_CATEGORIES and GET_CATS_COMPLETED actions:
here is the console log inside the reducer:
export default function Categories(state = initialState, action) {
switch (action.type) {
case GET_CATEGORIES:
debugger;
console.log('getting categories...');
return state.set('isLoading', true);
case GET_CATEGORIES_COMPLETED:
debugger;
console.log('setting categories...');
return state
.set('categories', fromJS(action.cats))
.set('isLoading', false);
Since this is happening with all of my reducers, I assume it has something to do with mapDispatchToProps:
const mapStateToProps = createStructuredSelector({
categories: makeSelectCategories(),
ownerId: makeSelectProfileId(),
selectedCategoryId: makeSelectSelectedCategoryId(),
isLoading: makeSelectIsLoading(),
});
const mapDispatchToProps = {
getCategories,
setCategory,
};
const withConnect = connect(
mapStateToProps,
mapDispatchToProps
);
const withReducer = injectReducer({ key: 'CategoryContainer', reducer });
const withSaga = injectSaga({ key: 'CategoryContainer', saga });
export default compose(
withSaga,
withReducer,
withConnect
)(CategoryContainer);
and here are my actions:
export function getCategories() {
console.log('inside getCategories()');
return {
type: GET_CATEGORIES,
};
}
export function getCategoriesCompleted(cats) {
return {
type: GET_CATEGORIES_COMPLETED,
cats,
};
}
and finally the saga:
export default function* CategoryContainerSaga() {
yield takeLatest(GET_CATEGORIES, getCategories);
}
function* getCategories() {
try {
const plidParam = yield call(getPlParam);
const profileId = yield call(getProfileId);
const url = getUrl();
const { categories2, playlist } = yield call(
getCatsRequest,
url,
profileId,
plidParam
);
yield put(getCategoriesCompleted(categories2));
if (playlist) yield put(setPlaylist(playlist));
} catch (error) {
yield put(getCategoriesCompleted([]));
yield put(setError(error.message));
}
}
Thanks to lecstor's comment, I was able to determine that this is expected behavior of Redux devtools.
I'm new to Redux Saga, coming from Redux Thunk. In some situations, I need to know whether an API call fails or succeeds from inside the view from which I called the action. With Redux Thunk, I would do something like the following.
My component and action creator would look like this:
class MyComponent extends Component {
componentDidMount() {
this.props.actions.fetchItems()
.then(result => {
if (result.status === 'OK') {
console.log('Request succeeded, take particular action in this view')
}
else {
console.log('Request failed, take other action in this view')
}
})
}
render() {
return (
<div>My view</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
items: state.items,
}
}
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators({
...actions,
}, dispatch),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MyComponent)
import * as t from './actionTypes'
import {CALL_API} from '../api'
const xhrGetItems = (params) => (dispatch, getState) => {
const action = {
[CALL_API]: {
type: t.XHR_ITEMS_FETCH,
endpoint: `http://www.example.com/myendpoint`,
method: 'get',
}
}
return dispatch(action)
}
My API middleware catches all actions with a CALL_API property, uses an ajax library to make the appropriate call, then returns a fulfilled promise. My reducer is set up to handle each of the possible states of the api call (success, failure, pending). All the while, I can still check the result of the call in my view where it originated.
So my question is, how can this be accomplished with Redux Saga? Right now my saga api middleware is doing everything it should be doing, but when I call fetchItems() in my view, the result is a plain JS object, so I can't check whether or not it succeeded.
It's possible that I'm going about this completely wrong, too. Any suggestions are very much appreciated.
A common pattern with redux and redux-saga is to create 3 Actions for an API call. In your case I would create:
LIST_ITEMS_START
LIST_ITEMS_SUCCEEDED
LIST_ITEMS_FAILED
Your saga would look something like this:
function* watchListItemsStart() {
yield takeLatest(LIST_ITEMS_START, listItemsStartFlow)
}
function* listItemsStartFlow() {
try {
const result = yield call(api.getItems)
if (result.status === 'OK') {
yield put(LIST_ITEMS_SUCCEEDED, items)
} else {
throw new Error(result.error)
}
} catch (error) {
yield put(LIST_ITEMS_FAILED, error)
}
}
The sagas have cleanly abstracted away the API side-effect. The reducer can concentrate on state management:
switch (action.type) {
case LIST_ITEMS_START:
return {
...state,
isLoading: true,
error: null,
items: [],
}
case LIST_ITEMS_SUCCEEDED:
return {
...state,
isLoading: false,
items: action.payload.items,
}
case LIST_ITEMS_FAILED:
return {
...state,
isLoading: false,
error: action.payload.error.getMessage(),
}
}
Now you have everything you need in your store that can be selected and reacted on in the component.
class MyComponent extends Component {
componentDidMount() {
this.props.fetchItems()
}
render() {
const { isLoading, items, error } = this.props
// Here you can react to the different states.
return (
<div>My view</div>
)
}
}
connect(state => ({
isLoading: itemsSelectors.isLoading(state),
items: itemsSelectors.getItems(state),
error: itemsSelectors.getError(state),
}), {
fetchItems: actions.fetchItems
})(MyComponent)
An important point here is, that your component gets very dumb. It just gets props and does not handle the result of the API call (no promise.then here).
I hope this answer will help you.