Redux change 'state' used in other reducers when using combined reducers - reactjs

I'm using redux and redux-saga. I have several reducers such as:
userReducer, itemReducer, etc. Each one serves a specific purpose for encapsulated related data.
The situation I have is that for every API request being made a token is returned. This token needs to update in the state foruserReducer.
Here's an example:
// Action
export function getItems () {
return {
type: 'GET_ITEMS'
}
}
//Saga
const getItems = function * getItems (action) {
const response = yield call(api.getItems)
yield put({ type: 'GET_ITEMS_SUCCESS' })
}
//itemReducer
switch(action.type) {
...
case 'GET_ITEMS_SUCCESS':
return {
...state,
action.data
}
...
}
This is just a basic example, now in this scenario, I'm getting items and updating the state when successful, pretty straight forward. What I also want to do is update the userReducer state with a token that would be available in action.token.
The obvious way is there would be duplicate cases, which really isn't reasonable.
The second option is in my userReducer, I can do a check before the switch for if the action contains a token and if it does, update it's state (seems ok, I guess).
The last option I can think, is to somehow modify my saga so that I can issue a new action 'UPDATE_TOKEN' or something similar, when each saga is run.
Would anyone have any suggestions on best practice on how to handle this?
EDIT:
Ok, so the best way I can see to do this now is by adding another yield in saga, such as:
yield put({ type: 'UPDATE_TOKEN', token: response.token }) and just use UPDATE_TOKEN in my userReducer. This seems pretty straight forward, just need to add it wherever I want to actually perform this action.

Related

Why is dispatch returning a promise with no result? (redux middleware)

I came into a React project that uses vanilla Redux with middleware. The way it is setup is as follows:
composeEnhancers(applyMiddleware(...middleware.map(f => f(services))))
Now middlware is an array of, well, middleware containing functions. services is an object containing external services that are injected into the middlware functions (api and so on).
The interesting part is the middleware, here is a sample of it:
...
const throwErrorFlow = ({ api }) => ({ dispatch, getState }) => next => async (action) => {
next(action)
if (action.type === actions.THROW_ERROR) {
try {
dispatch(actions.setLoadingSlot({ state: false, context: action.payload.context }))
const context = getState().ui.context
const payload = { location: action.payload.location, error: action.payload.error?.stack, context }
console.log(payload);
await api.context.throwError(payload)
dispatch(actions.setErrorModalVisibility({ payload, visibility: true }))
} catch (error) {
console.log(error);
}
}
}
const middleware = [
middlwareFunction1,
middlwareFunction2,
...
throwErrorFlow
]
export default middleware
Then I created my own test action that returns a test string. I added a similar middlware function as the rest. When dispatching this test action from the UI and logging its result, all I get is: PromiseĀ {<fulfilled>: undefined}
So I tried zooming in a bit. My action is just the following:
export const customAction = payload => ({
type: CUSTOM_ACTION,
payload: payload,
})
And my bit in the middleware is the following:
const customAsyncActionFlow = () => storeAPI => () => action => {
if (action.type === actions.CUSTOM_ACTION) {
console.log(action);
return 'TEST!'
}
}
const middleware = [
middlwareFunction1,
middlwareFunction2,
...
throwErrorFlow,
customActionFlow
]
export default middleware
And I call it from the UI as:
console.log(dispatch(customAction('Hello World!')));
My action is logged correctly to the console, but then I get PromiseĀ {<fulfilled>: undefined} instead of 'TEST!'. So I removed all other middleware functions and only kept my customActionFlow, and everything worked as I expected. Where is this Promise with no result coming from? Yes all other middleware functions do not return anything, they just modify the state. Does this have to do with this fact? And how do I 'fix' this?
EDIT: okay so I seem to understand what is going on. For each action that requires interaction with the api, a middleware is written for this action which gets applied. In the end there are 20 middleware functions all culminating with the async action for each one. The action that I defined with the test middleware that returns a value gets "lost" in the mix I guess? I am still not sure as to why my return has no effect whatsoever.
Is there a way to make my dispatch action call the my test middleware exclusively while keeping all other middlewares applied?
Oh dear. While this isn't a direct answer to your question...
I've seen that style of "write all Redux logic as custom middleware" tried a few times... and it is a bad idea!
It makes things highly over-complicated, and adding all these extra middleware for individual chunks of functionality adds a lot of overhead because they all have to run checks for every dispatched action.
As a Redux maintainer I would strongly recommend finding better approaches for organizing and defining the app logic. See the Redux Style Guide for our general suggestions:
https://redux.js.org/style-guide/
Now, as for the actual question:
When you call store.dispatch(someAction), the default behavior is that it returns the action object.
When you write a middleware, that can override the return value of store.dispatch(). A common example of this is the redux-thunk middleware, which just does return thunkFunction(dispatch, getState). This is commonly used to let thunks return promises so that the UI knows when some async logic is complete:
https://redux.js.org/tutorials/essentials/part-5-async-logic#checking-thunk-results-in-components
https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns#thunks-and-promises
In this case, the middleware is itself defined as an async function, and every async function in JS automatically returns a promise. So, just having one async middleware in the chain is going to end up returning a promise from store.dispatch(anything). (This would be another reason to not write a bunch of logic directly in a custom middleware like that.)

