How to write clean, readable ReactJS Components with conditional renders? - javascript

The result is not showing in my window. I have created a global object for testing:
const obj = {
onam: [
{
name: "name3",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg"
},
{
name: "name2",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg"
}
],
christmas: [
{
name: "name1",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg"
},
{
name: "name0",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg"
}
]
}
Below is the function I am calling inside render.
const grid = (props) => {
if (props == "All") {
const keys = Object.keys(obj);
// looping through keys
keys.forEach((key, index) => {
// looping through array in keys
for(let j = 0; j < Object.keys(obj).length; j++) {
return (
<div>
<a><img src={obj[key][j].image}>{obj[key][j].name}</img></a>
</div>
)
}
});
}
}
I know there is some error in the above function but I cant sort it out. I am calling {grid("All")} inside render. My aim is to display a div containing a div with all images with a tag. I would like to learn a clean way of conditionally rendering my components.

There are several concerns here Ashwin.
Your component name should begin with uppercase.
Use an array instead of object obj. Its easier to work with arrays and loop through them than it is to loop objects.
Don't wrap the entire component within an if statement. Its bad practice.
props == "All" is just wrong. props is an object not a string. This is the main reason nothing is rendering. That if statement will always return false.
<img> doesn't take children so don't pass {obj[key][j].name} into the img tag. Move that bit after the closing img tag into the <a> tag.
I'm rewriting your component with some comments to help you learn, but there are several other ways to achieve the same result, perhaps with a better approach. This works for me and I find it's more readable and easier to understand.
import React from "react"
/* An array of objects is always easier to work than objects with
multiple arrays as values. You might end up duplicating a few keys like
"festival" in this example, but it will make your life easier.
*/
const arr = [
{
festival: "oman",
name: "name3",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg",
},
{
festival: "oman",
name: "name2",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg",
},
{
festival: "christmas",
name: "name1",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg",
},
{
festival: "christmas",
name: "name0",
image: "https://resize.hswstatic.com/w_828/gif/kelp-biofuel.jpg",
},
]
/*
Always make sure your components are uppercase
De-structure your props with { propName1, propName2 }
because that way you or anybody else looking at your
component immediately can recognize what props are being
passed.
*/
const Grid = ({ whatToRender }) => {
/*
Create a custom render function inside your component if
you need to render conditionally. Its easier to work with
debug and maintain.
*/
const renderGrid = () => {
if (whatToRender === "all") { // use === over == because that will check for type equality as well.
return arr.map((item) => ( // () is equivalent to return () when looping.
/* Always pass a unique key if you are creating lists or lopping */
<div key={item.image}>
<a>
<img src={item.image} alt={item.name} />
{item.name}
</a>
</div>
))
}
if (whatToRender === "onam") {
return arr.map((item) => {
if (item.festival === "onam") {
return (
<div key={item.image}>
<a>
<img src={item.image} alt={item.name} />
{item.name}
</a>
</div>
)
}
})
}
if (whatToRender === "christmas") {
return arr.map((item) => {
if (item.festival === "christmas") {
return (
<div key={item.image}>
<a> {/* Image tags don't take children they are self-closing */}
<img src={item.image} alt={item.name} />
{item.name}
</a>
</div>
)
}
})
} // Return an empty div if none of the cases pass. So, you return valid JSX
return <div></div>
}
return renderGrid() // Finally return your custom render function
}
export default Grid
EDIT
On visiting this question again, I realized that there was a better way to write it. A much shorter version. It uses the same arr defined in the above code sample.
const Grid = ({ whatToRender }) => {
const renderGrid = () => {
return arr.map((item) => {
if (item.festival === whatToRender || whatToRender === "all") {
return (
<div key={item.image}>
<a>
<img src={item.image} alt={item.name} />
{item.name}
</a>
</div>
)
}
})
}
return renderGrid()
}
export default Grid
Which one to use? The second version not only because it's much shorter, but also because its reusable. If in the future you add another festival to arr say Easter, you just need changes in the array and the prop value you are passing. The component will require no changes.

Related

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

How can I change State when an Event is triggered with useState?

