Is it necessary to save everything in redux state? - reactjs

I'm using redux-toolkit. Everything it's ok, but now I'm facing an architecture problem.
I have an endpoint that I need to call in order to get some data so I can do a final call.
This final call response will be the one that I'll use in order to create and do some logic in order to dispatch and save in the store, so:
1- Should I do both calls in the same createAsyncThunk call and just return what I need?
2- this asyncthunk call will just handle the data, i dont really need it to save anything, based on those 2 calls, it will dispatch others actions. Where I should place this?

I have async thunks that dispatch other async thunks, no problem.
1- Yes, although it might be harder to track which api failed
2- It's easier if this thunk lives with the others in my opinion
Example:
const BASE = 'user';
// some other thunk in another file, ignore implementation
const saveInLocalStorageAC = createAsyncThunk()
const fetchUserAC = createAsyncThunk(`${BASE}/fetch`, async (email, { dispatch }) => {
const id = await getIdFromEmail(email); // service
await dispatch(saveInLocalStorageAC({ loggedInUser: { id, email } })); // thunk
return id;
})
const slice = createSlice({
name: BASE,
initialState: someInitialState,
reducers: {},
extraReducers: builder => {
builder.addCase(fetchUserAC.fulfilled, (state, action) => {
// here is where I decide if I want to update the state or not
state.data = action.payload // id
})
},
})
export default slice.reducer;
If you check the actions with some sort of logger you would see something like:
user/fetch/pending
storage/save/pending
storage/save/fulfilled
user/fetch/fulfilled

Related

Pros & cons of using extra reducers vs dispatching within async thunk

I'm rather new to the whole React & Redux ecosystem & am trying to understand when & why to use extra reducers vs directly dispatching actions within an async thunk when working with the Redux toolkit.
Probably best explained with an example showing both solutions:
Version 1: Using extra reducers
auth.slice.ts
// ...
export const login = createAsyncThunk<LoginResponse, LoginData>(
'auth/login',
async ({ email, password }, thunkAPI) => {
const data = await AuthService.login(email, password);
// Extract user info from login response which holds other information as well
// in which we're not interested in the auth slice...
const userInfo = loginResponseToUserInfo(data);
LocalStorageService.storeUserInfo(userInfo);
// Return the whole login response as we're interested in the other data
// besides the user info in other slices which handle `login.fulfilled` in
// their own `extraReducers`
return data;
}
);
// ...
const authSlice = createSlice({
// ...
extraReducers: builder => {
builder.addCase(login.fulfilled, (state, { payload }) => {
// Again: Extract user info from login response which holds other
// information as well in which we're not interested in the auth slice...
const userInfo = loginResponseToUserInfo(payload);
return { ...state, userInfo };
}))
// ...
},
});
// ...
Version 2: Using dispatch inside async thunk
auth.slice.ts
// ...
export const login = createAsyncThunk<LoginResponse, LoginData>(
'auth/login',
async ({ email, password }, thunkAPI) => {
const data = await AuthService.login(email, password);
// Extract user info from login response which holds other information as well
// in which we're not interested in the auth slice...
const userInfo = loginResponseToUserInfo(data);
LocalStorageService.storeUserInfo(userInfo);
// !!! Difference to version 1 !!!
// Directly dispatch the action instead of using `extraReducer` to further
// process the extracted user info
thunkAPI.dispatch(authSlice.actions.setUserInfo(userInfo));
// Return the whole login response as we're interested in the other data
// besides the user info in other slices which handle `login.fulfilled` in
// their own `extraReducers`
return data;
}
);
// ...
const authSlice = createSlice({
// ...
reducers: {
setUserInfo: (state, { payload }: PayloadAction<UserInfo>) => ({
...state,
userInfo: payload,
}),
// ...
},
});
// ...
Question
If I'm not completely wrong, both examples do the exact same thing but looking through the internet I mostly find people suggesting option 1 using the extraReducer which is why I'm asking:
Are both versions basically ok/correct or am I missing something?
Are there any benefits of sticking to the "extraReducers approach"?
One minor drawback in this specific example is that I have to perform the conversion with loginResponseToUserInfo in 2 places (the async thunk & the extraReducer) whilst I only need to call it once in the 2nd version...
In my opinion both are valid although I would go for #1 personally.
To justify my choice :
you should consider 'actions' as 'events', and the event is 'user logon successfull'. Having explicit action to set data into a slice is kinda wrong pattern
consider each of your slice as sub modules that should be able to work independently. The module in charge of authentication should not care if other slices are listening to its event or not ; in the future you might have other slices intereseted in this event and you dont want to end up with several extraneous dispatch in your thunk + the 'logon success' event might be triggered from another source.