Redux Toolkit generate and return an id (return data from action)

I'm trying to create an object and add it to a reducer, but have the action/reducer take care of generating the id.
Per this answer, it seems the accepted pattern is to generate the id in the action creator:
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
state.push(action.payload);
},
prepare(text) {
const id = uuid();
return { payload: {text, id} };
}
}
}
})
However, suppose I want to then use / refer to the id after creating the todo, something like
dispatch(addTodo('Say hello world')) // creates a todo with a uuid
...
id = <some way to get the id>
doSomethingElseWithTodoGivenId()
Does Redux-Toolkit provide any assistance with achieving this? I looked at createAsyncThunk, but that appears to be more focussed around async data fetching status.
I know I can do this with redux-thunk (by awaiting the dispatch and having the thunk action generate the id):
const id = await dispatch(createTodoWithGeneratedId('Say hello world'))
or by having the caller generate the id. But I'm wondering if there's a better way.
Unfourtonately, dispatch is not working that way. Plain Redux dispatch call retuns a dispatched action, but redux-toolkit using thunk middleware by default that changes a dispatch returning value to promise that fullfiled with dispatched action. You can't get a result of dispatching result from dispatch call itself, it is impossible and conflicts with it's nature.
However, you can create a thunk that dispatches different actions based on promise result. Check official documentation about creating thunk with side effects. For more advanced usage check this answer in SO.
If you are using redux-toolkit, you can use createAsyncThunk, it is implements the same logic but without much more boilerplate code.
There is no accepted pattern in this case. Sometimes you have to figure out things based on your needs. That is why leetcode algorithms are such a big deal in hiring process.
If you follow your above implementation
import { nanoid } from '#reduxjs/toolkit';
prepare(text) {
const id = nanoid();
return { payload: {text, id} };
}
the only way to get the "id" is you have to get the state with the useSelector and then loop through where text===currentToDo. This is too much work for a simple task.
Instead, why not pass id with the dispatching? You arrange your payload where accepts an object. (Because you cannot pass multiple args)
const id=nanoid()
dispatch(addTodo({todo:'Say hello world',id:id}))
// now use that id todo something
in this scenario, you do not even need the prepare phase. (you did not before neither). just push the payload object to the array

How to chain redux actions using returned result of the previous action?

