How to prevent non-deterministic state updation in Redux? - reactjs

When working with Redux, maintaining the shape of the initial state is crucial. The results/data of side effects like API call will change the shape of the state since we have no control over the properties. For example, consider this initial state:
const book = {
id: 0,
name: 'something'
};
And updation is made to it by the book sub-reducer as follows based on the API data:
//receives `book` part of the state
const bookReducer = (state=book, action) => {
switch(action.type) {
case 'SET_BOOK': {
return { ...action.payload };
} default:
return state;
}
}
Two scenarios that could happen:
If the data sent from the API is null, then newly produced state is now {} as a result of the spread operator. If some parts of UI were to listen to the book part of the state, then it will break. Possibly access individual properties from the API data? In that case, null/undefined checks needs to be performed for properties. Is there a more elegant solution?
There could also be additional properties in the data which we may not be interested in. Possibly use an object mapper to filter unused properties?
What is the best practice to handle these kind of scenarios and prevent state becoming non-deterministic? Please share your experience on how you approached these scenarios.

Only the reducer has to be pure/deterministic, not the stuff outside of it.
To prevent your reducer from overwriting data incorrectly, write some logic between the API response and the dispatch-call to ensure the reducer always gets valid data.
For example a thunk might look like:
const createBook = (name) => {
return async dispatch => {
// suppose the api call gives back "uid" plus extra data
const { uid, ...unneededData } = await myApi.setBook(name);
// dispatch the data in the way the reducer expects it
dispatch({ type: 'SET_BOOK', id: uid, name });
}
}
In the above example, the api call gives me uid, but no name, and a bunch of extra data. Just prepare the data before dispatching it.

The best practice is the one where you prevent your app from breaking from every aspect, which means you need to check and format your data before returning from the reducer.
In your case, I would check for both data validity and map it to a necessary format.
only dispatch 'SET_BOOK' if API response has both id and book.
in order to avoid unnecessary additional properties, you can always map your data const book = {id: apiData.id, book: apiData.book} before dispatching.

In your reducer you can do like below. This way only id and name will get updated even though there are other key/values in the response. Also this will make sure that if null values are received then those values will not be updated in the state. Hope this will help in resolving the issue.
//receives `book` part of the state
const bookReducer = (state=book, action) => {
const { type, payload } = action;
switch(type) {
case 'SET_BOOK': {
return {
...state,
...(payload.id && {id: payload.id}),
...(payload.name && {name: payload.name})
};
} default:
return state;
}
}

Your redux reducer logic should not worry about that due to its deterministic nature. You handle your api call and response handling elsewhere (redux thunk or component), and then dispatch the action to set your redux. Building off of your example:
book.reducer.js
const book = {
id: 0,
name: ''
};
const bookReducer = (state=book, action) => {
switch(action.type) {
case 'SET_BOOK': {
return { ...action.payload };
} default:
return state;
}
book.actions.js
const setBook = (book) => ({
type: SET_HEROES,
payload: book
});
// thunk
const findBook = name => async dispatch => {
const book = await bookService.findBook(name);
if (book) {
dispatch(setBook(book));
}
};
book.service.js
const findBook = async (name) => {
// Do your api call
const bookResponse = axios.get(`${apiUrl}/book/search/${name}`);
// Handle the response
if (!bookResponse) {
// Logic you do if book not found
return null;
}
return {id: bookResponse.id, name: bookResponse.name};
}
Now in a component you can just dispatch the findBook call
Component.js
const Component = () => {
const [search, setSearch] = useState('');
const dispatch = useDispatch();
const handleOnSearch = () => {
dispatch(findBook(search));
}
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)}/>
<button onClick={handleOnSearch}>Search</button>
</div>
);
}

If field value from API is undefined then convert it into null and store so that the code doesn't break and operatable. If API gives other params as well then de-structure the API returned object and extract the required fields. So that storing unnecessary data can be avoided.
const bookReducer = (state=book, action) => {
switch(action.type) {
case 'SET_BOOK': {
const {id, name, otherParam1, otherParam2} = action.payload;
return {
id: id || null,
name: name || null,
otherParam1,
otherParam2
}
} default:
return state;
}
}
Having the value null won't break the code instead, it renders nothing
which is better than undefined which breaks the code
Hope this helps you

