Redux single state update - reactjs

I receive the data for all lamps in one request.
(I can, however, receive each lamp data from the server individually).
The data I receive from the server looks like this.:
[
{
"id": "1",
"state": 'ON'
},
{
"id": "2",
"state": 'OFF'
},
{
...
},
...
]
In my app, this data will be stored in a single redux state. I use FlatList to render this data along with a simple Switch so that the user can turn each lamp ON or OFF.
When the user changes a lamp's state, a LOADING action will be dispatched and the whole lamps will show a spinner.
When the updated data is received from the server, a SUCCESS action will be dispatched, the redux state will be updated, the spinner will disappear and finally, the updated data will be shown.
Problem 1: When the user interacts with only one lamp, I don't want all lamps to go into LOADING state!
Problem 2: I never know how many lamps I will receive in my requests.
Desired Behaviour: Only the lamp, with which the user has interacted, must show a spinner and goes thrown the update process.
I need help with handling the redux states.
I hope this information helps. Below you can find my Rudcers for lightingInitReducer (getting data for all lamps) and lightingChangeStateReducer (changing the state of a lamp). isLoading is used for showing the spinner. Additionally, when the change state process was successful (LIGHTING_CHANGE_STATUS_SUCCESS is dispatched), the new data will be requsted from the server with an init request:
export const lightingInitReducer = (state = {
isLoading: false,
errMess: null,
info: {},
}, action) => {
switch (action.type) {
case ActionTypes.LIGHTING_INIT_SUCCESS:
return {
...state, isLoading: false, errMess: null, info: action.payload,
};
case ActionTypes.LIGHTING_INIT_FAILED:
return {
...state, isLoading: false, errMess: action.payload, info: {},
};
case ActionTypes.LIGHTING_INIT_LOADING:
return {
...state, isLoading: true, errMess: null, info: {},
};
default:
return state;
}
};
export const lightingChangeStateReducer = (state = {
isLoading: false,
errMess: null,
info: {},
}, action) => {
switch (action.type) {
case ActionTypes.LIGHTING_CHANGE_STATUS_SUCCESS:
return {
...state, isLoading: false, errMess: null, info: action.payload,
};
case ActionTypes.LIGHTING_CHANGE_STATUS_FAILED:
return {
...state, isLoading: false, errMess: action.payload, info: {},
};
case ActionTypes.LIGHTING_CHANGE_STATUS_LOADING:
return {
...state, isLoading: true, errMess: null, info: {},
};
default:
return state;
}
};

Check out immutability-helper: https://github.com/kolodny/immutability-helper
It's amazing. Reducers can get real messy with deeply nested states, and this lib helps tidy things up a lot.
For problem 1, here is an idea of how you could update a single lamp, and display a loading state for that one item:
You can use the index of the pressed list item from your FlatList and pass that along to the LOADING action that dispatches your server request. You can then use this index in your reducer to update that specific lamp in your state. Using the immutability-helper library, the reducer might look like this:
import update from 'immutability-helper';
case UPDATE_LAMP_REQUEST:
let newState = update(state, {
lamps: {
[action.index]: {
loading: { $set: true }, // turns on loading state for lamp at index
}
}
})
return newState;
Once the web request has complete, and your SUCCESS action has dispatched, you can then pass that same index from your FlatList to your reducer, and update the lamp in your state with the updated lamp from the server:
import update from 'immutability-helper';
case UPDATE_LAMP_SUCCESS:
let newState = update(state, {
lamps: {
[action.index]: {
loading: { $set: false }, // turn off loading
$set: action.lamp // set the new lamp state
}
}
})
return newState;
You'd then connect the "loading" field to the component responsible for a single list view item in your FlatList and use that value to hide / show the loading state.
As for problem 2, I'm not sure I understand the question.

Related

Redux reducer: Add object to array when the array doesn't have the object data

