React - Passing value to setState callback - javascript

I'm trying to pass a value determined outside my prevState callback into the function. Here is what I have:
uncheckAllUnsentHandler = (e) => {
const newCheckMarkValue = e.target.checked;
this.setState((prevState, props, newCheckMarkValue) => {
const newUnsentMail = prevState.unsentMail.map(policy => {
mail.willSend = newCheckMarkValue;
return mail;
});
return {
unsentMail: newUnsentMail
}
});
}
newCheckMarkValue is undefined inside the map function and I'm not sure why.
Long description:
I have a table where clients select whether they want to send mail or not. By default all mail items are checked in the table. In the header they have the ability to uncheck/check all. I'm trying to adjust the state of the mail items in the table to be unchecked (willSend is the property for that on Mail) when the header checkbox is clicked to uncheck. All of this works if I hardcode the willSend to true or false in code (ex: mail.willSend = true;) in the code below, but I can't seem to get the value of the of the checkbox in the header in.

setState
The updater function takes only two arguments, state and props. state is a reference to the component state at the time the change is being applied.
(state, props) => stateChange
You can simply access the version of newCheckMarkValue enclosed in the outer scope of uncheckAllUnsentHandler.
uncheckAllUnsentHandler = (e) => {
const newCheckMarkValue = e.target.checked;
this.setState((prevState, props) => {
const newUnsentMail = prevState.unsentMail.map(policy => {
mail.willSend = newCheckMarkValue;
return mail;
});
return {
unsentMail: newUnsentMail
}
});
}

Related

React JS Updating item in State object to take effect immediately