to learn react im trying to implement a basic shop.
My Idea was to have many product-images. If an user clicks on an product-image this image turns around and shows something like comments, rating, etc of the product.
For this question i have 3 js Files:
Container.js (contains everything from the product-cards to navbar etc),
ProductList.js (returns the UL with all the different Products) and ItemCard.js (returns the actual product as LI ).
My Goal is to just invert the backsideVisible value.
I provide an minimal example for better understanding:
Container.js:
function Container() {
const [item, setItem] = useState([{
title: "some product title",
price: "14.99$",
backsideVisible: false
id: 1
}]);
function handleTurn(event, itemId) {
//here i want to change the backsideVisible value
event.preventDefault();
setItem(item.map(item => {
if(item.id === itemId) {
item.backsideVisible = !item.backsideVisible;
}
}))
}
return(
<ProductList items={item} handleTurn={handleTurn}/>
);
}
ProductList.js:
function ProductList(props) {
return(
<ul>
<CardItem items={props.items} handleTurn={props.handleTurn} />
</ul>
);
}
CardItem.js
function CardItem(props) {
return(
{props.items.map(item =>(
<li key={item.id} onClick={event => props.handleTurn(event, item.id)}>
product-image etc...
</li>
))}
);
}
But everytime i try this, ill get an "TypeError: can't access property "id", item is undefined" error.
As soon as i change the handleTurn Method to something like
function handleTurn(event, itemId) {
event.preventDefault();
console.log(itemId);
}
everything works fine and the console displays the id of the clicked item. So for me it seems, that my handleTurn Function has some errors.
Do you guys have any idea where my fault is?
Your help is much appreciated. Thank you.
You're setting item (which should really be called items since it's an array. names matter) to an array of undefined elements, because your map() callback doesn't return anything:
setItem(item.map(item => {
if(item.id === itemId) {
item.backsideVisible = !item.backsideVisible;
}
}))
Either return the updated object:
setItem(item.map(item => {
if(item.id === itemId) {
item.backsideVisible = !item.backsideVisible;
}
return item;
}))
or have the whole expression be a returned object:
setItem(item.map(item => ({
...item,
backsideVisible: item.id === itemId ? !item.backsideVisible : item.backsideVisible
})));

Why we are making copy of reference types in react before we mutate them?

I am new in react world. I have this example code here where with the deletePersonHandler method i am deleting some objects from the array.
class App extends Component {
state = {
persons: [
{ name: "peter", age: 24 },
{ name: "john", age: 25 },
{ name: "jake", age: 30 }
],
showPersons: true
}
deletePersonHandler = index => {
const persons = this.state.persons;
persons.splice(index,1);
this.setState({persons:persons})
}
togglePersonsHandler = () => {
this.setState({ showPersons: !this.state.showPersons })
}
render() {
let persons = null;
if (this.state.showPersons) {
persons = (
<div>
{this.state.persons.map((person, index) => {
return <Person
click={() => this.deletePersonHandler(index)}
name={person.name}
age={person.age}
key={index}
/>
})}
</div>
);
}
return (
<div className="App">
{persons}
<button onClick={this.togglePersonsHandler}>Click to hide/show</button>
</div>
)
}
}
export default App;
and it is working fine.
My question is:
when this works - without making copy in this case on persons
deletePersonHandler = index => {
const persons = this.state.persons;
persons.splice(index,1);
this.setState({persons:persons})
}
why the recommended way is to make the COPY FIRST AND THEN MODIFY THE REFERENCE TYPE ?
deletePersonHandler = index => {
const persons = [...];
persons.splice(index,1);
this.setState({persons:persons})
}
I am explaining this based on my experience with object in JavaScript. I had one publisher and subscriber code where there was an array of object which used to keep track of some message number and their handler like this
let observer = {"8000":[handler1, handler2]};
So when something happens i publish 8000 message and all handlers get executed like this
for(var item in observer[8000]){
//execute handler/
}
. Till here it was working pretty cool. Then I started removing handler when it has been processed. So after removing handler length of array observer[8000] reduced by 1. So in next sequence it could not find next handler which didn't execute(Objects are pass by reference in JavaScript). So to resolve this I had to make a copy of array object before directly modifying this. In short if object has many dependencies then before processing make copy of it then process or if it is used only single place then use in place processing. It depends on situation, there aren't any strict rules to follow like copy then process.

React Nested Map and Filter with conditional render does not work

