Retain state data on re render in reactjs - reactjs

I cannot share the whole code as it is very complex.
The app is basically a todo list task in which we can add main task and subtasks corresponding to main tasks
Structure is like this
Main task 1
1.
2.
3.
Now i maintain an index variable in my json structure and when send my data to the child component as
this.state.Todo[index].items
Now the problem i am facing is
when i add the first task ie this.state.Todo[0].items and its subtasks then it works fine
But if i add second task this.state.Todo1.items then child componen only gets data for second index and subtasks of first child that i previously added disappears from the page.
Main task 1
//Disappears
Main task 2
1.
2.
3.
I am not able to figure it out how to solve this issue
UPDATE:
here is my json
{
Todo:[
{name:"primary",
items:[
{
item:'',
isDone:false,
cost:0}
]
}
],
filter:[
{
keyword:'',
Status:"SHOW_ALL"
}
],
selectedCatelog:"0"};
**starting Component todoAdd.js**
var React=require('react');
var ReactDOM=require('react-dom');
import TodoBanner from './TodoComponents/TodoBanner.js';
import TodoForm from './TodoComponents/TodoForm.js';
import TodoList from './TodoComponents/TodoList.js';
import TodoCost from './TodoComponents/TodoCost.js';
var TodoApp = React.createClass({
getInitialState : function(){
return {Todo:[{name:"primary",items:[{item:'',isDone:false,cost:0}
]}],filter:[{keyword:'',Status:"SHOW_ALL"}],selectedCatelog:"0"};
},
updateItems: function(newItem){
var item = {item:newItem.item,isDone:false,cost:newItem.cost};
var newtodo = this.state.Todo;
var allItems = this.state.Todo[this.state.selectedCatelog].items.concat([item]);
newtodo[this.state.selectedCatelog].items = allItems;
this.setState({
Todo: newtodo
});
},
deleteItem : function(index){
var newtodo = this.state.Todo;
var allItems = this.state.Todo[this.state.selectedCatelog].items.slice(); //copy array
allItems.splice(index, 1); //remove element
newtodo[this.state.selectedCatelog].items = allItems;
this.setState({
Todo: newtodo
});
},
filterItem : function(e){
this.state.filter[0].Status = e.target.value;
this.setState({
filter: this.state.filter
});
},
searchItem : function(e){
this.state.filter[0].keyword = e.target.value;
this.setState({
filter: this.state.filter
});
},
AddCatalog: function(newCatalog){
var Catalog = {name:newCatalog,items:[]};
var newtodo = this.state.Todo.concat([Catalog]);
this.setState({
Todo: newtodo
});
},
setID:function(index)
{
if(index-this.state.selectedCatelog===1)
{
this.setState({
selectedCatelog: index
});
}
},
setSelectedCatalog: function(index){
},
setIndexItem:function(index)
{
this.setState({
selectedCatelog: index
});
},
render: function(){
console.log(JSON.stringify(this.state.Todo))
const AppBarExampleIcon = () => (
<AppBar title="Module Cost Estimation" />
);
return (
<div>
<div>
<MuiThemeProvider>
<AppBarExampleIcon />
</MuiThemeProvider>
<TodoCost cost={this.state.Todo}/>
<ToDoCatalogForm onFormSubmit = {this.AddCatalog} />
<ToDoCatelog setItemId={this.setIndexItem} set={this.updateItems} onSelectDemo={this.setID} selectedID = {this.state.selectedCatelog} onSelected={this.setSelectedCatalog} Todos = {this.state.Todo} />
</div>
<div>
<TodoList ListIndex={this.state.selectedCatelog} items = {this.state.Todo} filter = {this.state.filter} onDelete={this.deleteItem}/>
<ToDoFilter onFilter = {this.filterItem} onSearch = {this.searchItem} filter={this.state.filter}/>
</div>
</div>
);
}
});
ReactDOM.render(<TodoApp />, document.getElementById("app"));
When i Add a task its name gets added in name property of Json and when i enter subtasks then subtasks are populated in items property corressponding to the name and i am updating the index using selectedCatelog.Whenever a new task is added selectedCatelog is updated in setIndexItem method and then i pass entire json to TodoList Component
Here is the screenshot before adding second main task
Here is the screenshot after adding second main task