What I do is to have all of my logic in my action method and create reducers for when an action is correctly fulfilled and another one for when is rejected. In the fulfilled reducer, I would do the regular instructions and in the rejected reducer I would add the data to a variable called error which I always have in my state and use in the frontend to show an error message if needed.
Example
This is an action that creates a house by sending a post request to my api which returns the created object or an error if something went wrong.
export const createHouse = houseData => {
const URL = HTTP://EXAMPLE.URL
return async dispatch => {
try {
const response = await axios.post(`${URL}`, houseData);
const data = await response.data;
dispatch({ type: "CREATE_HOUSE_DRAFT_FULFILLED", data });
} catch (err) {
dispatch({ type: "CREATE_HOUSE_DRAFT_REJECTED", data: err });
}
};
};
Then I would have 2 reducer methos to recieve the fulfilled or the rejected response, like this.
case 'CREATE_HOUSE_DRAFT_FULFILLED': {
return {
houses: [action.data, ...state.houses],
house: action.data,
houseCount: state.houseCount + 1,
fetched: true,
error: null
};
}
case 'CREATE_HOUSE_DRAFT_REJECTED': {
return {
...state,
error: action.data.response.data,
fetched: false,
success: null
};
}
Hope this works for you!

Related

Pulling Redux data with userID param error - React / Node / Express