I'm trying to store AJAX call returned data object to an array of reducer state of Redux.
I have some conditions to check if the fetched data already exists inside of the reducer.
Here are my problems:
The component to call AJAX call actions, it's a function from mapDispatchToProps, causes an infinite loop.
isProductLikedData state in reducer doesn't get updated properly.
Can you tell what I'm missing?
Here are my code:
isProductLikedActions.js - action to fetch isProductLiked data. response.data looks like {status: 200, isProductLiked: boolean}
export function fetchIsProductLiked(productId) {
return function (dispatch) {
axios
.get(`/ajax/app/is_product_liked/${productId}`)
.then((response) => {
dispatch({
type: 'FETCH_IS_PRODUCT_LIKED_SUCCESS',
payload: { ...response.data, productId },
});
})
.catch((err) => {
dispatch({
type: 'FETCH_IS_PRODUCT_LIKED_REJECTED',
payload: err,
});
});
};
}
isProductLikedReducer.js - I add action.payload object to isProductLikedData array when array.length === 0. After that, I want to check if action.payload object exists in isProductLikedData or not to prevent the duplication. If there is not duplication, I want to do like [...state.isProductLikedData, action.payload].
const initialState = {
isProductLikedData: [],
fetching: false,
fetched: false,
error: null,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_IS_PRODUCT_LIKED': {
return { ...state, fetching: true };
}
case 'FETCH_IS_PRODUCT_LIKED_SUCCESS': {
return {
...state,
fetching: false,
fetched: true,
isProductLikedData:
state.isProductLikedData.length === 0
? [action.payload]
: state.isProductLikedData.map((data) => {
if (data.productId === action.payload.productId) return;
if (data.productId !== action.payload.productId)
return action.payload ;
}),
};
}
case 'FETCH_IS_PRODUCT_LIKED_REJECTED': {
return {
...state,
fetching: false,
error: action.payload,
};
}
}
return state;
}
Products.js - products is an array that fetched in componentWillMount. Once nextProps.products.fetched becomes true, I want to call fetchIsProductLiked to get isProductLiked` data. But this causes an infinite loop.
class Products extends React.Component {
...
componentWillMount() {
this.props.fetchProducts();
}
...
componentWillReceiveProps(nextProps) {
if (nextProps.products.fetched) {
nextProps.products.map((product) => {
this.props.fetchIsProductLiked(product.id);
}
}
render() {
...
}
}
export default Products;
Issue 1
The component to call AJAX call actions, it's a function from mapDispatchToProps, causes an infinite loop.
You are seeing infinite calls because of the condition you used in componentWillReceiveProps.
nextProps.products.fetched is always true after products (data) have been fetched. Also, note that componentWillReceiveProps will be called every time there is change in props. This caused infinite calls.
Solution 1
If you want to call fetchIsProductLiked after products data has been fetched, it is better to compare the old products data with the new one in componentWillReceiveProps as below:
componentWillReceiveProps(nextProps) {
if (nextProps.products !== this.props.products) {
nextProps.products.forEach((product) => {
this.props.fetchIsProductLiked(product.id);
});
}
}
Note: you should start using componentDidUpdate as componentWillReceiveProps is getting deprecated.
Issue 2
isProductLikedData state in reducer doesn't get updated properly.
It is not getting updated because you have used map. Map returns a new array (having elements returned from the callback) of the same length (but you expected to add a new element).
Solution 2
If you want to update the data only when it is not already present in the State, you can use some to check if the data exists. And, push the new data using spread syntax when it returned false:
case "FETCH_IS_PRODUCT_LIKED_SUCCESS": {
return {
...state,
fetching: false,
fetched: true,
isProductLikedData: state.isProductLikedData.some(
(d) => d.productId === action.payload.productId
)
? state.isProductLikedData
: [...state.isProductLikedData, action.payload],
};
}

Updating a value in an object in Redux reducer

I am looking to update value(s) in a nested object based on user input. So for instance, if the user chooses to update their country and street, only those values will be passed as the action.payload to the reducer and updated (the rest of the state will be kept the same). I provide my initial state and reducer:
My state:
const initialState = {
userData: [
{
firstName: "",
lastName: "",
country: "",
address: {
street: "",
houseNumber: "",
postalCode: "",
}
}
],
error: null,
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_USER_DATA:
return { ...state, userData: action.payload };
case UPDATE_USER_DATA:
return {
...state,
userData: [{
...state.userData,
...action.payload,
}]
};
default:
return state;
}
};
Any help would be great, thank you!!
It doesn't appear that you need the array wrapping the object. If that is true, remove it for simplicity. Then userData becomes a plain object, and your update becomes:
return {
...state,
userData: { // <- no wrapping array, just update the object
...state.userData,
...action.payload,
}
};
Since there is an array, you would need to destructure the object at the correct index.
return {
...state,
userData: [{
...state.userData[0], // <- destructure the object, not the array
...action.payload,
}]
};
If you do need the array, and there will be more than one object, you will also need to pass an identifier in the action payload in order to know which index to update, but with the current information given this is the most complete answer possible.

Redux initial state in reducer not accepting new values

I'm running into a strange problem where the initial state in one of my reducers is not accepting new values. I've been able to add new values to this initial state easily, but for some reason now new initial state entries come back as undefined when I mapStateToProps.
//REDUCER
const initialState = {
(...cutting out a bunch of state here),
collectionSearchResults: {
results: {},
loading: false,
loaded: false,
error: ''
},
collectionImage: {
image: '',
loading: false,
loaded: false,
error: '',
},
collectionBeingEdited: {
collectionId: '',
loading: false,
complete: false,
error: '',
test: '',
},
removeReport: {
loading: false,
}
}
//INDEX of Component
const mapStateToProps = state => ({
(...cutting out a bunch of state here)
collectionBeingEdited: state.research.collectionBeingEdited,
removeReport: state.research.removeReport,
userInfo: state.account.myAccount.info,
})
//IN COMPONENT
console.log(this.props)
//result -> removeReport: undefined
InitialState of a reducer is not a reducer. As Martin suggested, you need to post your actual reducer.
I'm willing to bet that in one of your reducer cases its not returning the rest of the state:
case 'something':
return {
someKey: action.value
}
instead of:
case 'something':
return {
...state,
someKey: action.value
}
and thats why the property you're expecting doesnt exist.

Redux state coming out to be undefined

I am having tough time figuring out why my redux state is coming out to be undefined on a later stage.
so I have an action which looks like this
export const coinUpdateState = (booleanValue) => {
console.log(typeof booleanValue) //This logs Boolean
return function (dispatch) {
dispatch({
type: COIN_UPDATE_STATE,
payload: booleanValue
})
}
}
and a reducer which looks like this
const initialState = {
DataFetching: true,
DataSucess: [],
DateError: [],
DateError: false,
DataSort: true,
DataUpdate: true
}
export default function(state = initialState, action) {
switch (action.type) {
case EXCHANGE_CURRENCY_FETCHING:
return {
DataFetching: true,
DataSort: true
}
case EXCHANGE_CURRENCY_FETCH_SUCCESS:
return {
...state,
DataSucess: action.payload,
DataFetching: false,
DataSort: false
}
case EXCHANGE_CURRENCY_FETCH_ERROR:
return {
...state,
DateError: action.payload,
DataFetching: true,
DateError: true,
DataSort: true
}
case COIN_UPDATE_STATE:
console.log(action.payload) //This logs the boolean value I am sending
return {
DataUpdate: action.payload
}
default:
return state
}
}
Later I am using it like this in my app
render () {
console.log(this.props.cryptoUpdateState)
if (this.props.cryptoUpdateState) {
console.log("Inside if render")
displayData = async () => {
this.coinURL = await AsyncStorage.getItem("CryptoCurrencySelected").catch((error) => {
console.log(error)
})
if (this.coinURL != "undefined" && this.coinURL != null) {
console.log("Inside Async", this.coinURL)
this.props.exchangeToDisplay(this.coinURL)
}
if (this.coinURL == null || this.coinURL == "undefined") {
console.log("Null", this.coinURL)
this.coinURL = "BTC"
}
}
displayData()
this.props.coinUpdateState(false)
}
In the above snippet notice the initial console.log, it logs true correctly the first time, there after it logs undefined in console when I am actually passing false (this.props.coinUpdateState(false)).
Also Notice the logs in my code, they are logging the value I am sending correctly everywhere besides in the later stage where it is logging undefined (in console.log).
Question: What could I be doing wrong here?
A reducer will return a new version of the state of your app. If you don't want to lose your previous state you will need to make sure you always return it with any additions/modifications you need.
Whatever is returned from the switch case that is triggered is going to be the next version of state, which for COIN_UPDATE_STATE is going to just have DataUpdate in.
Make sure all you are doing ...state for all your redux reducer actions in order to make sure you keep state.
e.g.
case COIN_UPDATE_STATE:
return {
...state,
DataUpdate: action.payload
}

Handle request call responses in react-redux app to avoid using callbacks

This is rather a design question, but I'm wondering if there's a best practice to handle such situations.
I have a react redux app. The app state structure looks like this:
{
groups: {gid1: group1, gid2: group2, ....},
expenses: {eid1: expense1, eid2: expense2, ...},
//.... the rest
}
The NewGroup component is aimed to create a new group. If the create request (ajax request to the backend) is successful , it is added to the list of groups in the app state. The NewGroup, which is a modal, is closed and list of groups updates.
Though the NewGroup component should not be closed if the request was not successful.
The main question is: Should I incorporate the request status (if it is successful or unsuccessful, the error message in the latter case, if the result is returned or not, etc.) in the app state ? This is where I am really doubtful because it makes the app state very ugly, large and in some cases it is incapable of handling some situations, in this case: Creating a new group.
Let's say I add the response status code and error message to the app state:
{
groups: {
newGroupId1: {
data: newGroupData1,// null in case it was not successful
isLoading: true/false, // if the call is returned or not
errorFetching: message1
}
}
}
On the second unsuccessful create request to the backend, it is not possible to distinguish between the previous and the current calls, as both of them have errorFetching and similar isLoading values. (for example both have "No group found" for errorFetching and "false" for isLoading)
What is the best practice to distinguish between the status of the request calls and the real data in the app state when redux is being used ?
Update 1
This is the reducer:
import { FETCH_GROUPS_SUCCESS, FETCH_GROUPS_ERROR, CREATE_GROUP_SUCCESS, CREATE_GROUP_ERROR, DELETE_GROUP, FETCH_GROUP_SUCCESS, FETCH_GROUP_ERROR } from '../actions/creators';
export const FULL_GROUP = 'full_group';
export const SNIPPET_GROUP = 'snippet_group';
const INITIAL_STATE = {
data: null,
isLoading: true,
errorFetching: null
};
export default function(state=INITIAL_STATE, action) {
switch(action.type) {
case FETCH_GROUPS_SUCCESS:
return {
data: _.zipObject(_.map(action.payload, group => group.id),
_.map(action.payload, group => ({
data: group,
isLoading: true,
errorFetching: null,
mode: SNIPPET_GROUP
}))
),
isLoading: false,
errorFetching: null
};
case FETCH_GROUPS_ERROR:
return {
data: null,
isLoading: false,
errorFetching: action.payload.error
};
case FETCH_GROUP_SUCCESS:
return {
data: {...state.data, [action.payload.id]: {
data: action.payload,
isLoading: false,
errorFetching: null,
mode: FULL_GROUP
}},
isLoading: false,
errorFetching: null
};
case FETCH_GROUP_ERROR:
const stateWithoutId = _.omit(state, action.payload.id);
return {
data: {...stateWithoutId},
isLoading: false,
errorFetching: null
};
case CREATE_GROUP_SUCCESS:
debugger;
//TODO: how to let the NewGroup know that a group was created successfuly ?
return { ...state, [action.payload.id]: {
data: action.payload,
isLoading: true,
errorFetching: null,
mode: SNIPPET_GROUP
}};
default:
return state;
}
}
This is the action creator:
export function groupsFetchSucceeded(response) {
return {
type: FETCH_GROUPS_SUCCESS,
payload: response
}
}
export function groupsFetchErrored(errorWithId) {
return {
type: FETCH_GROUPS_ERROR,
payload: errorWithId
}
}
export function groupCreateSucceeded(group) {
return {
type: CREATE_GROUP_SUCCESS,
payload: group
}
}
export function groupCreateErrored(errorWithId) {
return {
type: CREATE_GROUP_ERROR,
payload: response
}
}
export function groupFetchSucceeded(groupData) {
return {
type: FETCH_GROUP_SUCCESS,
payload: groupData
}
}
export function groupFetchErrored(errorWithId) {
//handle the error here
return {
type: FETCH_GROUP_ERROR,
payload: errorWithId
}
}
This is the component. PLEASE! disregard the fact that this is a redux form, I don't care it's a redux form or not, I'm looking for a general idea. You can assume that it is a react-redux component with mapStateToProps and mapDispatchToProps and other related stuff.
import { createGroup } from '../../actions';
import { validateName, validateDescription } from '../../helpers/group_utils';
import { renderField, validate } from '../../helpers/form_utils';
const validators = {
name: validateName,
description: validateDescription
};
class NewGroup extends Component {
_onSubmit(values) {
this.props.createGroup(values);
}
render() {
const { handleSubmit } = this.props;
return (
<div>
<form onSubmit={handleSubmit(this._onSubmit.bind(this))}>
<Field name="name" label="Name" type="text" fieldType="input" component={renderField.bind(this)}/>
<Field name="description" label="Description" type="text" fieldType="input" component={renderField.bind(this)}/>
<button type="submit" className="btn btn-primary">Create</button>
<button type="button" className="btn btn-primary" onClick={() => this.props.history.push('/group')}>Cancel</button>
</form>
</div>
);
}
}
export default reduxForm({
validate,
//a unique id for this form
form:'NewGroup',
validators
})(
connect(null, { createGroup })(NewGroup)
);
The tricky part of your problem is that while you want to record all request statuses in redux's state, the number of requests might increase exponentially and take a lot of state's volume if not being handled well. On the other hand, I don't think distinguishing request data and state data is a big deal: you only need to separate them in state:
{
groups: {gid1: group1, gid2: group2, ....},
groupRequests: {grid1: request1, grid2: request2, ...},
expenses: {eid1: expense1, eid2: expense2, ...},
...
}
where each group data contains:
{
data: groupData,
lastRequestId: grid // optional
}
and each group request data contains:
{
groupId: gid,
isLoading: true/false,
errorFetching: message
}
When new request is sent, you update in groupRequests and track it there; NewGroup component only need to hold groupId to find the latest request fired and look after its status. And when you need to know the status of previous requests, take them from the list.
Again, the problem with this approach is that most of the data in groupRequests is one-time-used, meaning at some point (e.g. when NewGroup component closes), you don't even need it anymore. So make sure that you properly throw old request status away before groupRequest grows too big.
My advice is that when dealing with redux state, try to normalize it properly. If you need an 1-n relationship (group data and request data of creating group), break it down further instead of grouping them together. And avoid storing one-time-used data as much as possible.

Resources