For the few information you give us it looks like the problem is updating the state.
When you update a member of the state you overwrite that member with the new one, so you have to take the information you had before and add the new one.
Besides, you can't directly mutate the state like this.state.Todo.push(newTodo), so being Todo an array you should use a method like concat to add a new element as it returns a new Array.
const { Todo } = this.state;
let newTodo = {"name":'sec',"items":[2,4,5,7]}
this.setState({ Todo:Todo.concat([newTodo])});

Related

autosuggest not showing item immediately

I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.

How to update the whole app due to specific event?

I build a 3D product configurator and a problem appeared. I am pretty new to React wherefore I can't figure out how to solve it.
My app structure is as follows:
App.js
- Scene3D
- Options
- Scene3D
- CheckoutBar
All changes can be made by the user by clicking different buttons in the option section. One of them is to change the product model. Each product model provides different options. All options are stored in an object in App.js.
My thought was to create an event "change-model" which triggers the render function of App.js again with the choosen model. Unfortunately this doesn't work. The option section won't be updated. The new option data won't get passed to all component underlying App.js.
How is it possible to solve that?
I appreciate your help. Thank you!
App.js
constructor(props) {
super(props)
this.state = {
configurations: {
standard: require('./configurations/standard.json'),
second: require('./configurations/standard.json'),
third: require('./configurations/third.json')
}
}
}
componentDidMount() {
window.addEventListener('change-model', this.render)
this.loadScripts()
}
render = e => {
return (
<div className="proconf">
<Scene3d defaultConfig={this.getDefaultConfig()} />
<Controls configuration={this.state.configurations[e && e.detail.model ? e.detail.model : "standard"]} />
<CheckoutBar defaultConfig={this.getDefaultConfig()} prices={this.getPrices()} />
</div>
)
}
Controls.js
emitModelChangeEvent(modelName) {
let event = new CustomEvent('change-model', { detail: {
model: modelName
}})
window.dispatchEvent(event)
}
createOptions = (options) => {
let optionStorage = []
for (var x in options) {
this.emitModelChangeEvent(value)
this.setState(prev => ({
productConfig: {
...prev.productConfig,
model: value
}
}))
let buttonStorage = []
for (var y in options[x].values) {
buttonStorage.push(<div onClick={(e) => { e.preventDefault(); emitChangeEvent(this, valueKey) }}>{valueInfo}</div>)
}
optionStorage.push(<div>{buttonStorage}</div>)
return optionStorage
}
render() {
return (
<div>
<button>
{this.props.stepName}
</button>
<div>
{this.createOptions(this.props.options)}
</div>
</div>
)
}

React Native Firebase Ref() and Navigation complication

I am using firebase along with React navigation for this app.
For a component of mine, I am able to retrieve data from my firebase realtime database to populate an array of components. This is done in my componentDidMount() function.
If, however, I navigate to another page, and then navigate back (the goBack() function is not manually called), no items are rendered. By using console logs I was able to figure out the following:
-componentDidMount() IS being called again when I revisit the page.
-I am able to enter into the function specified under:
Ref.on('value', (snap) => { .... });
as I am able to call console logs from within there.
-states are reset back to what the constructor assigns, as loading is set to true again.
It seems like I am simply unable to retrieve data from firebase after I call the ref() the first time. The following is the important aspects of my code.
export default class RestaurantPage extends Component<Props> {
constructor(props){
super(props);
this.state = {
showSearch:false,
loading:true,
index:0,
data: []
}
this.rootRef = firebaseApp.database().ref();
this.cards = [];
}
componentDidMount(){
var Ref = firebaseApp.database().ref("Restaurants");
Ref.on('value', (snap) => {
var restaurants = snap.val();
this.setState({data: Object.keys(restaurants)});
console.log(this.state.data);
var day = moment().format('dddd');
for(var i = 0; i < this.state.data.length; i++){
var newSnap = snap.child(this.state.data[i]);
var id = newSnap.val()
var name = newSnap.child('Name').val();
var cuisine = newSnap.child('Cuisine').val();
var price = newSnap.child('Price').val();
var address = newSnap.child('Address').val();
var closing = newSnap.child('Closing/' + day).val();
var description = newSnap.child('Description').val();
this.cards.push(
<RestaurantCard key = {i}
id = {id}
title = {name}
cost = {price}
cuisine = {cuisine}
tags = {756}
closing ={closing}
distance = {address}
description = {description}/>);
}
this.setState({loading:false});
});
}
renderCards = () => {
console.log("rendercards called");
return(
{this.cards}
);
}
Does anyone have insight on this?
Side Note: No problems occur if I simply am going to another page and then use the goBack() function, as I believe all states are retained anyway in that case.
Additionally: I am using this.props.navigation.navigate(), rather than push().
ALSO: Something that might be quite significant: The issue occurs when I navigate from one page (Feed) to the page I am having issues with (RestaurantPage). Then, I navigate back to (Feed) using my navBar, and then navigate back to (RestaurantPage), at which point I see that the firebase ref() is not working.
Stack Navigator:
export default createStackNavigator(
{
RestaurantProfile: RestaurantProfile,
RestaurantPage: RestaurantPage,
SearchPage: SearchPage,
SearchResults: SearchResults,
Feed: Feed,
OtherUserProfile: OtherUserProfile,
OtherUserTags: OtherUserTags,
PersonalTags: PersonalTags
},
{
headerMode:'none',
initialRouteName: 'Feed',
transitionConfig: () => ({ screenInterpolator: () => null })
},
);
The problem was that I did not call off() on the firebase ref after calling on()!
The solution is to call Ref.off() in componentDidUnmount or simply use a Ref.once() function rather than Ref.on()

How to update state of specific object nested in an array

I have an array of objects. I want my function clicked() to add a new parameter to my object (visible: false). I'm not sure how to tell react to update my state for a specific key without re-creating the entire array of objects.
First of all, is there an efficient way to do this (i.e using the spread operator)?
And second of all, perhaps my entire structure is off. I just want to click my element, then have it receive a prop indicating that it should no longer be visible. Can someone please suggest an alternative approach, if needed?
import React, { Component } from 'react';
import { DefaultButton, CompoundButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import OilSite from './components/oilsite';
import './index.css';
class App extends Component {
constructor(props){
super(props);
this.state = {
mySites: [
{
text: "Oil Site 1",
secondaryText:"Fracking",
key: 3
},
{
text: "Oil Site 2",
secondaryText:"Fracking",
key: 88
},
{
text: "Oil Site 3",
secondaryText:"Fracking",
key: 12
},
{
text: "Oil Site 4",
secondaryText:"Fracking",
key: 9
}
],
}
};
clicked = (key) => {
// HOW DO I DO THIS?
}
render = () => (
<div className="wraper">
<div className="oilsites">
{this.state.mySites.map((x)=>(
<OilSite {...x} onClick={()=>this.clicked(x.key)}/>
))}
</div>
</div>
)
};
export default App;
Like this:
clicked = (key) => {
this.state(prevState => {
// find index of element
const indexOfElement = prevState.mySites.findIndex(s => s.key === key);
if(indexOfElement > -1) {
// if element exists copy the array...
const sitesCopy = [...prevState.mySites];
// ...and update the object
sitesCopy[indexOfElement].visible = false;
return { mySites: sitesCopy }
}
// there was no element with a given key so we don't update anything
})
}
You can use the index of the array to do a O(1) (No iteration needed) lookup, get the site from the array, add the property to the object, update the array and then set the state with the array. Remeber, map has 3 parameters that can be used (value, index, array).
UPDATE: Fixed Some Typos
class Site
{
constructor(text, scdText, key, visible=true)
{
this.text = text;
this.secondaryText = scdText;
this.key = key;
this.isVisible = visible;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
mySites: [
new Site("Oil Site 1", "Fracking", 3),
new Site("Oil Site 2", "Fracking", 88),
new Site("Oil Site 3", "Fracking", 12),
new Site("Oil Site 4", "Fracking", 9)
],
}
this.clicked = this.clicked.bind(this);
};
//Change to a normal function
clicked(ind)
{
//Get the sites from state
let stateCopy = {...this.state}
let {mySites} = stateCopy;
let oilSite = mySites[ind]; //Get site by index
//Add property to site
oilSite.isVisible = false;
mySites[ind] = oilSite;//update array
//Update the state
this.setState(stateCopy);
}
render = () => (
<div className="wraper">
<div className="oilsites">
{this.state.mySites.map((site, ind) => (
//Add another parameter to map, index
<OilSite {...site} onClick={() => this.clicked(ind)} />
))}
</div>
</div>
)
};
I'm not sure how to tell react to update my state for a specific key without re-creating the entire array of objects.
The idea in react is to return a new state object instead of mutating old one.
From react docs on setstate,
prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props
You can use map and return a new array.
clicked = (key) => {
this.setState({
mySites: this.state.mySites.map(val=>{
return val.key === key ? {...val, visibility: false} : val
})
})
}

Filtering an icon from an array of icon strings for re-render

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Resources