I am trying to pull a log of user data belonging to userID from my backend in React. The backend API is working correctly with Postman however I can't get this working with the userID. It is console logging the correctly userID in the component however I am yet to get a Thunk Redux store data working with a param passed in.
Is there anywhere obvious I am going wrong from looking at the code below? I have had my Redux working previously with data not using a param so I know it is not an issue with my store / redux index etc.
component
const userDiveLogList = useSelector(state => state.user.userDiveLogList);
const [userDiveLog, setUserDiveLog] = useState({
userDiveLogList: [],
expanded: false
})
const dispatch = useDispatch();
useEffect(() => {
dispatch(requireUserDiveLogData(user.userID));
}, []);
action
// load a users diveLog
export const requireUserDiveLogData = createAsyncThunk('diveLog/requireData',
async (userID, thunkAPI) => {
const response = await userDiveLogList(userID);
return response.data;
},
{
condition: (_, { getState }) => {
const { user } = getState();
if (user.didLoadData) {
return false;
}
}
}
)
reducer
export const userSlice = createSlice({
name: 'user',
initialState: {
userDiveLogList: [],
didLoadData: false,
},
reducers: {
[requireUserDiveLogData.pending.type]: (state) => {
state.didLoadData = true;
},
[requireUserDiveLogData.fulfilled.type]: (state, action) => {
return {
...state,
...action.payload
}
},
service
// returns list of dives per user from db
export const userDiveLogList = (userID) => {
return axios.get(API_URL + `userdiveloglist/${userID}`);
};
Update
Does this have something to do with my action call for when data gets called if it is already called? I have the getState condition const as userDiveLog when it is in the user branch and it is user/userDiveLogList in the branch, it is also named userDiveLogList when it is being sent at the backend. I can see the backend request being made for the correct user number but nothing is returning.

redux-toolkit -> non-serializable value detected

Error:
index.js:1 A non-serializable value was detected in an action, in the
path: payload.config.transformRequest.0.
Value: ƒ
transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) || utils.isArrayBuffer(data) || utils.i…
Slice:
export const getProducts = createAsyncThunk(
'products/getProducts',
async() => {
const res = await axios.get('http://localhost:5000/products/view-products', {withCredentials: true});
return res;
}
)
const getProductsSlice = createSlice({
name : 'products',
initialState : {
list : [],
status : null
},
extraReducers : {
[getProducts.pending] : (state) => {
state.status = 'loading'
},
[getProducts.fulfilled] : (state, {payload}) => {
console.log("produtcts payload: ", payload.data)
state.list = payload.data
state.status = 'success'
},
[getProducts.rejected] : (state) => {
state.status = 'failed'
}
}
})
Inside Component:
const dispatch = useDispatch();
const data = useSelector(state => state.products.list);
console.log("the products are :", data);
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
Other slices in the app work fine. Having a hard time working around the non-serializable
The problem with returning the result of axios.get as it is, is that apart from data it contains various other fields related to the request, including config for which you got the error. Although you're only saving data and not config, as the whole res object passes to the store it goes through a middleware called Serializability, which is included in redux-toolkit and enforces Redux recommendation to only store serializable data.
Serializable means it can be written down as text and converted back to original object without losing information, which doesn't work with functions. A javascript function apart from code also have scope (memory associated to it), which cannot be represented as text.
Serializability checks the whole payload (it is executed before your data reaches the store, so it doesn't know which parts will be used) and notices config. As config has methods among its members Serializability alerts you that it is not serializable. You could switch the middleware off, but it can detect genuine issues, so it's generally a better idea to just keep only relevant data in the payload.

How to handle redux master and detail

I am using redux for an api-centric application and having trouble with the concept of master and detail retrievals.
Say I get may master list with a redux-thunk action like this:
// action:
export const fetchItems = params => async (dispatch, getState) => {
dispatch({type: FETCH_ITEMS_REQUEST});
let response = await api.fetchItems(params);
dispatch({type: FETCH_ITEMS_SUCCESS, payload: normalize(response, schema)});
}
Then I want to drill down on a single item that I retrieve like this:
export const fetchItem = id = async (dispatch, getState) => {
dispatch({type: FETCH_SINGLE_ITEM_REQUEST});
let response = await api.fetchItem(id);
dispatch({type: FETCH_SINGLE_ITEM_SUCCESS, payload: normalize(response, schema)});
}
They both end up in my reducer in the same way -
const reducer = (state, {type, payload}) => {
switch(type) {
case FETCH_ITEMS_SUCCESS:
case FETCH_SINGLE_ITEM_SUCCESS:
return {
...state,
allIds: payload.result.data,
byId: payload.entities.item,
error: null,
fetching: false
};
case FETCH_ITEMS_REQUEST:
case FETCH_SINGLE_ITEM_REQUEST:
// how do I/should I separate fetching id=1 vs fetching all items??
return { ...state, fetching: true, error: null };
case FETCH_ITEMS_FAILURE:
case FETCH_SINGLE_ITEM_FAILURE:
// how do I/should I separate fetching id=1 vs fetching all items for error??
return { ...state, fetching: false, error: payload };
}
}
I don't think I should be sharing the fetching and error as variables for the single item and all items.
I started down the road of hashing errors and fetching status by id for the single item, but it's kind of ugly.
Should the single item be moved into its own reducer? In which case should it not follow the allIds/byId pattern? I don't think I want to do that because I want to introduce caching so I can check if the record exists before querying the API.
How do I structure my redux reducers for both many and the single record?

Why doesnt add layers on redux

I'm trying to add an array of layers using redux.
First, I create an array of promises.
Secnod, I use Promise.all with promises array and send to database all the info and returns all layers created on database.
Third, totalLayers contains current layers with new layers from database.
My problem is that launch dispatch and draw layers on my map, but dont update the array of redux with totalLayers.
SET_MAP_LAYERS update layers stored in Store as you can see in mapGlLayers variable.
What I'm doing wrong??
static addMultipleLayersFromDataSet(layers, source) {
return (dispatch) => {
let mapGlLayers = store.getStore().getState().maplayers.maplayers.slice();
let position = mapGlLayers.filter(l => l.name).length;
let promises = layers.map( layer => uploadMultipleLayers(layer, source, position++));
Promise.all(promises)
.then(downloadedlayers => {
let totalLayers = [...mapGlLayers, ...downloadedlayers];
dispatch({
type: LayerTypeConstants.SET_MAP_LAYERS,
payload: totalLayers
});
})
.catch(error => {
dispatch({
type: LayerTypeConstants.MAPLAYER_ERROR,
payload: error
});
});
};
}
REDUCER:
import { LayerTypeConstants } from '../utils/ReduxConstants';
const initialStateApp = {
maplayers: [],
};
export default function LayerReducer(state = initialStateApp, action) {
switch (action.type) {
case LayerTypeConstants.SET_MAP_LAYERS: {
return Object.assign({}, state, {
maplayers: action.payload
});
}
case LayerTypeConstants.MAPLAYER_ERROR: {
return Object.assign({}, state, {
messageMapLayer: action.payload
});
}
case LayerTypeConstants.INIT_LAYERS:
return Object.assign({}, initialStateApp);
default:
return state;
}
};
Here is an image of my redux state:
Here is console message with layers:
Promise is a state machine under the hood. State machines perform state transitions. Redux alone is not prepared to handle such transitions so it heeds a helper. I'd suggest reading about Async Redux Actions, link. The two widely used helpers are redux-thunks and redux-saga.

To add loading svg image for async request api

I want to add custom svg image in my code for loading till the response comes from api called.
I have tried adding flag but it's not working.
Please help me how can i add svg image for loading. I am in learning phase of react and redux.
I have processChatMessage for requesting the api call.
The reducer.js is like:
const processChatMessage = (state, action) => {
console.log("[reducer]", action.type);
const { messages } = state;
const { message } = action.payload;
const newMessages = [...messages, message];
return { ...state, messages: newMessages };
};
const commitChatMessageRequest = processChatMessage;
const commitChatMessageSuccess = processChatMessage;
const commitChatMessageError = processChatMessage;
// Hub Reducer
const ChatMessageReducer = (state = initState, action) => {
let newState;
switch (action.type) {
case types["CHAT/MESSAGE_REQUEST"]:
newState = commitChatMessageRequest(state, action);
break;
case types["CHAT/MESSAGE_SUCCESS"]:
newState = commitChatMessageSuccess(state, action);
break;
case types["CHAT/MESSAGE_ERROR"]:
newState = commitChatMessageError(state, action);
break;
default:
newState = state;
}
return newState;
};
export default ChatMessageReducer
And the action.js is as below:
import types from "./action-types";
let isErrorMessage = false;
let onProcess = false;
let sender = 'Bot'
let error = null;
// action creator
export const msgChatMessageRequest = text => ({
type: types["CHAT/MESSAGE_REQUEST"],
payload: {
message: {
text,
sender: "User",
isErrorMessage
},
onProcess: true,
error
}
});
export const msgChatMessageSuccess = text => ({
type: types["CHAT/MESSAGE_SUCCESS"],
payload: {
message: {
text,
sender,
isErrorMessage
},
onProcess,
error
}
});
export const msgChatMessageError = error => ({
type: types["CHAT/MESSAGE_ERROR"],
payload: {
message: {
text: "Sorry! " + error.message,
sender,
isErrorMessage: true
},
onProcess,
error
}
});
Since you are new here, quick advise : just the "it's not working" is never sufficient. Really try to describe the expected behaviour (you did it) but also the current behaviour. For example here I have no idea of what is happening : it the onProcess set to true at anytime ? Is it just maybe a problem of you not succeeding to implement the image itself in your code ? ... So much possibilities.
Anyway I even do not see any call to an API in your code so again, it is difficult to see where the error is coming from...
BUT positive thing, I see that you had the good intuition to use an onProcess boolean in your store, I guess to track the state of your pending request.
Now the logic you need to implement is with 1 action and 3 reducers :
FetchChatMessage : First call IsFetching reducer to pass your onProcess to True, then call your API, then on answer call OnChatMessageSucces reducer if success, or OnChatMessageFail on failure and on each of these methods set your onProcess to false.
Here is a quick example of what you are actually trying to do : https://jsfiddle.net/ibobcode/fgk917bz/5/
.

Resources