React JS class component
I know there have been many posts on this subject but I can't seem to get this scenario to work.
Basically on my HandleClickSave event I want to update an item in my object in state without affecting the other values and then passing this updated oblect onto my service to get updated in the db.
The 'item' in question is the 'design' from the (unLayer) React-email-editor.
Problem is after the service is run in 'HandleClickSave' point 3 below, the receiving field 'DesignStructure' in the db is NULL every time. The other two fields are fine as these are saved to state object elsewhere.
Part of the problem is that the Email-Editor doesn't have an 'onChange' property which is where I would normally update the state. The other two values in the object are input texts and they do have an onChange which is how their state counterparts are updated.
This is the object 'NewsletterDesign':
{
"DesignId": "1",
"DesignName": "DesignLayout 1 Test",
"DesignStructure": null
}
In my React class component...
this.state = {
NewsletterDesign: {}
}
And the HandleClickSave event....
HandleClickSave () {
const { NewsletterDesign } = this.state
this.editor.saveDesign((design) => {
this.setState(prevState => ({
NewsletterDesign: {
...prevState.NewsletterDesign,
DesignStructure: design
}
}));
// Update to database passing in the object 'NewsletterDesign'. Field 'DesignStructure' in db is null every time, but other two fields are updated.
NewsletterService.UpdateCreateNewsletterDesign(NewsletterDesign)
etc....etc..
React's setState is not update immediately. read more here.
You can simply do it inside setState by
this.setState(prevState => {
const newState = {
NewsletterDesign: {
...prevState.NewsletterDesign,
DesignStructure: design
}
};
NewsletterService.UpdateCreateNewsletterDesign(newState.NewsletterDesign);
return newState;
});
The setState is an async operation. Meaning, that it's not guaranteed that the new state that you have updated will be accessible just after the state is updated. You can read more here
So in such cases, one of the way is to do the required operation first and then use the result at multiple places.
HandleClickSave () {
const { NewsletterDesign } = this.state
this.editor.saveDesign((design) => {
let newNewsletterDesign = { ...NewsletterDesign,
DesignStructure: design
};
this.setState(newNewsletterDesign);
NewsletterService.UpdateCreateNewsletterDesign(newNewsletterDesign)

How can I set State based on other state in functional component?

I build a simple todo app with a react with an array of todos:
const todos = [description: "walk dog", done: false]
I use the following two states:
const [alltodos, handleTodos] = useState(todos);
const [opencount, countOpen] = useState(alltodos.length);
This is the function which counts the open todos:
const countTodos = () => {
const donetodos = alltodos.filter((item) => {
return !item.done;
});
countOpen(donetodos.length);
};
When I try to add a new todo, I also want to update the opencount state with the countTodos function.
const submitTodo = (event) => {
event.preventDefault();
const data = {
description: todo,
done: false,
};
handleTodos([...alltodos, data]);
console.log(alltodos);
countTodos();
};
This does not work as expected, the when I run console.log(alltodos) it will show an empty array. The function itself works, but it seems to have a "delay", I guess based on the async nature of the useState hook.
I tried to pass the countTodos function as callback like this, since I have seen something similar in class based components.
handleTodos([...alltodos, data], () => {
countTodos();
});
I get the following error:
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
How can I solve this problem? What is the best way for me to update the state based on another state?
I think you should useEffect, (clearly stated on the log message ). this is an example :
useEffect(()=>{
const donetodos = alltodos.filter((item) => {
return !item.done;
});
countOpen(donetodos.length);
//countTodos();
},[alltodos]];
You can refer to the documentation : https://reactjs.org/docs/hooks-effect.html
Here is an example : https://codesandbox.io/s/react-hooks-useeffect-forked-4sly8

My mapStateToProps is not called after adding custom object array to redux state

I am trying for few hours but can't figure out why my state is not called after adding an array of custom object.
// In my component...
const myRemoteArray = getRemoteArray() // Is working
props.addAdItems(myRemoteArray) // Calls **1 via component.props
/// ...
const mapDispatchToProps = (dispatch) => {
return {
addAdItems: (items) => { // **1
// Items contains my array of objects
dispatch(addAdItems(items)) // Calls **2
},
}
}
// My action
export const addAdItems = (items) => { // **2
// Items contains my array of objects
return { // Calls **3
type: AD_ITEMS,
adItems: items,
}
}
const productsReducer = (state = initialState, action) => {
switch (action.type) { // **3
case AD_ITEMS:
// Is working!
// action.adItems contains my array!
const _state = {
...state,
adItems: action.adItems, // Here is the issue, I am not sure how to add my NEW array to existing state and update it.
// Like that: ??? "adItems: ...action.adItems" or adItems: [action.adItems]
}
// The new state contains my Array!!!
return _state
default:
return state
}
}
// In my component... !!!!
// THIS IS NOT CALLED or it is called with empty array from initialState!!!
const mapStateToProps = (state) => {
return {
updatedItem: state.changedItem,
adItems: state.adItems,
}
}
It seems to me that Redux is having a problem with my array containing the following data. Has Redux issues with my class methods?
class Ad {
constructor(
id,
isPublished
) {
this.id = id
this.isPublished = isPublished
}
someMessage = () => { return "Help me!" }
needHelp = () => { return true }
}
My Redux is working already with other calls, data, and objects, which means my createStore and all other stuff is correct.
PS: I don't have multiple stores.
UPDATE
Now my mapDispatchToProps is called with current array but is not persisting.
UPDATE 2
If I save my file and force to refresh the App, the props.adItems contains my loaded array, but if I want to access props.adItems at runtime (e.g. on FlatList refresh) it is empty array again!
Why?
Should I store my array in a useState property after it has changes via useEffect?
You were pretty close in the comments you added in the reducer, but neither of them were 100% accurate.
For Redux to notice that your array has changed, you need the property adItems of your new state to return an entirely new array. You can do it like this:
adItems: [...action.adItems]
With this code you'll be creating a new array, and then adding a copy of the items of the old one into it.
The reason why your current implementation (adItems: action.adItems) is not working is that action.adItems is actually a reference to an array in memory. Even though the array contents have changed, the value of action.adItems is still the same, a pointer to where the array is currently stored. This is the reason why your store is not being updated: as Redux does not check the values of the array itself but the reference to where the array is stored, the new state you're returning is exactly the same, so Redux is not aware of any changes.
As LonelyPrincess says, I was making this issue elsewhere, if you doing that xArray = yArra it means call by reference and not by value.

React (16.4.1) setState() updates state one onChange event behind the current onChange event with react-select component

I have 2 react-select components and the behavior I wish to set is that when the user makes their selection from both select components (order does not matter) then an ajax will be triggered to pull the data from the server. The selection from both of these components is required to fully populate the GET parameters for the ajax call.
I have these 2 handlers for the onChange event on the react-select elements:
filterSiteSelect(selection) {
const siteId = selection.id;
const siteName = selection.name;
this.setState({
siteId, siteName
}, this.getTableData());
}
filterLineTypeSelect(selection) {
const lineTypeId = selection.id;
const lineTypeName = selection.name;
this.setState({
lineTypeId, lineTypeName
}, this.getTableData());
}
And my getTableData() method looks like:
getTableData() {
const {
productId, siteId, lineTypeId, stages, tableUrl
} = this.state;
// tableUrl = `p=field&t=view&gapi=1&product_id=${productId}&site_id=${siteId}&line_type_id=${lineTypeId}&stage_ids=${stages}`
if (productId && siteId && lineTypeId && !_.isEmpty(stages)) {
Axios.get(tableUrl)
.then((result) => {
this.setState({
rawData: { ...result.data.data }
});
});
}
}
The behavior I am experiencing is that when the user select's an option from the second select box the ajax call does not fire. The user needs to go back and select something else to get the ajax call to fire and then it uses the first selection they chose.
I also tried to use ComponentDidUpdate() for the ajax call with this code (I removed the getTable() data from each of the setState() calls when I changed to componentDidUpdate(prevState)):
componentDidUpdate(prevState) {
const {
siteId, lineTypeId, stages
} = this.state;
if (lineTypeId !== prevState.lineTypeId && siteId !== prevState.siteId && !_.isEqual(stages, prevState.stages)) {
this.getTableData();
}
}
But what happens when using the componentDidUpdate() lifecycle method it fires the ajax over and over never stoping and I believe that is because the setState() is never updating the state for the last select component the user interacted with.
So I think I'm doing something wrong in my use/understanding of the setState() method (or the issue lies in the react-select component...).
Any insight, assistance, or discussion of what I'm trying to accomplish would be greatly appreciated!
When you pass a second argument to setState(), e.g. setState({foo: bar}, <something>), the <something> is supposed to be a callback function that gets called when setState() has finished updating. In your code, instead of passing your function this.getTableData as an argument, you are passing the expression returned from calling this.getTableData(), in this case undefined.
In your code here:
filterSiteSelect(selection) {
const siteId = selection.id;
const siteName = selection.name;
this.setState({
siteId, siteName
}, this.getTableData());
}
filterLineTypeSelect(selection) {
const lineTypeId = selection.id;
const lineTypeName = selection.name;
this.setState({
lineTypeId, lineTypeName
}, this.getTableData());
}
When you synchronously call setState() and add a state update to the queue, you are also synchronously calling this.getTableData() which runs immediately, checks some booleans in your state variables, maybe tries to do an ajax call, etc.
Try simply removing the () so that you're passing the function directly into setState as an argument instead of accidentally calling the function. :)
filterSiteSelect(selection) {
const siteId = selection.id;
const siteName = selection.name;
this.setState({
siteId, siteName
}, this.getTableData);
}
filterLineTypeSelect(selection) {
const lineTypeId = selection.id;
const lineTypeName = selection.name;
this.setState({
lineTypeId, lineTypeName
}, this.getTableData);
}
setState's second parameter is a callback. That means you should be passing a reference to the function to call, instead of calling the function itself.
this.setState({
siteId, siteName
}, this.getTableData());
Should be
this.setState({
siteId, siteName
}, this.getTableData);

Can I use condition in my action reducer?

Basically, in our case, we need to either get an alerts list that shows the first few items (mounting it first time in the DOM) or show the initial list + the next list (clicking a load more button).
Hence we needed to do this condition in our GET_ALERTS action:
case "GET_ALERTS":
if (action.initialList) {
newState.list = [...newState.list, action.res.data.list];
} else {
newState.list = newState.list.concat(
action.res.data.list
);
}
And when we call the action reducer in our Alerts component, we need to indicate whether initialList is true or false.
E.g.
componentDidMount() {
this.props.getAlerts(pageNum, true);
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum, true);
}
Anyway in such a case, is it fine to use conditional statement in the reducer?
I am against this idea. The reducer has a single responsibility: update Redux state according to the action.
Here are three ways to slove this:
easy way - initialize your list in Redux state to empty list
if you set the list in state to empty list ([]) then it's much simpler.
You can basically just change your reducer to this:
case "GET_ALERTS":
return {...state, list: [...state.list, action.res.data.list]
This will make sure that even if you have get initial list or more items to add to the list, they will be appended. No need to add any logic - which is awesome IMHO.
redux-thunk and separating type into two different types
create two actions: GET_INIT_ALERTS and GET_MORE_ALERTS.
switch(action.type) {
case "GET_INIT_ALERTS":
return {...state, list: action.res.data.list }
case "GET_MORE_ALERTS":
return {...state, list: [...state.list, ...action.res.data.list]}
case "CHECK_READ_ALERTS":
return {...state, read: [...state.read, ...action.res.data.list]}
}
In the component I will have:
componentDidMount() {
this.props.getInitAlerts();
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum);
}
In alerts action with the help of redux-thunk:
export const getAlerts = (pageNum : number) => (dispatch) => {
return apiAction(`/alerts/${pageNum}`, 'GET').then(res => dispatch({type: "GET_MORE_ALERTS", res});
}
export const getInitAlerts = () => (dispatch) => {
return apiAction('/alerts/1', 'GET').then(res => dispatch({type: "GET_INIT_ALERTS", res});
}
I guess you update pageNum after readMore or componentDidMount. Of course you can save that state in Redux and map it back to props and just increment it when calling the getAlerts action.
write your own middleware
Another way to do this is to write an ad-hoc/feature middleware to concat new data to a list.
const concatLists = store => next => action => {
let newAction = action
if (action.type.includes("GET") && action.initialList) {
newAction = {...action, concatList: action.res.data.list}
} else if (action.type.includes("GET") {
newAction = {...action, concatList: [...state[action.key].list, action.res.data.list]}
}
return next(newAction);
}
And change your reducer to simply push concatList to the state:
case "GET_ALERTS":
return {...state, list: action.concatList}
In addition, you will have to change your action to include key (in this case the key will be set to alert (or the name of the key where you store the alert state in redux) and initialList to determine whether to concat or not.
BTW, it's a good practice to put these two under the meta key.
{
type: "GET_ALERT",
meta: {
initialList: true,
key: "alert",
},
res: {...}
}
I hope this helps.
I would suggest you to have following set of actions:
ALERTS/INIT - loads initial list
ALERTS/LOAD_MORE - loads next page and then increments pageNo, so next call will know how many pages are loaded
ALERTS/MARK_ALL_AS_READ - does server call and reinitializes list
The store structure
{
list: [],
currentPage: 0
}
And component code should not track pageNum
componentDidMount() {
this.props.initAlerts();
}
markAllAsRead() {
this.props.markAllAsRead();
}
readMore() {
this.props.loadMore();
}

Categories

Resources