I use redux's combineReducers() helper function to combine two reducers like so:
const rootReducer = combineReducers({
user: userReducer,
accounts: accountsReducer
})
I know each reducer can only modify a slice of the store it's assigned to, "user" and "accounts" in this case.
How can i modify the "user" part of my store from my accounts reducer?
You can't.
You can either listen to the same action in both reducers, or if you need to update the user state based on the update to the accounts' state, then you can use Redux thunks - https://github.com/gaearon/redux-thunk
const action = () => (dispatch, getState) => {
// dispatch action that accounts reducer listens to
dispatch({
type: 'SOME_ACTION'
})
// get the new accounts state
let { accounts } = getState()
// dispatch action that user reducer listens to, passing the new accounts state
dispatch({
type: 'ANOTHER_ACTION',
payload: {
accounts
}
})
}
// call with
dispatch(action())
Related
How can I get data from the store using React Redux Toolkit and get a cached version if I already requested it?
I need to request multiple users for example user1, user2, and user3. If I make a request for user1 after it has already been requested then I do not want to fetch user1 from the API again. Instead it should give me the info of the user1 from the store.
How can I do this in React with a Redux Toolkit slice?
Edit: This answer predates the release of RTK Query which has made this task much easier! RTK Query automatically handles caching and much more. Check out the docs for how to set it up.
Keep reading if you are interested in understanding more about some of the concepts at play.
Tools
Redux Toolkit can help with this but we need to combine various "tools" in the toolkit.
createEntityAdapter allows us to store and select entities like a user object in a structured way based on a unique ID.
createAsyncThunk will create the thunk action that fetches data from the API.
createSlice or createReducer creates our reducer.
React vs. Redux
We are going to create a useUser custom React hook to load a user by id.
We will need to use separate hooks in our hooks/components for reading the data (useSelector) and initiating a fetch (useDispatch). Storing the user state will always be the job of Redux. Beyond that, there is some leeway in terms of whether we handle certain logic in React or in Redux.
We could look at the selected value of user in the custom hook and only dispatch the requestUser action if user is undefined. Or we could dispatch requestUser all the time and have the requestUser thunk check to see if it needs to do the fetch using the condition setting of createAsyncThunk.
Basic Approach
Our naïve approach just checks if the user already exists in the state. We don't know if any other requests for this user are already pending.
Let's assume that you have some function which takes an id and fetches the user:
const fetchUser = async (userId) => {
const res = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return res.data;
};
We create a userAdapter helper:
const userAdapter = createEntityAdapter();
// needs to know the location of this slice in the state
export const userSelectors = userAdapter.getSelectors((state) => state.users);
export const { selectById: selectUserById } = userSelectors;
We create a requestUser thunk action creator that only executes the fetch if the user is not already loaded:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const existing = selectUserById(getState(), userId);
return !existing;
}
}
);
We can use createSlice to create the reducer. The userAdapter helps us update the state.
const userSlice = createSlice({
name: "users",
initialState: userAdapter.getInitialState(),
reducers: {
// we don't need this, but you could add other actions here
},
extraReducers: (builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
});
export const userReducer = userSlice.reducer;
But since our reducers property is empty, we could just as well use createReducer:
export const userReducer = createReducer(
userAdapter.getInitialState(),
(builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
)
Our React hook returns the value from the selector, but also triggers a dispatch with a useEffect:
export const useUser = (userId: EntityId): User | undefined => {
// initiate the fetch inside a useEffect
const dispatch = useDispatch();
useEffect(
() => {
dispatch(requestUser(userId));
},
// runs once per hook or if userId changes
[dispatch, userId]
);
// get the value from the selector
return useSelector((state) => selectUserById(state, userId));
};
isLoading
The previous approach ignored the fetch if the user was already loaded, but what about if it is already loading? We could have multiple fetches for the same user occurring simultaneously.
Our state needs to store the fetch status of each user in order to fix this problem. In the docs example we can see that they store a keyed object of statuses alongside the user entities (you could also store the status as part of the entity).
We need to add an empty status dictionary as a property on our initialState:
const initialState = {
...userAdapter.getInitialState(),
status: {}
};
We need to update the status in response to all three requestUser actions. We can get the userId that the thunk was called with by looking at the meta.arg property of the action:
export const userReducer = createReducer(
initialState,
(builder) => {
builder.addCase(requestUser.pending, (state, action) => {
state.status[action.meta.arg] = 'pending';
});
builder.addCase(requestUser.fulfilled, (state, action) => {
state.status[action.meta.arg] = 'fulfilled';
userAdapter.upsertOne(state, action.payload);
});
builder.addCase(requestUser.rejected, (state, action) => {
state.status[action.meta.arg] = 'rejected';
});
}
);
We can select a status from the state by id:
export const selectUserStatusById = (state, userId) => state.users.status[userId];
Our thunk should look at the status when determining if it should fetch from the API. We do not want to load if it is already 'pending' or 'fulfilled'. We will load if it is 'rejected' or undefined:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const status = selectUserStatusById(getState(), userId);
return status !== "fulfilled" && status !== "pending";
}
}
);
In my redux action, I have one action will be called by another two actions, code is below:
export const addParticipantFromPopupRequest = (participant, project_id, currentStep) => async (dispatch) => {
const result = await addParticipant(participant)
dispatch({ type: PARTICIPANT_ADD, payload: result })
dispatch(updateProjectStep(project_id, currentStep))
}
export const handleFinalStep = (projectId, currentStep) => async (dispatch) => {
dispatch(updateProjectStep(projectId, currentStep))
}
const updateProjectStep = (projectId, currentStep) => async (dispatch, getState) => {
dispatch({ type: MODAL_STATUS_CHANGE, payload: { projectId, currentStep } })
dispatch({ type: PROJECT_PROCESS_LIST_UPDATE, payload: { project_id: projectId, currentStep } })
const { projectsProcessListsReducer } = getState()
localStorage.setItem("projectsProcessLists", JSON.stringify(projectsProcessListsReducer))
}
If I dont' use dispatch when call updateProjectStep, the addParticipantFromPopupRequest and handleFinalStep cannot run correct.
My question is can I call dispatch actions in this way and is it correct? Why I need the "dispatch" when I call updateProjectStep in another actions rather than call function name directly?
My question is can I call dispatch actions in this way and is it correct?
Yes. You should always call with the dispatch.
Why I need the "dispatch" when I call updateProjectStep in another actions rather than call function name directly?
If you call updateProjectStep directly without dispatch, it will become a normal js function call and your store won't be aware of it. Dispatch is the only way to trigger a state change in store.
In redux the store is single source of truth, the dispatch you are using is actually comes from store (store.dispatch).
If you call a function normally then it won't be aware by the store. That action won't pass through the middlewares (thunk/saga) that store is aware of and won't do the store update via reducers.
If store is not updated, your components won't receive any updates. Eventually your UI won't re-render.
You can find more about dispatch here.
How can I use redux store inside the mapDispachToProps(), for example to log the redux state in the onLogin function?
const mapDispachToProps = (dispatch) => {
return {
onLogin: (e) => {
console.log();
e.preventDefault();
},
onSetValue: (e) => {
if (e.target.name==="password"){
dispatch({ type: "Set Password", value: e.target.value })
} else if (e.target.name==="username") {
dispatch({ type: "Set User", value: e.target.value })
}
}
};
};
Here I am assuming you have implemented store and other parts of the redux framework.You can use connect API provided by react-redux in the component.You can also read this blog about react redux https://blog.logrocket.com/react-redux-connect-when-and-how-to-use-it-f2a1edab2013/ in case needed.
import { connect } from 'react-redux';
//Your code
export default connect(mapStateToProps => in your case it will be null, mapDispatchToProps)(Component Name);
There is no direct API in mapDispachToProps available to access the store.
If you need the store state to dispatch actions, you typically use some sort of Redux middleware like Redux Thunk. The concrete library isn't important, you even could apply your own middleware to Redux, whose sole purpose is to additionally receive the state in a callback. What matters is that you decouple the concrete store instance from the UI layer in order to abstract complex logic involving state and action creation away and make the system better testable.
Simple example with Redux Thunk
You can return a callback function as return of the dispatch that receives getState besides dispatch as arguments.
const mapDispachToProps = (dispatch) => {
return {
...
onSetValue: e => dispatch(setPasswordOrUser(e));
};
};
const setPasswordOrUser = e => (dispatch, getState) => {
// do something with getState
if (e.target.name==="password"){
dispatch({ type: "Set Password", value: e.target.value })
} else if (e.target.name==="username") {
dispatch({ type: "Set User", value: e.target.value })
}
}
Other approaches
Alternative 1: receive the store via React Context (ReactReduxContext Consumer) directly. React Redux is based on Context API which can pass down the store in the component tree, e.g to mapDispatchToProps via ownProps argument.
Alternative 2: expose store/state as singleton module export like this:
// store.js
import { createStore } from 'redux'
let store = createStore(rootReducer)
export const getState = store.getState
// client.js
import { getState } from "./store"
Big disadvantage here is, that you couple your store instance to React/UI layer, it's easy to mix up architecture layer responsibilities and distribute code dealing with the store in many code locations.
The whole purpose of mapDispatchToProps is to abstract dispatch away from the components. I would really encourage you to use some form of middleware for action dispatching that involves the store state. The other approaches come along with architecture costs.
I am developing a react-native app and new to redux. I've 3 screens and almost all state of all screens dependent on each other.
e.g First screen scans the product, second screen takes customer details and third screen displays the summary from of products and customer.
I am hitting the wall since 2 days to create the state structure and I end up with combineReducers. But in combine reducers, each reducer maintains its own slice of state and doesn't let me access or update the state of another reducer.
So,
Q1. Should I continue with combieReducers or should maintain single reducer only?
Q2. If combineReducers is OK, how should access or update the same state along all reducers?
for u r first question yes u have to combine because combineReducers will give all reducers data in single object
example:
const rootReducer = combineReducers({
posts: PostsReducer,
form: formReducer
});
inside component
function mapStateToProps(state) {
return { posts: state.posts };
}
export default connect(mapStateToProps, { createPost })(PostsIndex);
in the above code through Redux connect you can access state which is produced by combine reducera like this.props.posts inside your component
To update the you need to triger an action which go through all reducers and based on type you can update the state
example:
export function createPost(values, callback) {
const request = axios
.post(`${ROOT_URL}/posts${API_KEY}`, values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
using middleware you can achieve the funtionality
export default ({ dispatch, getState }) => next => action => {
next(action);
const newAction = { ...action, payload: "",state:getState () };
dispatch(newAction);
};
Just discovered that I can use the getState() to pass state values to action creators. However I am noticing that getState() is returning all the combined states rather than the one specified in the argument. What am I doing wrong, as I don't suppose this is the correct behavior?
Reducer:
import { combineReducers } from "redux";
import { reducer as reduxForm } from "redux-form";
import authReducer from "./authReducer";
export default combineReducers({
auth: authReducer,
form: reduxForm
});
Action creator snippet:
export const handleProviderToken = token => async (dispatch, getState) => {
let testState = getState("auth");
console.log(testState);
const res = await axios.get(`${API_URL}/api/testEndpoint`);
dispatch({ type: FETCH_USER, payload: res.data });
};
The console.log(testState) is showing me the entire state tree object (auth and form, rather than just auth).
Quoting redux-thunk documentation
The inner function receives the store methods dispatch and getState as
parameters.
and quoting from redux's documentation
getState() Returns the current state tree of your application. It is
equal to the last value returned by the store's reducer.
So the getState you pass thanks to redux-thunk, is actually redux's getState() function itself and thus it is the default behavior.
To get a specific value from your state tree, you can either use one of the following
const { auth } = getState()
// OR
const testState = getState()
const auth = testState.auth
It is the correct behavior. From docs:
Returns the current state tree of your application. It is equal to the
last value returned by the store's reducer.
It is correct behavior. You will need to pick the reducer key from the complete state.
export const handleProviderToken = token => async (dispatch, getState) => {
let testState = getState();
let authReducer = testState.auth;
const res = await axios.get(`${API_URL}/api/testEndpoint`);
dispatch({ type: FETCH_USER, payload: res.data });
};
What works in similar case for me is
const lastFetchedPage =getState().images.lastFetchedPage;
I do not know how it goes with guidelines, however.