Storing Normalized data into Redux - reactjs

When making my API request, I get a deeply nested object back from the server, from reading up on this it's bad practice for Redux to store nested object data, therefore, I am using Normalizr on that object which returns me many different entities.
The issue I am facing is when making that request inside my Action. How I can store each entity into a different reducer effectively.
action.ts
export const fetchClaim = (claimId: string) => async (dispatch: any) => {
dispatch({ type: REQUEST_CLAIM, payload: true });
try {
const response = await req.get(`claim/${claimId}`);
const normalizedData = normalize(response.data, Claim);
dispatch({ type: SET_LIABILITY, payload: normalizedData.entities.LiabilityType });
dispatch({ type: SET_CLAIM_STATUS, payload: normalizedData.entities.ClaimStatus });
dispatch({ type: SET_CLAIM, payload: normalizedData.entities.Claim });
dispatch({ type: SET_ACTIONS, payload: normalizedData.entities.Actions });
} catch (e) {}
}
I then have an index.ts file inside my reducer folder which combines the reducers.
import { combineReducers } from 'redux';
import claimReducer from './claimReducer';
import actionReducer from './actionReducer';
import liabilityReducer from './liabilityReducer';
import claimStatusReducer from './claimStatusReducer';
export default combineReducers({
claim: claimReducer,
actions: actionReducer,
liability: liabilityReducer,
claimStatus: claimStatusReducer
});
Finally, each reducer looks identical in terms of storing its data.
import { LiabilityActionTypes, LiabilityState } from "../types/Liability";
const INITIAL_STATE: LiabilityState = {
liability: null
}
// State - Returns what the state was when this reducer was previously ran.
// Action - This is what the action creator dispatched.
export default (state = INITIAL_STATE, action: LiabilityActionTypes): LiabilityState => {
switch (action.type) {
case 'SET_LIABILITY':
return { ...state, liability: action.payload };
default:
return state;
}
}
My question is, it doesn't seem right that if i have around 40 entities, that I need to call dispatch 40 times inside the fetchClaim action. Is there a best practice to achieve this?

Related

Test redux actions file properly

im writing a react app who has a default state management: View dispatch an action than change reducer state. I was able to test the view and the reducer but didn't find a way to test my actions file because return a dispatch function
Action File that need to be tested:
import {Dispatch} from 'redux'
import {AuthAction, AuthActionTypes, SetUserAction} from "../actions-types/auth-actions-types";
export const setUserAction = (user: User) => {
return async (dispatch: Dispatch<SetUserAction>) => {
dispatch({
type: AuthActionTypes.SET_USER,
payload: user
})
}
}
reducer
import {AuthAction, AuthActionTypes} from "../actions-types/auth-actions-types";
export const initialAuthState = {
auth: {},
user: null
};
const reducer = (state = initialAuthState, action: AuthAction) => {
switch(action.type) {
case AuthActionTypes.SET_USER:
return {
...state,
user: action.payload,
};
default:
return state
}
}
export default reducer
reducer Test working ok.
import authReducer, {initialAuthState} from "./auth-reducer";
import {AuthActionTypes} from "../actions-types/auth-actions-types";
describe('Auth Reducer', ()=>{
test('should return user correclty ', ()=>{
const mockPayload = {
name: 'any_name',
emaiL: 'any_email',
accessToken: 'any_tokem'
}
const newState = authReducer(initialAuthState, {
type: AuthActionTypes.SET_USER,
payload: mockPayload
})
expect(newState.user).toEqual(mockPayload);
})
})
Action File test with problems
describe('AuthAction', ()=>{
test('setUserAction', ()=>{
const user = {
name: 'any_user',
email: 'any_email',
token: 'any_token'
}
const result = setUserAction();
expect(result).toEqual(user);
})
})
Expected: {"email": "any_email", "name": "any_user", "token": "any_token"}
Received: [Function anonymous]
Writing an action creator
Here is the official documentation that shows how to create an action creator
I do not see the benefit for your action creator to do a dispatch, you can simply write it and use it in the following way:
// action.ts
import { Dispatch } from 'redux'
import { AuthAction, AuthActionTypes, SetUserAction } from "../actions-types/auth-actions-types";
export const setUser = (user: User) => ({
type: AuthActionTypes.SET_USER,
payload: user
})
// somewhere.ts
dispatch(setUser(user))
Now the redux team recommends using redux-toolkit and they provide a simple tool called createAction
And if you want to create your reducer and action creator at the same time in the easier possible way you can use createSlice
How to test a reducer and an action?
To avoid an opinionated response to this answer you have two paths:
testing reducer with your action creator
a test for the reducer and a test for the action
Testing a reducer with your action creator
The reducer test should confirm that the triggered action has the expected impact.
Here is an example of using your reducer and your action creator together:
describe('Auth Reducer', ()=>{
test('should set user correctly', ()=> {
const newState = authReducer(initialAuthState, setUser(mockPayload))
expect(newState.user).toEqual(mockPayload);
})
})
The benefit of this is that you just write one test and you assert that both action creator and reducer work well together.
How to test an action creator alone?
You do not need to test your action creator if you test your reducer with it.
An action is just an object with a type and payload basically, so you can test it in the following way
describe('AuthAction', () => {
test('setUserAction', () => {
const user = {
name: 'any_user',
email: 'any_email',
token: 'any_token'
}
const result = setUser(user);
expect(result).toEqual({ type: AuthActionTypes.SET_USER, user });
})
})