I spent so much time on fixing this issue, i decided to ask people for help.
so basically what i am trying is to render a component.
but before rendering i have 2 arrays(labels, notes).
and using one of them(labels), i used map function and inside that i used filter function to get only element i want to pass to the component that is rendered.
I think there are some posts similar to this, but slightly different.
and some answers said, "when there is nested map function, the inner map needs to be wrap with tag like fragment" but what i found in other posts did not wrap it...
If anyone could help me clearing my confusion, would you please tell me if it is necessary or not.
Cannot Render Nested Maps In ReactJS <- says wrap with tag
React Nested map/forEach wont work <- does not use tag
here is my code.
let notes = [
{id:1, position:3, content: "apple"},
{id:2, position:2, content: "banana"},
{id:3, position:0, content: "orange"},
]
const labels = ["Discussion questions", "Quotes", "Sensory Ques", "Songs"];
const renderNotes = (notes) => {
return labels.map(label => {
console.log("---" + label + "---")
return (
notes.filter(note => {
// console.log(positionLabels[note.position] + " - " + label);
if (positionLabels[note.position] === label) {
console.log("BINGO");
return (
<CustomComponent
key={note.id}
content={note.content}
/>
)
}
})
)
})
}
and inside the return i am calling this function like below
{
renderNotes(notes)
}
when i was testing some like this
const renderNotes = (notes) => {
return positionLabels.map(label => {
return <div>
{
notes.filter(note => {
return (
<div>
TEST
</div>
)
})
}
</div>
})
}
it did not even work,
I have no idea what the error of objects are not valid as a react child is talking about.
First do filter then map it. Try like below
const renderNotes = (notes) => {
return labels.map(label => {
console.log("---" + label + "---")
return (
notes.filter(note => positionLabels[note.position] === label).map((value)=> {
console.log("BINGO");
return (
<CustomComponent
key={value.id}
content={value.content}
/>
)
})
)
})
}

setState long execution when managing large amount of records

I am trying to solve a problem that happens in react app. In one of the views (components) i have a management tools that operate on big data. Basically when view loads i have componentDidMount that triggers ajax fetch that downloads array populated by around 50.000 records. Each array row is an object that has 8-10 key-value pairs.
import React, { Component } from "react";
import { List } from "react-virtualized";
import Select from "react-select";
class Market extends Component {
state = {
sports: [], // ~ 100 items
settlements: [], // ~ 50k items
selected: {
sport: null,
settlement: null
}
};
componentDidMount() {
this.getSports();
this.getSettlements();
}
getSports = async () => {
let response = await Ajax.get(API.sports);
if (response === undefined) {
return false;
}
this.setState({ sports: response.data });
};
getSettlements = async () => {
let response = await Ajax.get(API.settlements);
if (response === undefined) {
return false;
}
this.setState({ settlements: response.data });
};
save = (key, option) => {
let selected = { ...this.state.selected };
selected[key] = option;
this.setState({ selected });
};
virtualizedMenu = props => {
const rows = props.children;
const rowRenderer = ({ key, index, isScrolling, isVisible, style }) => (
<div key={key} style={style}>
{rows[index]}
</div>
);
return (
<List
style={{ width: "100%" }}
width={300}
height={300}
rowHeight={30}
rowCount={rows.length || 1}
rowRenderer={rowRenderer}
/>
);
};
render() {
const MenuList = this.virtualizedMenu;
return (
<div>
<Select
value={this.state.selected.sport}
options={this.state.sports.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("sport", option)}
/>
<Select
components={{ MenuList }}
value={this.state.selected.settlement}
options={this.state.settlements.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("settlement", option)}
/>
</div>
);
}
}
The problem i am experiencing is that after that big data is downloaded and saved to view state, even if i want to update value using select that has ~100 records it takes few seconds to do so. For example imagine that smallData is array of 100 items just { id: n, name: 'xyz' } and selectedFromSmallData is just single item from data array, selected with html select.
making a selection before big data loads takes few ms, but after data is loaded and saved to state it suddenly takes 2-4 seconds.
What would possibly help to solve that problem (unfortunately i cannot paginate that data, its not anything i have access to).
.map() creates a new array on every render. To avoid that you have three options:
store state.sports and state.settlements already prepared for Select
every time you change state.sports or state.settlements also change state.sportsOptions or state.settlementsOptions
use componentDidUpdate to update state.*Options:
The third option might be easier to implement. But it will trigger an additional rerender:
componentDidUpdate(prevProps, prevState) {
if (prevState.sports !== this.state.sports) {
this.setState(oldState => ({sportsOptions: oldState.sports.map(...)}));
}
...
}
Your onChange handlers are recreated every render and may trigger unnecessary rerendering of Select. Create two separate methods to avoid that:
saveSports = option => this.save("sport", option)
...
render() {
...
<Select onChange={this.saveSports}/>
...
}
You have similar problem with components={{ MenuList }}. Move this to the state or to the constructor so {MenuList} object is created only once. You should end up with something like this:
<Select
components={this.MenuList}
value={this.state.selected.settlement}
options={this.state.settlementsOptions}
onChange={this.saveSettlements}
/>
If this doesn't help consider using the default select and use a PureComponent to render its options. Or try to use custom PureComponents to render parts of the Select.
Also check React-select is slow when you have more than 1000 items
The size of the array shouldn't be a problem, because only the reference is stored in the state object, and react doesn't do any deep equality on state.
Maybe your render or componentDidUpdate iterates over this big array and that causes the problem.
Try to profile your app if this doesn't help.

Categories

Resources