Redux: Make http call ONLY IF data not available in store? - reactjs

I do not want to make an http call unless it is actually required:
This is a workaround I have come up with, I am checking the state before making http call
export const fetchOneApi = (id) => async (dispatch, getState) => {
const docDetails = getState().DocState;
// tricking redux to not send http request unless actually required
if (docDetails.docList[id]) {
return dispatch({
type: FETCH_DOC_SUCCESS,
payload: docDetails.docList[id],
});
}
try {
dispatch({
type: FETCH_DOC_REQUEST,
});
const { data } = await api.get(`/api/${id}`);
dispatch({
type: FETCH_DOC_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: FETCH_DOC_FAIL,
payload: error.error,
});
}
};
Wondering if there is some redux hook or feature that takes care of this OR atleast a better approach.

I've written multiple different custom versions of this functionality for various projects. I toyed with sharing some examples but it's all excessively complicated since I really love to abstract things.
Based on your question, what you are asking for is the createAysncThunk function from redux-toolkit. This function creates an action creator which handles dispatching the pending, fulfilled, and rejected actions at the appropriate times.
There are many ways to customize the behavior of the async thunk. Conditional fetching is described in the docs section "Cancelling Before Execution":
If you need to cancel a thunk before the payload creator is called, you may provide a condition callback as an option after the payload creator. The callback will receive the thunk argument and an object with {getState, extra} as parameters, and use those to decide whether to continue or not. If the execution should be canceled, the condition callback should return a literal false value:
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
{
condition: (userId, { getState, extra }) => {
const { users } = getState()
const fetchStatus = users.requests[userId]
if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') {
// Already fetched or in progress, don't need to re-fetch
return false
}
}
}
)
In your example you are short-circuiting by dispatching a success action with redundant data. The standard behavior for the above code is that no action will be dispatched at all if the fetch condition returns false, which is what we want.
We want to store the pending state in redux in order to prevent duplicate fetches. To do that, your reducer needs to respond to the pending action dispatched by the thunk. You can find out which document was requested by looking at the action.meta.arg property.
// example pending action from fetchUserById(5)
{
type: "users/fetchByIdStatus/pending",
meta: {
arg: 5, // the userId argument
requestId: "IjNY1OXk4APoVdaYIF8_I",
requestStatus: "pending"
}
}
That property exists on all three of the dispatched actions and its value is the argument that you provide when you call your action creator function. In the above example it is the userId which is presumably a string or number, but you can use a keyed object if you need to pass multiple arguments.

Related

Call two different action types from one redux-saga

