Reset state after doing something without re-rendering - javascript

I wanna to highlight a new task after it is created. However, the whole table will re-render when a new task is added, so even though I have so many occasions to do something before rendering, it will be reset. So I declare a newTaskID: null state intended store the id and highlight it after rendering which is good. However, I also need to set newTaskID: null back to null which again cause a re-render which will reset my table and my highlighted task. I know I miss something but cannot understand it yet. Please help.
constructor(props) {
this.state = {
newTaskID: null
}
}
createTask = (value) => {
/.....
this.setState({ newTaskID: value.id })
}
componentDidMount() {
gantt.attachEvent('onGanttRender', function () {
if (this.state.newTaskID) {
//Highlight task and set state back to null
highlightTask(this.state.newTaskID)
//Set state back to null so it won't highlight anything
this.setState({ newTaskID: null })
}
})
}

Add an additional flag to the state to check if there is an highlighted task. It would be something like this:
this.state = {
newTaskID: null,
taskIsHighlighted: false,
}
And you should set taskIsHighlighted to true when task was highlighted and set to false in the next render. Your componentDidMount will be:
componentDidMount() {
gantt.attachEvent('onGanttRender', function () {
if (this.state.newTaskID) {
//Highlight task and set state back to null
highlightTask(this.state.newTaskID)
//Set state back to null so it won't highlight anything
this.setState({ taskIsHighlighted: true })
}
if (this.state.taskIsHighlighted) {
this.setState({ newTaskID: null, taskIsHighlighted: false })
}
})
}

Related

componentDidUpdate creates infinite loop and not sure how to avoid it

Hi I am calling componentdidupdate in order to re-render my page with new ToDos like so
this.state = {
displayToDos: false,
displayLanding: true,
todo: {
title: '',
task: ''
},
todos: [],
createdToDo: null,
updateModal: false
}
}
async componentDidMount() {
this.fetchItems()
}
componentDidUpdate(prevState) {
if (prevState.todos !== this.state.todos) {
this.fetchItems()
}
}
fetchItems = async () => {
try {
const todos = await getItems()
this.setState({ todos })
console.log('set state of todos', this.state.todos)
} catch (err) {
console.error(err)
}
}
my intended behavior for my app is that the component mounts fetches the items and sets them to state, and then whenever I make a post, update or delete call the state of todos changes and it fires off the fetch call to re render the todos on the page. However I am getting an infinite loop, how do I fix this?
You can compare arrays like this:
let arraysEqual = JSON.stringify(todo) == JSON.stringify(todo1);
Or I will suggest you use this library Loadsh
you can use the .isEqual method to check the arrays.

React setstate return undefined on componentDidMount()

setState user property inside removeListener return undefined when i console.log() it inside component but when i check state in react developer tool user object from firebase it is there with actual value I want
state = {
coinList: {},
fav: [],
currentFavourite: '',
prices: [],
authed: false,
user: null
};
componentDidMount = () => {
this.removeListener = firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.setState({
authed: true,
user
});
} else {
this.setState({
authed: false
});
}
});
this.ref = base.syncState('fav', {
context: this,
state: 'fav',
asArray: true
});}
If your console.log statement is inside the removeListener, I'd suspect that state hasn't been updated by the time console.log is called.
setState is asynchronous, so it's been updated in the background whilst the next statements are being.
You can provide setState with a function or statement that is only called once setState is completed....
this.setState({ user }, this.someFunction())
or simply...
this.setState({ user }, console.log(this.state.user));

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

Double rendering in React with asynchronous call in componentDidMount causing error

I'm building a blog application that has an articles index page, and from there you can click on an article to see the article or edit the article.
If you're going from the index page to the edit page, it works just fine because I already have all the articles in state. But if I refresh after I've gone to the edit-article page, I no longer have all the articles in state.
This is a problem because I'm making an asynchronous recieveSingleArticle call in the componentDidMount of my edit-article page, then I setState so my form is prepopulated. There's a double render which causes an "Uncaught TypeError: Cannot read property 'title' of undefined" error, presumably during the first render before the article has been received into state.
class ArticleEdit extends React.Component {
constructor(props) {
super(props);
this.state = {title: "", body: "", imageFile: ""};
this.handleChange = this.handleChange.bind(this);
this.handlePublish = this.handlePublish.bind(this);
this.handleFile = this.handleFile.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
componentDidMount() {
const { article, requestSingleArticle } = this.props;
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}
...
I tried wrapping my async calls inside of an "if (this.props.article)" but that didn't work. Is there a best way of dealing with this type of problem? Any advice greatly appreciated!
UPDATE:
Another solution that works is to have a componentDidUpdate in addition to componentDidMount. check in componentDidMount if this.props.article exists and if so, setState. And in componentDidUpdate, wrap the setState in the following conditional:
if (!prevProps.article && this.props.article)
Just check if the article is present in the props before calling async action
componentDidMount() {
const { article, requestSingleArticle } = this.props;
if (!(article && requestSingleArticle)) return; // this line
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}
Since you are not getting any render from this method , it means that the props are not yet obtained in the life cycle method componnetDidMount. So instead you can use componentWillReceiveProps like this
componentWillReceiveProps(nextProp) {
// this line here will check the article props' status so that
// we will not use setState each time we get a prop
if (this.props.article === nextProp.article) return;
// rest is just the same code from above
const { article, requestSingleArticle } = nextProp;
if (!(article && requestSingleArticle)) return; // this line
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}

prevState not consistent

I am trying to add thumbnail URL into my thumbnail state array. But also to prevent an infinite loop I am trying to compare the previous state from the current state. However, when doing so the previous state keeps referring back to the state object? The array that I need is the one that shows Explore line 35. And ideally I thought that would be the current State, since that is what it is printing.
Constructor
constructor(props) {
super(props);
this.props.getVideos();
this.state = {
thumbnail: []
};
}
ComponentDidUpdate
componentDidUpdate(prevState) {
console.log("Current State->" + this.state.thumbnail);
console.log("Previous State->" + JSON.stringify(prevState.videos));
// console.log("Previous State->" + prevState.videos.video);
if (this.props.videos == null) {
console.log("It's null");
} else {
if (this.state !== prevState) {
const videos = this.props.videos.video.map(video => {
this.setState(prevState => ({
thumbnail: prevState.thumbnail.concat(video.thumbnail)
}));
console.log(this.state.thumbnail);
});
}
}
}
Reducers snippet
case GET_VIDEOS:
return {
...state,
videos: action.payload,
loading: false
};
Few issues that I can see right away:
if (this.state !== prevState) is not a valid way of comparing the old state to the new. They are just comparing their references as variables (which will always be different) - not their contents. To do this properly you need to compare each state variable one-by-one. Either use a library for that, such as lodash's _isEqual or implement your own solution (good idea if your state is small).
Doing console.log(this.state.thumbnail); right after your setState() will actually print the previous value because setState() is asynchronous. Instead, put your log inside the callback parameter of setState():
this.setState(prevState => ({
thumbnail: prevState.thumbnail.concat(video.thumbnail)
}), () => console.log(this.state.thumbnail));

Categories

Resources