I'm building an app in React Native, and using Redux with redux-persist to act as on device database.
The crux of the issue is, how do I return the result of a redux action, to then dispatch another action with this data? Read on for more specifics.
The user can create custom habit types. When doing so, I dispatch an action to create a habit type in the store (e.g. "running"). This action generates a new unique UUID for the habit type. I then want to add this newly created habit type to a routine (e.g. "morning routine"), and so I need to receive back the UUID of the habit type and call another dispatch to add it to the routine.
I'm using immer to make manipulating the state in my reducers simpler, and have this code (simplified example):
import produce from "immer";
const userReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_CUSTOM_HABIT_TYPE: {
return produce(state, draftState => {
const newHabitType = {
id: generateUuid(),
name,
};
draftState.customHabitTypes.push(newHabitType);
return draftState;
});
}
}
};
I'm then dispatching it in my component, like so (simplified):
dispatch({
type: ADD_CUSTOM_HABIT_TYPE,
name: "running",
});
How can I then say, after creating this new habit type, to dispatch another action and add it to my routine?
I've looked at redux-thunk and redux-saga, and spent hours reading about these and trying to get redux-thunk to work, but to no avail. I'm sure this must be simple, but I'm coming up blank, and so maybe others are too, hence this post.
A very simple solution would be to generate the unique id before dispatching the action.
Example
const newHabitType = {
id: generateUuid(),
name,
};
dispatch({
type: ADD_CUSTOM_HABIT_TYPE,
habit: newHabitType,
});
dispatch({
type: ADD_CUSTOM_HABIT_TO_ROUTINE,
habit: newHabitType.id,
});
Pros
You no longer need to chain actions per se, you just need to dispatch them in order.
This preserves one of the most important Redux guidelines: your reducer should not have any side effects (in your case, generating a random id). reference
Cons
If you create the new habits in multiple places, you will have to generate the unique ids in every place where you dispatch the action. This might lead to repeated code. The solution to this would be to encapsulate the whole logic for creating the habits to a single component and then reuse this component everywhere.
Actions do not return data per se, the are simply objects which mutate the store based on the rules defined in the reducer. Two possible solutions:
Option A, create a composite action.
const compositeAction = args => {
return dispatch => {
return someAsyncCall(args).then(response => {
dispatch(addCustomHabitat(response))
dispatch(followUpAction())
}
}
}
const addCustomHabitat = response => {
return {
type: "ADD_CUSTOM_HABIT_TYPE",
data: response
}
}
const followUpAction = () => {
...another action...
}
Option B, connect the results of the first action to the dispatching component through react-redux and pass them to the second action.
import {connect} from 'react-redux';
const MyReactComponent = props => {
dispatch(addCustomHabitatTypeAction());
if(props.customHabitatType !== undefined)
dispatch(followUpAction(props.customHabitatType());
return (
...JSX here...
);
}
const mapStateToProps = state => {
return {
customHabitatType: state.userReducer.customHabitatType
}
}
connect(mapStateToProps)(MyReactComponent);
I hope this helps! Please excuse my abbreviated code and let me know if you have any questions.

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'})
}
}
}

Redux actions depending on/coupled to other actions

I am building a Redux application (my first) and am a little unclear about how much coupling is appropriate between actions.
My application has several forms whose values are serialized in the url.
For example, there is an input field for a particular search, and upon key-up a url parameter is updated. There are several other input fields that follow this pattern.
In my top-level index.js I have several blocks of code that look like this:
// Within the declaration of a high-level component
onForm1Change={(key, value) => {
// Listened to by "formValues" state cross-section reducer
store.dispatch({
type: actions.FORM1_CHANGE,
key: key,
value: value
});
// Listened to by "url" state cross-section reducer, leads to a url param update.
// Has it's own logic that is based upon the formValues state.
// Must run after FORM1_CHANGE finishes
store.dispatch({
type: actions.UPDATE_URL,
formValues: store.getState().formValues
});
}
}
Something about actions like UPDATE_URL doesn't feel right. This action doesn't stand on its own...it relies on other actions to be dispatched first.
Is this sort of coupling between actions a code smell? Are there any common techniques to de-couple/refactor these actions?
I think that's totally OK way of chaining synchronous actions. You can also use middleware like redux-thunk for this purpose to make it simpler to read (as you will store your actions dispatcher function as an action creater). Here is some article on this topic.
This is how i did it,
Defined a redux store middleware that will detect if any dispatched action has a queryString property, and update url with it.
import createHistory from "history/createBrowserHistory";
function queryStringMiddleware(history) {
return store => next => action => {
const { payload } = action;
if (payload.queryString) {
history.push({
search: queryString
});
}
next(action);
};
}
const history = createHistory();
const middlewares = [queryStringMiddleware(history)];
const store = configureStore({}, middlewares);
Then in your action:
const onForm1Change = (key, value) => {
store.dispatch({
type: actions.FORM1_CHANGE,
key: key,
value: value,
queryString: "?the=query"
});
};

Resources