How to update the whole app due to specific event? - reactjs

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

Related

How to properly update the screen based on state variables

I'm new to react and I'm learning how to fetch data from an api once the user clicks on a button. Somehow, I've gotten everything to work, but I don't think I'm using the library properly.
What I've come up with:
class App extends React.Component {
state = {
recipe: null,
ingredients: null
}
processIngredients(data) {
const prerequisites = [];
const randomMeal = data.meals[0];
for(var i = 1; i <= 20; i++){
if(randomMeal['strIngredient' + i]){
prerequisites.push({
name: randomMeal['strIngredient' + i],
amount: randomMeal['strMeasure' + i]
})
}
}
this.setState({
recipe: data,
ingredients: prerequisites,
})
console.log(prerequisites[0].name)
}
getRecipes = () => {
axios.get("https://www.themealdb.com/api/json/v1/1/random.php").then(
(response) => {
this.processIngredients(response.data);
}
)
}
render() {
return (
<div className="App">
<h1>Feeling hungry?</h1>
<h2>Get a meal by clicking below</h2>
<button className="button" onClick={this.getRecipes}>Click me!</button>
{this.state.recipe ? <Recipe food={this.state.recipe}
materials={this.state.ingredients} /> : <div/>}
</div>
);
}
}
The way I'm checking the value of state.recipe in render() and invoking the Recipe component, is it correct? Or does it seem like hacked together code? If I'm doing it wrong, what is the proper way of doing it?
It's really a minor nit, but in this case you can use an inline && logical operator since there's nothing to render for the "false" case:
{this.state.recipe && <Recipe food={this.state.recipe} materials={this.state.ingredients} />}
Checkout https://reactjs.org/docs/conditional-rendering.html for more info.

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 place return code in a function: React

I currently have a react project I'm working on. My render method looks like this going into my return method:
render() {
let elements = [];
this.dropdownCounter().forEach(item => {
if(item != "attributeProduct") {
console.log('setting');
elements.push(
<Dropdown
title={this.state[item][0]['title']}
arrayId={item}
list={this.state[item]}
resetThenSet={this.resetThenSet}
/>
);
}
});
this.state.attributeProduct.map(attributeItem => {
elements.push(
<Dropdown
title={attributeItem.name}
arrayId='attributeMetaProduct'
list={
this.state.attributeMetaProduct.filter(metaItem => metaItem.attribute_id == attributeItem.ID)
}
resetThenSet={this.resetThenSet}
/>
);
});
return (
I have a lot of code going on in the render area due to different drop downs dependent on other methods. Is there a way that I can do something like this instead?
render() {
allMyPrereturnStuff()
return()
}
Then just place all this code in allMyPrereturnStuff()? I've tried creating this function and passing everything there but it doesn't work due to all the "this". Any ideas?
Yes, you can easily drop in normal javascript expressions into JSX:
return (
<div>
{this.renderStuff()}
{this.renderOtherStuff()}
{this.renderMoreStuff()}
</div>
);
You can even base it on flags:
const shouldRenderMoreStuff = this.shouldRenderMoreStuff();
return (
<div>
{this.renderStuff()}
{this.renderOtherStuff()}
{shouldRenderMoreStuff ? this.renderMoreStuff() : null}
</div>
);
Do note that it is often an anti-pattern to have render* methods in your components other than the normal render method. Instead, each render* method should probably be its own component.
Don't forget to bind your allMyPrereturnStuff() method in the constructor so "this" will work inside it.
constructor(props) {
super(props);
// ... your existing code
this.allMyPrereturnStuff = this.allMyPrereturnStuff.bind(this);
}
allMyPrereturnStuff = (params) => {
// ... all the code
}
However, you might want to consider breaking out the code to components, which is more Reacty way to do things.
For example, you could refactor this
this.state.attributeProduct.map(attributeItem => {
elements.push(<Dropdown
title={attributeItem.name}
arrayId='attributeMetaProduct'
list={
this.state.attributeMetaProduct.filter(metaItem => metaItem.attribute_id == attributeItem.ID)
}
resetThenSet={this.resetThenSet}
/>);
});
To something like (somewhat pseudocody):
const DropdownList = (props) => {
return (<Dropdown
title={props.attributeItem.name}
arrayId='attributeMetaProduct'
list={props.list}
resetThenSet={props.resetThenSet}
/>);
}
And in the original component's render function, have something like
render() {
return (this.state.attributeProduct.map(attributeItem => {
<DropdownList attributeItem={attributeItem}
list={ this.state.attributeMetaProduct.filter(metaItem => metaItem.attribute_id == attributeItem.ID) }
resetThenSet={this.resetThenSet}
/>);
}

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

React function not working from child component

I am trying to get a function working which removes an image uploaded using React Dropzone and react-sortable.
I have the dropzone working, and the sort working, but for some reason the function I have on the sortable item which removes that particular item from the array does not work.
The onClick event does not seem to call the function.
My code is below.
const SortableItem = SortableElement(({value, sortIndex, onRemove}) =>
<li>{value.name} <a onClick={() => onRemove(sortIndex)}>Remove {value.name}</a></li>
);
const SortableList = SortableContainer(({items, onRemove}) => {
return (
<ul>
{items.map((image, index) => (
<SortableItem key={`item-${index}`} index={index} value={image} sortIndex={index} onRemove={onRemove} />
))}
</ul>
);
});
class renderDropzoneInput extends React.Component {
constructor (props) {
super(props)
this.state = { files: [] }
this.handleDrop = this.handleDrop.bind(this)
}
handleDrop (files) {
this.setState({
files
});
this.props.input.onChange(files)
}
remove (index){
var array = this.state.files
array.splice(index, 1)
this.setState({files: array })
this.props.input.onChange(array)
}
onSortEnd = ({oldIndex, newIndex}) => {
this.setState({
files: arrayMove(this.state.files, oldIndex, newIndex),
});
};
render () {
const {
input, placeholder,
meta: {touched, error}
} = this.props
return (
<div>
<Dropzone
{...input}
name={input.name}
onDrop={this.handleDrop}
>
<div>Drop your images here or click to open file picker</div>
</Dropzone>
{touched && error && <span>{error}</span>}
<SortableList items={this.state.files} onSortEnd={this.onSortEnd} onRemove={(index) => this.remove(index)} />
</div>
);
}
}
export default renderDropzoneInput
Update: This was caused by react-sortable-hoc swallowing click events. Setting a pressDelay prop on the element allowed the click function to fire.
This is old question, but some people, like me, who still see this issue, might want to read this: https://github.com/clauderic/react-sortable-hoc/issues/111#issuecomment-272746004
Issue is that sortable-hoc swallows onClick events as Matt found out. But we can have workarounds by setting pressDelay or distance.
For me the best option was to set minimum distance for sortable list and it worked nicely
You can also use the distance prop to set a minimum distance to be dragged before sorting is triggered (for instance, you could set a distance of 1px like so: distance={1})

Resources