How to properly update the screen based on state variables - reactjs

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.

Related

NextJS React, nested array in UseState not rendering on update inside of javascript

I have a hard time trying to understand "useState" and when a render is triggered. I want to make a drag and drop system where the user can drag elements from one box to another.
I have successfully done this by using pure javascript, but as usual, it gets messy fast and I want to keep it clean. I thought since I'm using react I should do it using UseState, and I've got the array to update the way I want it to but the changes don't render.
Shouldn't I use useState in this way? What should I use instead? I don't want to solve it the "hacky" way, I want it to be proper.
const [allComponents, updateArray] = useState([])
function arrayMove(array, closest) {
let moveChild = array[parentIndex].children[childIndex]
array[parentIndex].children = array[parentIndex].children.filter(function(value, index, arr){ return index != childIndex;});
array[closest].children = [...array[closest].children, moveChild];
return array;
}
var lastClosest = {"parent": null, "child": null};
var parentIndex = null;
var childIndex = null;
function allowDrop(ev, index) {
ev.preventDefault();
if(allComponents.length > 0) {
let closest = index;
if((parentIndex == lastClosest.parent && childIndex == lastClosest.child) || (closest == parentIndex)) {
return;
}
lastClosest.parent = parentIndex;
lastClosest.child = childIndex;
updateArray(prevItems => (arrayMove(prevItems, closest)));
}
}
function dragStart(pI, cI, ev) {
parentIndex = pI;
childIndex = cI;
}
function drop(ev) {
ev.preventDefault();
}
function addNewSpace() {
updateArray(prevItems => [...prevItems, {"name": "blaab", "children": [{"child": "sdaasd", "type": "text"}]}]);
}
return (
<div className={styles.container}>
<button onClick={addNewSpace}>Add</button>
<div className={styles.spacesWrapper}>
{allComponents.map(({name, children}, index) => (
<div key={"spacer_" + index} onDragOver={(event) => allowDrop(event, index)} onDrop={(event) => drop(event)} className={styles.space}>
{children.map(({type, child}, index2) => (
<div id ={"movable_" + index + ":" + index2} key={"movable_" + index2} className={styles.moveableOne}>
<div key={"draggable_" + index2} draggable="true" onDrag={(event) => onDrag(event)} onDragStart={(event) => dragStart(index, index2, event)}>
</div>
</div>
))}
</div>
))}
</div>
</div>
)
}
Here is the problem I'm experiencing with the nested array not updating properly:
A react page re-renders every time the state changes. So every time a setState is called, the react component re-renders itself and updates the page to reflect the changes.
You can also see how the state is updating if you install the react dev tools chrome extension.
Try to modify your code with the below code and see if it solves your issue,
import React, { useState, useEffect } from "react";
let newAllComponents = [...allComponents];
useEffect(() =>
{
newAllComponents = [...allComponents]
}, [allComponents]);
/* div inside return*/
{newAllComponents.map(({ name, children }, index) => (
<div
key={"spacer_" + index}
onDragOver={(event) => allowDrop(event, index)}
onDrop={(event) => drop(event)}
>

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

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

React Cannot read property 'getAttribute' of undefined

I get the following error when trying to compile my app TypeError: Cannot read property 'getAttribute' of undefined.
I'm having trouble tracking down why in this line: if (event.target.getAttribute('name') && !sectionsClicked.includes(event.target.getAttribute('name'))) { is error
Here is the main react component
class App extends Component {
constructor(props) {
super(props);
this.state = {
progressValue: 0,
sectionsClicked: [],
};
this.handleProgress = this.handleProgress.bind(this);
}
handleProgress = (event) => {
const { progressValue } = this.state;
console.log(progressValue);
const { sectionsClicked } = this.state;
if (event.target.getAttribute('name') && !sectionsClicked.includes(event.target.getAttribute('name'))) {
this.setState({
progressValue: Math.floor((sectionsClicked.length / 77) * 100),
sectionsClicked: sectionsClicked.push(event.target.getAttribute('name')),
});
console.log(sectionsClicked);
}
};
render() {
const { questions } = this.props;
const { progressValue } = this.state;
const groupByList = groupBy(questions.questions, 'type');
const objectToArray = Object.entries(groupByList);
return (
<>
<Progress value={progressValue} />
<div className="text-center mt-5">
<ul>
{questionListItem && questionListItem.length > 0 ?
(
<Wizard
onChange={(event) => this.handleProgress(event)}
initialValues={{ employed: true }}
onSubmit={() => {
window.alert('Hello');
}}
>
{questionListItem}
</Wizard>
) : null
}
</ul>
</div>
</>
);
}
}
As per the docs:
https://github.com/final-form/react-final-form#onchange-formstate-formstate--void
The expected param passed to onChange is formState not event. Try logging the parameter passed to the function it might have the changes you need.
The <FormSpy> component that originally calls onChange in <Wizard>, which you report upwards to <App>, passes a parameter that is not an event. It's of type FormState.
On that type, the target property is undefined, thus the error you're seeing.
If you see the properties of that object in the documentation, you'll probably get what you want by checking the dirty fields of the form, and finding the associated DOM elements.
In all likelihood, though, you can find a way of achieving the desired behavior without resorting to DOM attributes at all. The very same dirtyFields property has the names you might be looking for.
Also, take a look at your methods and binding. You're actually using three separate approaches to fix the context of your handleProgress function.
For more information, look at this answer:
Can you explain the differences between all those ways of passing function to a component?

Test for displaying button only when conditions are true

I have a render function as shown below:
private render(): JSX.Element {
return (
<div>
{this.props.x && this.state.y &&
<DefaultButton onClick = { this.onDeleteButtonClick } >
Delete
</DefaultButton>
}
</div>
);
}
Only when the 2 conditions this.props.x and this.state.y are true, then display the button.
Similarly,
class TitleContainer extends React.Component {
private isPageEnabled: boolean = false;
private isSubmitter: boolean = false;
render() {
return (
<div>
{this.isPageEnabled && this.isSubmitter &&
<div>
<br />
<SubmitAppFormContainer />
</div>
}
</div>
);
}
}
Only when the 2 conditions this.isPageEnabled && this.isSubmitter are true, then display SubmitAppFormContainer.
How do I write a test for the same? Please let me know. Thanks!
I assume that you know how to write tests in general. The tricky part here is to write classes, that are easily testable. In your first case it could be simple:
it('shows DefaultButton', () => {
const shallowRenderer = new ShallowRenderer();
shallowRenderer.render(<Component
x={true}
/>);
const result = shallowRenderer.getRenderOutput();
expect(result).toMatchSnapshot();
});
Create 2 tests, one time passing x = true, the other one passing x = false, so you test both cases. You just have to somehow set the y-state to true.
In your second case TitleContainer it seems more complicated. How can the values of isPageEnabled and isSubmitter be modified? If it's not possible from the outside, it can not be tested properly. Use props if possible.

Resources