I've been able to get by with basic sagas implementation for now but my app is getting a little more complex. I chose sagas for the asynchronous capabilities but seem to have misunderstood how things work.
I have a global search input within my application that needs to make two different api calls (different data objects), but the search input also has it's own loading states based on the search/ status of api calls. Based on this information this is the flow of the application:
Search happens (dispatches the action GLOBAL_SEARCH_REQUEST)
The saga watcher for GLOBAL_SEARCH_REQUEST kicks off (sets loading to true for the input)
In that saga - make a call to get all users / subscriptions that match the search query
On success, set loading for the input to false
On failure, set error
the global search request saga
function* globalSearchRequestSaga(action) {
const { query } = action
console.log(`searching subscriptions and users for : ${query}`)
try {
yield put(fetchUsersRequest(query))
// call for the subscriptions (leaving it out for simplicity in this example)
yield put(globalSearchSuccess(query))
} catch (error) {
console.log(`error: ${error}`)
yield put(globalSearchFailure(error.message))
}
}
where the fetch users saga looks like
export function* fetchUsersRequestSaga(action) {
const { query } = action
const path = `${root}/users`
try {
const users = yield axios.get(path, { crossDomain: true })
yield put(fetchUsersSuccess(query, users.data))
} catch (error) {
console.log(`error : ${error}`)
yield put(fetchUsersFailure(query, error.message))
}
}
(very basic)
If I do things this way, there is an issue where the the GLOBAL_SEARCH_SUCCESS action is executed before the completion of the request for users ( and I imagine the same thing if I added in subscriptions api call as well). One solution I found is if I change the line
yield put(fetchUsersRequest(query))
to
yield call(fetchUsersRequestSaga, fetchUsersRequest(query))
where fetchUsersRequestSaga is the saga from above, and fetchUsersRequest(query) is the action creator for fetching users. This causes the asnyc functionality to work, and GLOBAL_SEARCH_SUCCESS waits for the return of the users (correct behavior).
The only issue with this is that the FETCH_USERS_REQUEST action is no longer logged to the store.
I am wondering if there is a way to either get this to properly log to the store, or return to my previous implementation with proper blocking on the put(fetchUsersRequest(query))
The put function is a non-blocking action. It won't wait till the promise/api request resolves.
I would suggest you to just call sagas directly instead of dispatching actions.
try {
yield call(fetchUsersRequestSaga, query);
yield call(globalSearchSaga, query); // or whatever its called
}
call is a blocking action. It will wait until the request finishes, so both if your calls will execute in proper order.
It's been a while since I worked with sagas but here is some code that will give you a general idea how to wait for a dispatched action.
The way it works is that when you fetch and want to wait for it to fail or succeed you give the fetch action an id, then you can pass that to the waitFor function while simultaneously dispatch the action.
If you don't want or need to wait for it then you can just dispatch the action without an id and it'll still work:
const addId = (id => fn => (...args) => ({
...fn(...args),
id: id++,
}))(0);
const withId = ({ id }, action) => ({ action, id });
function* waitFor(id) {
const action = yield take('*');
if (action.id === id) {
return action;
}
return waitFor(id);
}
function* globalSearchRequestSaga(action) {
const { query } = action;
console.log(
`searching subscriptions and users for : ${query}`
);
try {
//add id to action (id is unique)
const action = addId(fetchUsersRequest, query);
//dispatch the action and then wait for resulting action
// with the same id
yield put(action);
const result = yield waitFor(action.id);
// call for the subscriptions (leaving it out for simplicity in this example)
yield put(globalSearchSuccess(query));
} catch (error) {
console.log(`error: ${error}`);
yield put(globalSearchFailure(error.message));
}
}
export function* fetchUsersRequestSaga(action) {
const { query } = action;
const path = `${root}/users`;
try {
const users = yield axios.get(path, {
crossDomain: true,
});
yield put(//add original id to success action
withId(action, fetchUsersSuccess(query, users.data))
);
} catch (error) {
console.log(`error : ${error}`);
yield put(
withId(//add original id to fail action
action,
fetchUsersFailure(query, error.message)
)
);
}
}

What is the best approach of writing redux actions that need data from other actions