How to call another autogenerated action within an autogenerated action of redux-toolkit

I'm developing a small game. And so i used the redux-toolkit for the first time. (i'm used to redux)
this createSlice method is awesome but i cannot figure out how to call actions inside actions.
so the actions are autogenerated and therefore not available before generation.
example:
const gameSlice = createSlice({
name: 'game',
initialState,
reducers: {
startNewGame: (state) => {
state.activeTeam = state.config.startTeam = random(0, 1)
state.winner = undefined
},
endRound: (state, action, dispatch) => { // last param not available
// ... code that handles the rounds inputs and counts points for each team ...
if(state.red >= 30) {
// HOW?! Here's something i tried
endGame(state, Team.Red)
dispatch({type: endGame, payload: Team.Red})
dispatch({type: gameSlice.endGame, payload: Team.Red})
}
if(state.blue >= 30) {
// HOW?! Here's something i tried
endGame(state, Team.Blue)
dispatch({type: endGame, payload: Team.Blue})
dispatch({type: gameSlice.endGame, payload: Team.Blue})
}
},
endGame: (state, action) => {
state.winner = action.payload
}
}
})
This is just a small example. There is nothing async and no backend-call. I just want to nest action-calls. Simple as hell is suppose... but i have no idea how to do it in a KISS-way.
I read something with extraReducers and createAsyncThunk but i won't do this for every action as i have no async stuff in here and because then there is no benefit in using redux-toolkit anymore. It's just a hell of more code for nothing.
I think i'm blind or stupid or just confused... but this drives me crazy, right now.
Without redux-toolkit this was easy.
Any help is appreciated.
You can never dispatch more actions inside of a Redux reducer, ever.
However, you can write additional helper functions to do common update tasks in reducers, and call those functions as needed. This can be especially helpful when using Immer-powered reducers in RTK's createSlice.
Additionally, you can call other case reducers as needed, by referring to the slice.caseReducers object like:
reducers: {
endRound(state, action) {
if (someCondition) {
// "Mutate" state using the `endGame` case reducer
mySlice.caseReducers.endGame(state)
}
},
endGame(state, action) {}
}

How can I cache data that I already requested and access it from the store using React and Redux Toolkit

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";
}
}
);

Handling Auth State using Redux

I have a chat-app that uses React, Redux and Firebase. I'm also using thunkmiddleware to do the async updates of the state with Firebase.
I successfully get everything I need, except that everything depends of a previously hard-coded variable.
The question is, how can I call inside my ActionCreators the getState() method in order to retrieve a piece of state value that I need in order to fill the rest of my states?
I currently have my auth: { uid = 'XXXZZZYYYY' }... I just need to call that like
getState().auth.uid
however that doesn't work at all.
I tried a lot of different questions, using mapDispatchToProps, etc. I can show my repo if needed.
Worth to mention that I tried following this other question without success.
Accessing Redux state in an action creator?
This is my relevant current code:
const store = createStore(
rootReducer,
defaultState,
applyMiddleware(thunkMiddleware));
And
function mapDispatchToProps(dispatch) {
watchFirebase(dispatch); // to dispatch async Firebase calls
return bindActionCreators(actionCreator, dispatch);
}
const App = connect(mapStateToProps, mapDispatchToProps)(AppWrapper);
Which I am exporting correctly as many other not pure functions work correctly.
For instance, this works correctly:
export function fillLoggedUser() {
return (dispatch, getState) => {
dispatch({
type: C.LOGGED_IN,
});
}
}
However as suggested below, this doesn't do a thing:
const logState = () => ( dispatch, getState ) => {
console.log(getState());
};
In general your thunked action creator should look something like the below (I have used a post id as an example parameter):
const getPost = ( postId ) => ( dispatch, getState ) => {
const state = getState();
const authToken = state.reducerName.authToken;
Api.getPost(postId, authToken)
.then(result => {
// where postRetrieved returns an action
dispatch(postRetrieved(result));
});
};
If this is similar to what you have then I would log your state out and see what is going on with a simple thunk.
const logState = () => ( dispatch, getState ) => {
console.log(getState());
};

Redux: Opinions/examples of how to do backend persistence?