Exporting extra reducers from redux toolkit

I made a todo list a while ago as a way to practice react and redux. Now I'm trying to rewrite it with redux toolkit and having some trouble with the action creators.
Here is the old actions creator:
export const changeDescription = (event) => ({
type: 'DESCRIPTION_CHANGED',
payload: event.target.value })
export const search = () => {
return (dispatch, getState) => {
const description = getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
axios.get(`${URL}?sort=-createdAt${search}`)
.then(resp => dispatch({ type: 'TODO_SEARCHED', payload: resp.data }))
} }
export const add = (description) => {
return dispatch => {
axios.post(URL, { description })
.then(() => dispatch(clear()))
.then(() => dispatch(search()))
} }
export const markAsDone = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: true })
.then(() => dispatch(search()))
} }
export const markAsPending = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: false })
.then(() => dispatch(search()))
} }
export const remove = (todo) => {
return dispatch => {
axios.delete(`${URL}/${todo._id}`)
.then(() => dispatch(search()))
} }
export const clear = () => {
return [{ type: 'TODO_CLEAR' }, search()] }
Now this is the one that I'm working on, I'm trying to replicate the actions of the old one but using redux toolkit:
export const fetchTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
const response = await axios.get(`${URL}?sort=-createdAt${search}`)
return response.data
})
export const addTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const response = await axios.post(URL, {description})
return response.data
})
export const todoReducer = createSlice({
name: 'counter',
initialState: {
description: '',
list: []
},
reducers: {
descriptionChanged(state, action) {
return {...state, dedescription: action.payload}
},
descriptionCleared(state, action) {
return {...state, dedescription: ''}
},
},
extraReducers: builder => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
const todo = action.payload
return {...state, list: action.payload}
})
.addCase(addTodos.fulfilled, (state, action) => {
let newList = state.list
newList.push(action.payload)
return {...state, list: newList}
})
}
})
The thing is, I can't find anywhere how to export my extra reducers so I can use them. Haven't found anything in the docs. Can someone help?
extraReducers
Calling createSlice creates a slice object with properties reducers and actions based on your arguments. The difference between reducers and extraReducers is that only the reducers property generates matching action creators. But both will add the necessary functionality to the reducer.
You have correctly included your thunk reducers in the extraReducers property because you don't need to generate action creators for these, since you'll use your thunk action creator.
You can just export todoReducer.reducer (personaly I would call it todoSlice). The reducer function that is created includes both the reducers and the extra reducers.
Edit: Actions vs. Reducers
It seems that you are confused by some of the terminology here. The slice object created by createSlice (your todoReducer variable) is an object which contains both a reducer and actions.
The reducer is a single function which takes the previous state and an action and returns the next state. The only place in your app when you use the reducer is to create the store (by calling createStore or configureStore).
An action in redux are the things that you dispatch. You will use these in your components. In your code there are four action creator functions: two which you created with createAsyncThunk and two which were created by createSlice. Those two will be in the actions object todoReducer.actions.
Exporting Individually
You can export each of your action creators individually and import them like:
import {fetchTodos, descriptionChanged} from "./path/file";
Your fetchTodos and addTodos are already exported. The other two you can destructure and export like this:
export const {descriptionChanged, descriptionCleared} = todoReducer.actions;
You would call them in your components like:
dispatch(fetchTodos())
Exporting Together
You might instead choose to export a single object with all of your actions. In order to do that you would combine your thunks with the slice action creators.
export const todoActions = {
...todoReducer.actions,
fetchTodos,
addTodos
}
You would import like this:
import {todoActions} from "./path/file";
And call like this:
dispatch(todoActions.fetchTodos())

