Open kimsagro1 opened 7 years ago
@kimsagro1 thanks for the question.
The way I like to handle these things, my UI events result in simple object actions being dispatched. Then I wire redux-logic up to listen for those actions and perform whatever side effects etc.
So I would have thin action creator which simply dispatches what happened.
function handleSubmit(values) {
return createEvent(values); // returns the action { type: EVENT, payload: values }
}
Then I would use redux-logic to listen for EVENT actions
const eventLogic = createLogic({
type: EVENT,
process({ API, routerActions, action }, dispatch, done) {
return API.createEvent(action.payload) // POST to server
.then(result => dispatch(eventSuccess(result))) // dispatch EVENT_SUCCESS action with result
.then(() => routerActions.push('/'))
.then(() => done()); // done dispatching
});
If I use the callback version of process hook w/ dispatch and done, then I can make the API call to the server, dispatch the result EVENT_SUCCESS action, and finally change the route.
So basically we have a lean action creator that just returns an action which gets dispatched.
Then we listen for that and perform whatever processing, side effects, and dispatch more actions.
You could certainly do the route change with logic listening for EVENT_SUCCESS, but you wouldn't have to. In my example we don't need any logic for EVENT_SUCCESS, it was all handled in the first EVENT logic.
So EVENT_SUCCESS is just used for a reducer update and thus it could be used from many scenarios.
One is basically shifting what was in the thunks and fat action creators into logic. So the logic listening to EVENT is where that is done.
Personally I would consider change of route and toast notifications to be side effects. (See below for a common definition). So I would normally perform that from my redux-logic either by calling a routeChange helper or by dispatching an event.
I certainly like to keep my API calls clean (free of actions, dispatching, routing), but in my redux-logic is where I integrate things: listen for actions, perform side effects, route changes, and dispatching actions.
I like to keep my UI very simple. Often I can render and use it without everything (routing) being hooked up. So it doesn't even need a router for some usage and unit testing. I just verify that it emits the right actions for the submit. (I still do full integration testing too).
Obviously there are many different ways to approach things, but this seems to work well for me thus far.
You are certainly welcome to mix and match if you prefer one style or another. These can work together just fine, but you'll want to have clear reasons on why to put something here or there. For me I keep each individual piece (API calls, UI, action creators) as simple as possible and tie it all together in redux-logic.
If there are other use cases you'd like help figuring out, just let me know. It's really good to discuss things to learn the pros and cons of various approaches.
Definition for side effect: http://softwareengineering.stackexchange.com/a/40314
A side effect refers simply to the modification of some kind of state - for instance:
Contrary to what some people seem to be saying:
It's really very simple. Side effect = changing something somewhere.
P.S. As commenter benjol points out, several people may be conflating the definition of a side effect with the definition of a pure function, which is a function that is (a) idempotent and (b) has no side-effects. One does not imply the other in general computer science, but functional programming languages will typically tend to enforce both constraints.
but in my redux-logic is where I integrate things: listen for actions, perform side effects, route changes, and dispatching actions.
Same here. I'm fine with logic being aware of some UI stuff.
For example I have panel-based UI where each item gets its own panel for viewing and editing/deleting it. When I call an editing action creator I pass a UI config
object from that panel that determines the panel state, what mode it is in with config.mode
, was there an error with config.error
or if the panel is disabled with config.disabled
while action is being taken.
Then in item editing logic I'd get the data needed plus panel config from action.payload
, first dispatch a changed panel config (now disabled: true
for panel to go inactive and not allow more clicks), then call whatever api.editSomething
promise-based API, in a success case .then
dispatch new config with disabled: false, error: null
and any changed data load action so it would update other needed state. In failure case .catch
I'd dispatch new config with disabled: false, error: err.msg
so panel could show relevant error inline.
This logic is easy to test as well, just pass in a dummy panel config object in payload and also observe if the disabled/error/etc values got set properly in addition to validating mock API calls and returned data change action dispatching.
@tehnomaag excellent. Thanks for sharing this. It is great to get insights and ideas on ways to approach things.
I am thinking to make my REST API calls redux setup more generic - something like crudFetch.js however the stuff like notifications and redirects in crudResponse.js would need to be done outside of reducer to make it customizable.
Lets say that I can trigger deletePost from post edit or from posts list. I will make the same API and both will update store similar way, however only in the edit view I want to redirect user to posts index.
Recently I found redux-pack#logging-beforeafter and for me its great way to solve this with deletePost(payload, meta)
like:
deletePost(post, { onSuccess: (response) => push('/posts') });
I was thinking to implement it in the logic creator function, but I thought I will ask: Do you think something like that (hooks defined in meta) could become part of redux-logic?
@tb I think with redux-saga or redux-logic we have to rethink how we did things before.
Lets say that I can trigger deletePost from post edit or from posts list.
That's the point here. You should not trigger deletePost
because it would only delete a post by its definition. Instead trigger clickOnDeleteButtonOfPostEditView
or clickOnDeleteButtonOfPostListItem
. Or maybe more generic way: clickOnDeleteButton({view: EditPost})
or something similar.
Then the business logic can decide what should happen in a descriptive order:
@ms88privat Thanks for answering. Yes, I tend to agree. Working with logic or sagas we tend to approach things in a more descriptive way and more event driven than the typical imperative way from something like thunks or fat action creators.
There are no wrong ways of doing things, but certain ways seem to be more elegant and easier to maintain. Keeping things small and descriptive reacting to the appropriate events seems to be a great way to go.
@ms88privat @jeffbski This is realy isteresting, but i have one question:
In this example with deleting post - How i can combited action deletePost
with specify action deletePostOnViewPostEdit
?
// Action creators
const deletePost = (postId) => ({
type: 'POST/DELETE/START',
payload: postId,
});
const deletePostOnViewPostEdit = (postId) => ({
type: 'POST_EDIT_VIEW/POST/DELETE/START',
payload: postId,
});
// Logics
export const deletePost = createLogic({
type: 'POST/DELETE/START',
processOptions: {
successType: 'POST/DELETE/SUCCESS',
failType: 'POST/DELETE/ERROR',
},
process({Api, action}) {
const postId = action.payload;
return Api.deletePost(postId);
}
});
export const deletePostOnViewPostEdit = createLogic({
type: 'POST_EDIT_VIEW/POST/DELETE/START',
process({action}, dispatch, done) {
const postId = action.payload;
dispatch(deletePost(postId));
// I need do this only if 'POST/DELETE/SUCCESS'
dispatch(addNotification('post.delete.success'));
dispatch(redirect.push('/posts'));
// I need do this only if 'POST/DELETE/ERROR'
dispatch(addNotification('post.delete.error'));
done();
}
});
Ofc. i can repeat the logic deletePost
in deletePostOnViewPostEdit
like this:
export const deletePostOnViewPostEdit = createLogic({
type: 'POST_EDIT_VIEW/POST/DELETE/START',
process({Api, action}, dispatch, done) {
const postId = action.payload;
return Api.deletePost(postId)
.then((data) => {
dispatch({type: 'POST/DELETE/SUCCESS', payload: data});
dispatch(addNotification('post.delete.success'));
dispatch(redirect.push('/posts'));
})
.catch((err) => {
dispatch({type: 'POST/DELETE/ERROR', payload: data, error: true});
dispatch(addNotification('post.delete.error'));
})
.then(done);
}
});
but i don't think that this is a good idea (DRY rule). In this case i don't repeat to many of my code, but i wondering what when my "child logic" (in this example deletePost
) will have more code?
Another way can be send callbacks do "child logic" like this:
export const deletePostOnViewPostEdit = createLogic({
type: 'POST_EDIT_VIEW/POST/DELETE/START',
process({action}, dispatch, done) {
const postId = action.payload;
dispatch(deletePost(postId, {
onSuccess: () => {
dispatch(addNotification('post.delete.success'));
dispatch(redirect.push('/posts'));
done();
},
onError: () => {
dispatch(addNotification('post.delete.error'));
done();
}
}));
}
});
But then when i want to delete post and user in the same logic, one after one (let's say: becouse this is the last post from this user) i fall into the callback hell.
export const doSomething = createLogic({
type: 'DO_SOMETHING',
process({action}, dispatch, done) {
const {postId, userId} = action.payload;
dispatch(deletePost(postId, {
onSuccess: () => {
dispatch(deleteUser(userId, {
onSuccess: () => {
dispatch(addNotification('post-and-user.delete.success'));
done();
},
onError: () => {
dispatch(addNotification('user.delete.error'));
done();
}
}));
},
onError: () => {
dispatch(addNotification('post.delete.error'));
done();
}
}));
}
});
So this can't be a good practice...
I would ask: what is the best practice in this case?
When using thunks, UI side-effects such as route rediection, toast notifications etc, can be handled by chaining onto the promise returned by the thunk
For example in a form submit handler
To accomplish the same using logic I'm having to intercept the creation success action and do the redirection there
For example
It doesn't seem right to me to have UI concerns within logic. It now means that the create action is no-longer re-usable as it will always cause a redirection on success.
Is there another way to handle this scenario?