I am wondering how folks using Redux are approaching their backend persistence. Particularly, are you storing the "actions" in a database or are you only storing the last known state of the application?
If you are storing the actions, are you simply requesting them from the server, then replaying all of them when a given page loads? Couldn't this lead to some performance issues with a large scale app where there are lots of actions?
If you are storing just the "current state", how are you actually persisting this state at any given time as actions happen on a client?
Does anyone have some code examples of how they are connecting the redux reducers to backend storage apis?
I know this is a very "it depends on your app" type question, but I'm just pondering some ideas here and trying to get a feel for how this sort of "stateless" architecture could work in a full-stack sense.
Thanks everyone.
Definitely persist the state of your reducers!
If you persisted a sequence of actions instead, you wouldn't ever be able to modify your actions in your frontend without fiddling around inside your prod database.
Example: persist one reducer's state to a server
We'll start with three extra action types:
// actions: 'SAVE', 'SAVE_SUCCESS', 'SAVE_ERROR'
I use redux-thunk to do async server calls: it means that one action creator function can dispatch extra actions and inspect the current state.
The save action creator dispatches one action immediately (so that you can show a spinner, or disable a 'save' button in your UI). It then dispatches SAVE_SUCCESS or a SAVE_ERROR actions once the POST request has finished.
var actionCreators = {
save: () => {
return (dispatch, getState) => {
var currentState = getState();
var interestingBits = extractInterestingBitsFromState(currentState);
dispatch({type: 'SAVE'});
window.fetch(someUrl, {
method: 'POST',
body: JSON.stringify(interestingBits)
})
.then(checkStatus) // from https://github.com/github/fetch#handling-http-error-statuses
.then((response) => response.json())
.then((json) => dispatch actionCreators.saveSuccess(json.someResponseValue))
.catch((error) =>
console.error(error)
dispatch actionCreators.saveError(error)
);
}
},
saveSuccess: (someResponseValue) => return {type: 'SAVE_SUCCESS', someResponseValue},
saveError: (error) => return {type: 'SAVE_ERROR', error},
// other real actions here
};
(N.B. $.ajax would totally work in place of the window.fetch stuff, I just prefer not to load the whole of jQuery for one function!)
The reducer just keeps track of any outstanding server request.
function reducer(state, action) {
switch (action.type) {
case 'SAVE':
return Object.assign {}, state, {savePending: true, saveSucceeded: null, saveError: null}
break;
case 'SAVE_SUCCESS':
return Object.assign {}, state, {savePending: false, saveSucceeded: true, saveError: false}
break;
case 'SAVE_ERROR':
return Object.assign {}, state, {savePending: false, saveSucceeded: false, saveError: true}
break;
// real actions handled here
}
}
You'll probably want to do something with the someResponseValue that came back from the server - maybe it's an id of a newly created entity etc etc.
I hope this helps, it's worked nicely so far for me!
Definitely persist the actions!
This is only a counterexample, adding to Dan Fitch's comment in the previous answer.
If you persisted your state, you wouldn't ever be able to modify your state without altering columns and tables in your database. The state shows you only how things are now, you can't rebuild a previous state, and you won't know which facts had happened.
Example: persist an action to a server
Your action already is a "type" and a "payload", and that's probably all you need in an Event-Driven/Event-Sourcing architecture.
You can call your back-end and send the actions inside your actionCreator (see Dan Fox's answer).
Another alternative is to use a middleware to filter what actions you need to persist, and send them to your backend, and, optionally, dispatch new events to your store.
const persistenceActionTypes = ['CREATE_ORDER', 'UPDATE_PROFILE'];
// notPersistenceActionTypes = ['ADD_ITEM_TO_CART', 'REMOVE_ITEM_FROM_CART', 'NAVIGATE']
const persistenceMiddleware = store => dispatch => action => {
const result = dispatch(action);
if (persistenceActionTypes.indexOf(action.type) > -1) {
// or maybe you could filter by the payload. Ex:
// if (action.timestamp) {
sendToBackend(store, action);
}
return result;
}
const sendToBackend = (store, action) => {
const interestingBits = extractInterestingBitsFromAction(action);
// déjà vu
window.fetch(someUrl, {
method: 'POST',
body: JSON.stringify(interestingBits)
})
.then(checkStatus)
.then(response => response.json())
.then(json => {
store.dispatch(actionCreators.saveSuccess(json.someResponseValue));
})
.catch(error => {
console.error(error)
store.dispatch(actionCreators.saveError(error))
});
}
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
createStore(
yourReducer,
aPreloadedState,
applyMiddleware(thunk, persistenceMiddleware)
)
(You can also use a middleware to send current state to the backed. Call store.getState().)
Your app already knows how to transform actions into state with reducers, so you can also fetch actions from your backend too.

Resources