Background
I am having react application which uses redux and redux-thunk.
Trying to understand how to orchestrate async calls. How to dispatch actions only if previously dispatched action (API call) was successul.
Use Case
I have EmployeesComponent. That component has two dummy presentational components
EmployeesList which basically lists employees.
AddEmployeeForm which is a modal for adding new employees.
When rendering the EmployeesComponent, within the useEffect hook, I dispatch an action to load employees. So far, so good.
When I user enters data in the AddEmployeeForm and submits the form, I want to achieve the load all employees again if the API call was successful.
I have issues understanding how to know if I am okay to dispatch the action to load all employees. Here is how my handler for submit looks like.
const handleSubmit = async (e) => {
e.preventDefault();
console.log("Handle submit add new employee..");
const employeeData = inputs;
// Validate data from the AddEmployeeForm
// in case of validation errors, abort
dispatch(addEmployee(employeeData)); // calls API to save the data
// How to await the result of the dispatched action (addEmployee) before continuing?
// If addEmployee was not successful abort
// The error data from the API call is saved in the redux store,
// so the UI gets notified and can display it properly
// If addEmployee was successful, I want to
clearForm(); // local state from the component
handleCloseForm(); // local state from the component
dispatch(loadEmployees());
};
Challenges
The current approach in handleSubmit smells to me. The business logic is kinda placed in the UI and the benefits of clearly separating the UI and business logic are lost. I have difficulties to figure out what is the approach to go and how to handle this use cases.
Another use case example:
dispatch login user
if login was successful, you get token from the backend
if login was successful, dispatch an action to load user data based on retrieved token
In Redux, async logic is normally written in the form of "thunk" functions. That lets you extract logic that needs to interact with the store outside of your React components, especially async logic that needs to do things like fetching data and dispatching the results.
You can also return a promise from a thunk and await that promise in the component after dispatching.
Related
I'm currently exploring the new Redux Toolkit setup. However, I stumbled upon CreateAsyncThunk when fetching data, but I cannot find pros and cons using this over a normal service class using e.g. axios.
Can someone explain to me why I should/shouldn't use the CreateAsyncThunk to fetch data from an api?
Redux is the place where you store your data.
So after your request you need to dispatch an action, to get your data into Redux.
Now, often your components are also interested in the question if you already are running a request.
That information could be stored in your component, but then other components won't know. So you store it in Redux, too.
Now you need to also dispatch an action to signal Redux that you started loading data.
The same goes for errors. You need to dispatch an action to signal Redux that loading failed.
createAsyncThunk takes code from you and executes that. But before, it dispatches an action. And depending if your code was a succcess or had an exception, it also dispatches either a "fulfilled" or "rejected" action for you.
So you only have to concentrate on the code you want to write.
So nothing stops you from doing
const myThunk = createAsyncThunk('myThunk', myApiService.getSomeData)
in the end - which would be using your api service, coupled with createAsyncThunk for the lifecycle actions - assuming your api service only takes one argument, otherwise it'd be
const myThunk = createAsyncThunk('myThunk', ({name, password}) => myApiService.getSomeData(name, password))
since cAT only forwards one argument from the action creator call.
I have stored a payload in the redux store. I couldn't access the store object. How to access the store object? I wanted to access it by redux-saga. I tried even by normal redux to access store but could'nt!
I wanted to access it by redux-saga. I tried even by normal redux to access store but could'nt!
In general case redux-saga combines two things - redux middleware and independent process manager. If you need only to fetch and directly manipulate with redux internal state without process managing, probably, it's better to write own simple middleware (https://redux.js.org/docs/advanced/Middleware.html ) and maybe compose it with saga.
Of course, you can easily use select effect (https://redux-saga.js.org/docs/api/#selectselector-args ). but take in account, that middleware first forwards the action to the reducers and then notifies the Sagas. This means that when you query the Store's State, you get the State after the action has been applied.
Most likely it's better to store mandatory information info current saga's closure, for example, before while-true loop in dedicated saga process.
function* someSaga() {
const localState = {};
while(true) {
const action = yield take('ACTION_PATTERN');
// some logic to dispatch the action
yield put('SOME_AFTER_ACTION')
}
}
Basically need to build a warning modal , when user tries to move from current page/screen to another page , showing there are some saved changes .
Any implementations using redux and redux saga
Sagas are the lib for this - they watch for any action of a specified type. Navigation will take two actions: one to indicate that navigation is about to happen (which the saga will watch) and one to actually update the current page. The saga watches for actions of the first type and shows a warning dialog if the data has changed.
Ex:
function showWarning(action) {
if (/* data has been changed but not saved */) {
displayWarningDialog(action.pageToNavigateTo)
}
else {
// action that updates the page/location
completeNavigation(action.pageToNavigateTo)
}
}
function* mySaga() {
// NAVIGATE_TO_PAGE_X are the actions that get fired when a user changes pages
yield takeEvery("NAVIGATE_TO_PAGE_1", showWarning)
yield takeEvery("NAVIGATE_TO_PAGE_2", showWarning)
}
There is the amazing Redux DevTools for state debugging. This tool was built by Redux author himself.
Here are its features
Lets you inspect every state and action payload
Lets you go back in time by “cancelling” actions
If you change the reducer code, each “staged” action will be
re-evaluated
If the reducers throw, you will see during which action this
happened, and what the error was
With persistState() store enhancer, you can persist debug sessions
across page reloads
I've thought about this recently as well and been thinking about writing some form of middleware to intercept routing actions.
When intercepting a routing action, the middleware could determine if application state indicates the user is editing some unsaved data, and if so, dispatch a different action instead. That action should reduce state and cause a warning to render. The user could then confirm wanting to continue navigating by dispatching an action also intercepted by the middleware to continue the routing process.
I would like many different redux actions in my app to all trigger common functionality in a specific reducer. I would like to avoid having to either repeat some flag in every action creator (like doThing: true) that the reducer looks for. I also don't want to have to have the reducer just look for every individual action that falls into this category, since that also requires someone to remember to do this every time they add a new action, like adding the flag.
I was thinking of dispatching a second action every time one of these actions is going to be dispatched. This would not be hard to do, but I'd rather not have 2 actions dispatched every time one thing happens. It seems like it would pollute the state history.
Is there a common way of solving this problem?
For more context to my specific problem, the specific feature is related to the API client my app uses to talk to our API. On every successful response, we'd like to do something in a reducer to update the state, and on every failed response, we'd like to do something else.
There are many different success and failure actions (such as ITEM_FETCH_SUCCESS or WIDGET_UPDATE_FAILURE), and adding a flag to all of them would be hard to remember to do when new ones are added.
Since all api requests go through a single function, that function COULD dispatch generic REQUEST_SUCCESS and REQUEST_FAILURE actions. But this would mean every response from the server would dispatch 2 actions (REQUEST_SUCCESS and ITEM_FETCH_SUCCESS). This is obviously not ideal since it would mean many more actions in my state history.
Assuming the generic REQUEST_SUCCESS and REQUEST_FAILURE actions are updating their own specific portions of the state-tree then it is fine to dispatch them as distinct actions. Doing this does not necessarily imply the pollution of your state history but can simply be a better description of the app's intentions.
ITEM_FETCH_SUCCESS: Change state for item
REQUEST_SUCCESS: Change state for request
WIDGET_UPDATE_FAILURE: Change state for widget
REQUEST_FAILURE: Change state for request
You can see that whilst the actions are intimately related, they are not necessarily the same thing as they change different parts of the state tree.
Accepting this, the question is: How best to implement the action-pairs so that adding new actions does not mean remembering to add its corresponding REQUEST_* partner?
I would consider applying a simple redux middleware component. This could intercept the return from your api and dispatch the appropriate REQUEST_* action automatically.
Here is an example from some live code. This middleware intercepts a disconnect event raised by a websocket and automatically dispatches a custom action as a result. It at least shows the principle:
//Dispatch a disconnect action when the websocket disconnects
//This is the custom action provided by the middleware
import io from 'socket.io-client'
import { actions } from './action'
const websocket = ({ websocketUrl }) => store => {
const socket = io(websocketUrl)
socket.on('disconnect', () => store.dispatch(actions.disconnect()))
}
export default websocket
//Apply the custom middleware via the redux createStore function
//Also include the thunk middleware because it is useful
import { applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import websocket from './middleware'
function websocketize (opts) {
return createStore => (reducers, initial, enhancer) => {
const middleware = applyMiddleware(thunk, websocket(opts))
return createStore(reducers, initial, middleware)
}
}
export default websocketize
// Create the top-level redux store passing in the custom middleware enhancer
const opts = {websocketUrl: env.WEBSOCKET_URL}
const store = createStore(reducers, websocketize(opts))
This implementation keeps everything inside your reducers as opposed to having logic outside in an interception(middleware). Both ways are valid.
Try a sub-reducer pattern. I usually feel gross when I see it used(because it is usually used wrong), but your situation sounds perfect.
Extract duplicate functionality out of your reducers to one single
sub-reducer.
Then pass that reducer as a function to all others that need it.
Then pass the action and state onto the sub-reducer.
The sub-reducer does it's thing and returns that slice of state to
your parent reducer to allow you to do whatever you want with it
there (ie return it, mutate some more, some logic).
Also if you are tired of worrying about typing out "all the stuff" for async then I highly recommend you try out redux-crud.js
It also is possible and a simple way to do that would be to give every action to one reducer and let it do that common mutation, in a single case:
case actionOne
actionTwo
actionThree
actionFour: {
//do common stuff here
}
. But you said it is not duplicated, it is similar, which means your case becomes complicated by branching logic. I also don't recommend this. Keep cases simple so you can easily catch invalid mutations. This should be a super power of redux that it is easy to catch mutation errors. And for this reason and many others I don't recommend normalizing data in the front end.
I am refactoring my app to use redux, and it's great.
One thing I'd like to do is to dispatch an initial action at the beginning, and every reducer would manage to initialize themselves at that moment.
Let's say i have a main.js that create the stores, the routes, etc. In that file, I could do:
store.dispatch({ type: 'app/init' });
If I do this, the action type app/init can be intercepted in each reducer which needs to initialize itself.
An example use case (among others)
When the app is launched, a third party library must be called to see if a user is currently authenticated. If so, a LOGIN_SUCCESS action must be triggered with that user data.
I'd like to see this code in the same file as the authentication reducer, triggered by a global init action (which would mean the store is created).
The problem
In the reducer (where init action is managed), other actions cannot be dispatched.
The advised way of implementing actions is by defining action creators, which is indeed very clean, and let us use middleware like thunks to dispatch other actions.
I could use init() action creators for each reducer (I define related actions and reducer in the same "ducks" file), but that means importing/calling each of them in main.js, which is what I was trying to avoid by dispatching the action directly.
How to get the best of all worlds by having one single app/init action dispatched, and being able to intercept it in each store and dispatch other actions?
Note: I thougth of just implementing those initialization code in each reducer, inline, but I do not have standard access to the dispatcher that way?
The reducer just calculates the next state based on an action. In Redux, it’s not the right place to put side effects like calling an API.
If you want to orchestrate an asynchronous workflow of actions, I suggest you to look at Redux Saga. It lets you define long-running functions (“sagas”) that can “wait” for specific actions, execute some side effects, and dispatch more actions.