I have made some research about possible ways to do it, but I can't find one that uses the same architecture like the one in the app I'm working on. For instance, React docs say that we should have a method which makes the HTTP request and then calls actions in different points (when request starts, when response is received, etc). But we have another approach. We use an action which makes the HTTP call and then dispatches the result. To be more precise, my use case is this:
// action to get resource A
getResourceA () {
return dispatch => {
const result = await axios.get('someLink');
dispatch({
type: GET_RES_A,
payload: result
});
};
}
// another action which needs data from resource A
getSomethingElseByIdFromA (aId) {
return async dispatch => {
const result = await axiosClient.get(`someLink/${aId}`);
dispatch({
type: GET_SOMETHING_BY_ID_FROM_A,
payload: result
});
};
}
As stated, the second action needs data from the first one.
Now, I know of two ways of doing this:
return the result from the first action
getResourceA () {
return async dispatch => {
const result = await axios.get('someLink');
dispatch({
type: GET_RES_A,
payload: result
});
return result;
};
}
// and then, when using it, inside a container
async foo () {
const {
// these two props are mapped to the getResourceA and
// getSomethingElseByIdFromA actions
dispatchGetResourceA,
dispatchGetSomethingElseByIdFromA
} = this.props;
const aRes = await dispatchGetResourceA();
// now aRes contains the resource from the server, but it has not
// passed through the redux store yet. It's raw data
dispatchGetSomethingElseByIdFromA(aRes.id);
}
However, the project I'm working on right now wants the data to go through the store first - in case it must be modified - and only after that, it can be used. This brought me to the 2nd way of doing things:
make an "aggregate" service and use the getState method to access the state after the action is completed.
aggregateAction () {
return await (dispatch, getState) => {
await dispatch(getResourceA());
const { aRes } = getState();
dispatch(getSomethingElseByIdFromA(aRes.id));
};
}
And afterward simply call this action in the container.
I am wondering if the second way is all right. I feel it's not nice to have things in the redux store just for the sake of accessing them throughout actions. If that's the case, what would be a better approach for this problem?
I think having/using an Epic from redux-observable would be the best fit for your use case. It would let the actions go throughout your reducers first (unlike the mentioned above approach) before handling them in the SAME logic. Also using a stream of actions will let you manipulate the data throughout its flow and you will not have to store things unnecessary. Reactive programming and the observable pattern itself has some great advantages when it comes to async operations, a better option then redux-thunk, sagas etc imo.
I would take a look at using custom midleware (https://redux.js.org/advanced/middleware). Using middleware can make this kind of thing easier to achieve.
Something like :
import {GET_RESOURCE_A, GET_RESOURCE_B, GET_RESOURCE_A_SUCCESS, GET_RESOURCE_A_ERROR } from '../actions/actionTypes'
const actionTypes = [GET_RESOURCE_A, GET_RESOURCE_B, GET_RESOURCE_A_SUCCESS, GET_RESOURCE_A_ERROR ]
export default ({dispatch, getState}) => {
return next => action => {
if (!action.type || !actionTypes.includes(action.type)) {
return next(action)
}
if(action.type === GET_RESOURCE_A){
try{
// here you can getState() to look at current state object
// dispatch multiple actions like GET_RESOURCE_B and/ or
// GET_RESOURCE_A_SUCCESS
// make other api calls etc....
// you don't have to keep stuff in global state you don't
//want to you could have a varaiable here to do it
}
catch (e){
} dispatch({type:GET_RESOURCE_A_ERROR , payload: 'error'})
}
}
}

Dispatch method in middleware not triggering reducer

Using the store method dispatch from the parameter provided by Redux Thunk middleware does not trigger the reducer. While using next() works properly as it triggers the reducer. Why is this happening?
middlerware
export default function createSlimAsyncMiddleware({
dispatch,
getState
}) {
return next => action => {
const {
types,
callAPI,
shouldCallAPI = () => true,
} = action;
if (!actionIsValid(action)) next(action);
if (shouldCallAPI(getState())) {
return Promise.resolve(getState());
}
const [pendingType, successType, errorType] = types;
dispatch({
type: pendingType
});
return callAPI()
.then(response => {
dispatch({ // Does not work, use next()
type: successType,
payload: response,
});
console.log('call resolved with type', successType)
return Promise.resolve(getState());
})
.catch(error => {
dispatch({ // Does not work, use next()
type: errorType,
payload: error,
});
return Promise.reject(error);
});
};
}
store
const store = createStore(
appReducer,
composeWithDevTools(applyMiddleware(
thunk,
createSlimAsyncMiddleware,
routerMiddleware(history)
))
)
Regarding this response https://stackoverflow.com/a/36160623/4428183 the dispatch should also work.
This is stated in the linked response you included, but calling dispatch() will create a new action, which then goes through the entire middleware chain from the beginning. In your case, this includes the middleware you're troubleshooting. From what I can see, the only time you call next() is in the case that an incoming action is deemed invalid. Otherwise, the subsequent API call results in dispatch() being called again whether the call succeeds or fails, and so the action never gets to the reducer because it's constantly being set at the beginning of your middleware chain and never gets to move along via next().
When you say this code doesn't work, what is the specific behavior? Does your app hang? Does it crash? Because this scenario essentially sets up a recursive function with no base case, I'd bet that you're seeing 'maximum call stack exceeded' sorts of errors.
I guess I'd ask why you need to use dispatch() for request results as opposed to sending them along using next(), or why you haven't set this up in a way that sets a conditional that uses the result of the previous call to determine whether the API gets called again.

Redux axios request cancellation

I have a React Native application with Redux actions and reducers. I'm using the redux-thunk dispatch for waiting the asyncron calls. There is an action in my application:
export const getObjects = (id, page) => {
return (dispatch) => {
axios.get(`URL`)
.then(response => {
dispatch({ type: OBJECTS, payload: response });
}).catch(error => {
throw new Error(`Error: objects -> ${error}`);
});
};
};
That's working properly, but sometimes the user click on the back button before the action finished the request, and I must cancel it. How can I do it in a separated action? I read this, but I didn't find any option in axios for abort. I read about the axios cancellation, but it's create a cancel method on the function scope and I can't return, because the the JS don't support multiple returns.
What is the best way to cancel axios request in an other Redux action?
I would recommend using something like RxJS + Redux Observables which provides you with cancellable observables.
This solution requires a little bit of learning, but I believe it's a much more elegant way to handle asynchronous action dispatching versus redux-thunk which is only a partial solution to the problem.
I suggest watching Jay Phelps introduction video which may help you understand better the solution I'm about to propose.
A redux-observable epic enables you to dispatch actions to your store while using RxJS Observable functionalities. As you can see below the .takeUntil() operator lets you piggyback onto the ajax observable and stop it if elsewhere in your application the action MY_STOPPING_ACTION is dispatched which could be for instance a route change action that was dispatched by react-router-redux for example:
import { Observable } from 'rxjs';
const GET_OBJECTS = 'GET_OBJECTS';
const GET_OBJECTS_SUCCESS = 'GET_OBJECTS_SUCCESS';
const GET_OBJECTS_ERROR = 'GET_OBJECTS_ERROR';
const MY_STOPPING_ACTION = 'MY_STOPPING_ACTION';
function getObjects(id) {
return {
type: GET_OBJECTS,
id,
};
}
function getObjectsSuccess(data) {
return {
type: GET_OBJECTS_SUCCESS,
data,
};
}
function getObjectsError(error) {
return {
type: GET_OBJECTS_ERROR,
data,
};
}
const getObjectsEpic = (action$, store) = action$
.ofType(GET_OBJECTS)
.switchMap(action => Observable.ajax({
url: `http://example.com?id=${action.id}`,
})
.map(response => getObjectsSuccess(response))
.catch(error => getObjectsError(error))
.takeUntil(MY_STOPPING_ACTION)
);

how to write async redux actions in a non-fake app

tl;dr: I need an example of an asynchronous redux-thunk action shows how to make an async call (e.g. fetch), and trigger a state update. I also need to see how someone might chain multiple such actions together, like: (1) see if user exists in cloud, then (2) if no, register them, then (3) use the new user record to fetch more data.
All the examples I've found makes the assumption that the redux store can be imported directly into the module that defines the actions. It's my understanding that this is a bad practice: the calling component is responsible for providing access to the store, via this.props.dispatch (which comes from the store being injected via the <Provider>).
Instead, every action in the redux world should return a function that will receive the appropriate dispatch; that function should do the work, and return... something. Obv, it matters what the something is.
Here's the pattern I've tried, based on the documentation, that has proven to be a failure. Nothing in the docs makes it clear why this doesn't work, but it doesn't -- because this action doesn't return a promise.
/**
* pushes a new user into the cloud; once complete, updates the store with the new user row
* #param {hash} user - of .firstName, .lastName
* #return {promise} resolves with user { userId, firstName, lastName, dateCreated }, or rejects with error
*/
Actions.registerUser = function(user) {
return function reduxAction(dispatch) {
return API.createUser(user) // API.createUser just does return fetch(...)
.then(function onUserRegistered(newUser) {
return dispatch({
type: 'ADD_USERS',
users: [newUser]
});
});
};
};
I have a reducer that responds to the ADD_USERS event; it merges the incoming array of one or more users with the array of users already in memory. Reducers are easy to write. That's why I switched to redux: one store, pure functions. But this thunk business is an absolute nightmare.
The error I receive is that .then is undefined on Actions.registerUser -- i.e. that Actions.registerUser doesn't return a promise.
I think the problem is obviously that I'm returning a function -- the reduxAction function -- but that doesn't seem to be negotiable. The only way to shoot data at the store is to use the dispatch method that is provided, and that means I can't return a promise.
Changing the onUserRegistered to simply invoke dispatch and then return the desired value doesn't work either, nor does having it return an actual promise.
PLZ HALP. I really don't get it. I can't believe people put up with all this.
EDIT: To provide some context, here's the kind of action composition I think I'm supposed to be able to perform, and which these thunk actions are frustrating:
Actions.bootSetup = function() {
return dispatch => {
return Actions.loadUserId() // looks for userId in local storage, or generates a new value
.then(Actions.storeUserId) // pushes userId into local storage
.then((userId) => {
return Actions.fetchUsers(userId) // fetches the user, by id, from the cloud
.then((user) => {
// if necessary, pushes the user into the cloud, too
return user || Actions.postUser({ userId: userId, firstName: 'auto-registered', lastName: 'tbd'});
});
})
.then((user) => {
console.log(`boot sequence complete with user `, user);
return dispatch({ type: 'ADD_OWNER', user });
});
};
};
I would expect that Actions.storeUserId and Actions.fetchUsers would, in addition to returning promises that resolve with values of my choosing, dispatch data to the store as a side-effect. I think the dispatch is occurring, but the chain breaks because none of these actions return promises - they return plain functions.
Not only does this seem much worse than Flux, it seems incomprehensible. I can't believe that all this madness was necessary just to consolidate app state into a single reducing store.
And yes -- I have tried the new version of flux, with its ReducerStore, but it has some inappropriate dependencies on CSS libraries that are incompatible with react-native. The project maintainers have said they don't intend to resolve the issue. I guess their state container is dependent on CSS functionality.
EDIT: my store
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import Reducers from './reducers';
const createStoreWithMiddleWare = applyMiddleware(thunk)(createStore);
export const initialState = {
users: [] // will hold array of user objects
};
const store = createStoreWithMiddleWare(Reducers);
export default store;
EDIT: Here's the calling code. This is the root-level react-native component.
// index.ios.js
import Store from './store';
class myApp extends Component {
componentDidMount() {
Store.dispatch(Actions.bootSetup())
.then(() => {
console.log('*** boot complete ***');
});
}
render() {
return (
<Provider store={Store}>
<ApplicationRoutes />
</Provider>
);
}
}
My assumption is that Store.dispatch expects a function, and provides it with a reference to the store's dispatch method.
I can see one mistake right off the bat
Actions.bootSetup = function() {
return dispatch => {
return Actions.loadUserId()
You aren't chaining thunk actions correctly. If your actions returns a function, you need to pass dispatch to that action.
Take a look at this action creator(this is a fully functional real-world app, feel free to poke around), look at the 9th line, where loginUser is called.
export function changePassword(credentials) {
return (dispatch, getState) => {
dispatch(changePasswordStart(credentials))
return Firebase.changePassword(credentials)
.then(() => {
return logout()
})
.then(() => {
return loginUser(credentials.email, credentials.newPassword)(dispatch)
})
.then(() => {
dispatch(changePasswordSuccess(credentials))
toast.success('Password successfully changed')
}).catch(error => {
dispatch(changePasswordError(error.code))
toast.error('An error occured changing your password: ' + error.code)
})
}
}
Because loginUser is also a thunk action, it needs to have dispatch passed to the result of calling it. It makes sense if you think about it: the thunk doesn't do anything, it just creates a function. You need to call the function it returns to get it to do the action. Since the function it returns takes dispatch as an argument, you need to pass that in as well.
Once that's done, returning a promise from a thunk action will work. In fact, the example I gave above does exactly that. loginUser returns a promise, as does changePassword. Both are thenables.
Your code probably needs to look like this (though I am not sure, I don't have the actions being called)
Actions.bootSetup = function() {
return dispatch => {
return Actions.loadUserId()(dispatch) // pass dispatch to the thunk
.then(() => Actions.storeUserId(dispatch)) // pass dispatch to the thunk
.then((userId) => {
return Actions.fetchUsers(userId)(dispatch) // pass dispatch to the thunk
.then((user) => {
// pass dispatch to the thunk
return user || Actions.postUser({ userId: userId, firstName: 'auto-registered', lastName: 'tbd'})(dispatch);
});
})
.then((user) => {
console.log(`boot sequence complete with user `, user);
return dispatch({ type: 'ADD_OWNER', user });
});
};
};

Resources