Integrate socket.io into reactjs + redux that uses REST

My project consists of a backend(nodejs , express, mysql) and a frontend (reactjs, redux).
The flow of a rendered component is in a simple redux pattern-
in ComponentDidMount I call an action creator this.props.getResource()
in action creator I use axios to call the backend and dispatch an action in callback like so :
actions.js
export const getResource = () => dispatch => {
axios.get(API_URL/path/to/resource)
.then(res => {
dispatch({
type: SOME_RESOURCE,
payload: res.data
});
})
.catch(e =>
dispatch({
type: ERROR,
payload: e
})
);
};
in reducer I send back to component the state with the new array :
reducers.js
export default function(state = initialState, action) {
switch (action.type) {
case SOME_RESOURCE:
return {
...state,
resources: [...state.resources, action.payload] // add new resource to existing array
};
}
default: return state;
}
}
It is working as it should using REST APIs but now I wish to replace a certain API call with a socket so that data is shown in real-time without needing to refresh the page.
How can I convert above example to use sockets instead of API calls?
This is what I have tried:
Flow starts the same - I call an action creator in ComponentDidMount
I changed the action creator to the following :
actions.js
import io from 'socket.io-client';
const socket = io(); // localhost backend
export const getResource= () => dispatch => {
socket
.on("getResourceEvent", res => {
dispatch({
type: SOME_RESOURCE,
payload: res.data
});
})
.on("onError", e => {
dispatch({
type: ERROR,
payload: e
});
});
};
no changes in reducers.js
This works but with each rendering of the component, the store.getState() gets called 1 additional time. On first render getState() is called 1 time and if I refresh the page I get 2 calls from getState() and so on.
What's causing this behavior and how can I prevent it?
Edit:
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [ thunk ];
var createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
var store = createStoreWithMiddleware(
rootReducer,
initialState,
applyMiddleware(...middleware)
)
store.subscribe(() => console.log("Store.getState()", store.getState()))
export default store;

Unable to get updated state after call multiple dispatch

I'm using redux with React to manage states but when I called two dispatch function from one action creator, it's return state from the first dispatch but unable to get updated state after another dispatch call.
I've tried to call dispatch from different reducers and tried to call after API call.
Here are my actions.
export const setLoader = (loader) => dispatch => {
dispatch({ type: SET_LOADER, payload: loader });
};
export const fetchCategory = (setLoader) => async dispatch => {
setLoader(true);
try {
const instance = axios.create();
instance.defaults.headers.common['Authorization'] = AUTHORIZATION_TOKEN;
const response = await instance.get(API_PATHS.SERVICE_CATEGORY_API);
dispatch({ type: FETCH_CATEGORY, payload: response.data });
} catch (e) {
setLoader(false);
}
};
Here i defined reducers:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_CATEGORY:
return { ...state, categoryList: action.payload };
case SET_LOADER:
return { ...state, isLoading: action.payload };
default:
return state;
}
};
Here my component connected with redux:
const mapStateToProps = state => {
return ({
categoryList: state.locator.categoryList
});
}
export default connect(
mapStateToProps,
{ fetchCategory, setLoader }
)(ServiceLocator);
I expect the output to return updated categoryList, but the actual it returns a blank array.
You are performing an asynchronous task in your action creator, which Redux can't handle without a middleware. I recommend using the middleware redux-thunk. This will allow you to perform asynchronous actions in your action creators and dispatch multiple times.
Hope this helps!
UPDATE:
If you have the redux-think middleware installed and added to Redux (per your comment), then next I would look at setLoader() - it looks like that function is curried and I don't think you want it to be. I would remove the setLoader() step and dispatch that action directly from fetchCategory():
export const fetchCategory = () => async dispatch => {
dispatch({ type: SET_LOADER, payload: true });
try {
const instance = axios.create();
instance.defaults.headers.common['Authorization'] = AUTHORIZATION_TOKEN;
const response = await instance.get(API_PATHS.SERVICE_CATEGORY_API);
dispatch({ type: FETCH_CATEGORY, payload: response.data });
} catch (e) {
dispatch({ type: SET_LOADER, payload: false });
}
};

How to access redux store from action creator?

I'm looking at trying to chain actions together. In my current issue, when the SET_CURRENT_USER action occurs, I'd like it to modify the state (set the current user) and then fire off a bunch of other side-effect tasks (go fetch data, rebuild the UI, etc). My first thought was "well, I'll set a listener on the store"... which resulted in this previous question: How to access 'store' in react redux? (or how to chain actions) There, I was basically that setting listeners is an anti-pattern.
The solution suggested was to 'dispatch' multiple actions chained together. I didn't follow how to do that (my "mapDispatchToProps" is based on the redux tutorials and looks nothing like the suggested mapDispatchToProps) so I did some additional googling about how to chain side-effect actions together and got to this page: https://goshakkk.name/redux-side-effect-approaches/
Trying the first example, I went to my action creator file, which looks like this:
(actionCreators.js)
export function setCurrentUser(username) {
return { type: SET_CURRENT_USER, payload: username }
}
export function actionTwo(username) {
return { type: ACTION_TWO, payload: username }
}
export function actionThree(username) {
return { type: ACTION_THREE, payload: username }
}
and I tried to change the 'setCurrentUser' action creator to something resembling what was in the demo but without the async parts for simplicity - just looking to see if the actions fire:
export function setCurrentUser(username) {
return dispatch => {
dispatch( { type: SET_CURRENT_USER, payload: username } );
dispatch( { type: ACTION_TWO, payload: username } );
dispatch( { type: ACTION_THREE, payload: username } );
}
}
In my app, my mapDispatchToProps looks like this:
const mapDispatchToProps = {
setCurrentUser: setCurrentUser,
}
and I call this in the switchUser handler, like this: this.props.setCurrentUser(this.state.newUsername);
... and I get an error saying that actions must be plain objects.
How can I chain actions together?
Perhaps a deeper issue is that I don't know how to access the store in order to be able to call store.dispatch. (which was my previous question noted above)
i cannot leave a comment so a question: why dont you just set mapState and pass the state as an argument to dispatched action?
Here are Component stuff
class AComponent extends Component{
...
... onClick={()=>this.props.anAction(this.props.state)}
}
const mapStateToProps = state => {
return { state: state.YourReducer }
}
const mapDispatchToProps = dispatch => {
return { anAction: () => dispatch(actions.doAnyStaffWith(state)) }
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerReviewer)
actions file:
export function actionThree(state) {
return { type: ACTION_TYPE, state }
}
That's what you're looking for?
You'll need thunk in order to enhance your action creators. the reason why you're getting the "must be plain objects" error is because your action creator is returning a function(), not an object. Thunk allows you to return functions in your action creators and with it, the code you wrote should work.
import { createStore, combineReducers, applyMiddleware } from "redux"
import thunk from "redux-thunk"
const store = createStore(combineReducers({
data1: reducer1,
data2: reducer2
}), {}, applyMiddleware(thunk))
Hope that works.
You should be able to access your store like this (illustrating with a fetch request):
import { someAPICALL } from '../api'
export function setCurrentUser(username) {
return async (dispatch, getState) => {
const { yourStateVariable } = getState().YourReducer
, data = await someAPICall(yourStateVariable)
.catch(e => dispatch({ type: FETCH_ERROR, payload: e }))
if (data) {
dispatch( { type: SET_CURRENT_USER, payload: username } );
dispatch( { type: ACTION_TWO, payload: username } );
dispatch( { type: ACTION_THREE, payload: username } );
} else {
dispatch( { type: SOME_OTHER_ACTION, payload: 'whatever state update you like' } )
}
}
}
Redux thunk would be the ideal way to do it but you can still chain actions by returning promises as your payload.(If you do not want to use thunk)
export function setCurrentUser(username) {
return {
type: SET_CURRENT_USER,
payload: new Promise(resolve => resolve(username))
}
}
export function actionTwo(username) {
return {
type: ACTION_TWO,
payload: new Promise(resolve => resolve(username))
}
}
export function actionThree(username) {
return {
type: ACTION_THREE,
payload: new Promise(resolve => resolve(username))
}
}
And then you can call all three like:
this.props.setCurrentUser(this.state.newUsername)
.then(newUsername => this.props.actionTwo(newUsername))
.then(newUsername => this.props.actionThree(newUsername))
I think you mapDispatchToProps function should look like this: const mapDispatchToProps = {
setCurrentUser: (data) => dispatch(setCurrentUser(data)),
}. Regarding accessing store from your action creators, you need to export it from where you declared it(index.js). import it to your actionCreators file and then use it there. It goes as follows:
in index.js file: export const store = createStore(...) and in your actionCreators file: import { store } from "your index.js file's path" and you are good to go. Hope that solves your problem.

Resources