Currently I've got a react component that looks like this:
const GeraCard = (cards, cart = false) => {
return cards.map((v, i) => {
return (
<div key={i} className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div>
);
});
};
Based on the cards array, it renders something like this:
Rendered Component
Whenever I click the trash button, I make a request to my backend, edit the list on my database and rerender the component based on the now updated "cards". The problem is that this takes sometime to happen, so i wanted a way to remove it from the dom instantly while my backend does it's job.
somehting like
{show ? renderCompoennt : null}
I've tried using vanilla javascript to grab the parent from the trash can, which would be the card i want to remove, but the results are unpredictable and it's quite slow as well.
My latest try was this:
const GeraCard = (cards, cart = false) => {
return cards.map((v, i) => {
const [show, setShow] = useState(true);
return (
<div key={i}>
{show ?
<div className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
setShow(false);
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div> :
null
}
</div>
);
});
};
but react won't let me do this. Even tho its fast, everytime one item gets deleted, react complains that "less hooks were rendered" and crashes the app.
You are attempting to do some Optimistic UI, in which you assume that your action will succeed, and reflect the expected/assumed state instantly, before the request to the backend completes. This would be in lieu of showing some progress/busy indicator, like a spinner, until the action completes with the server.
The first problem and immediate problem in your code-- it violates the rules of hooks, which state that hooks may only be used at the top-level (never inside loops, conditionals, etc).
The second problem is that you are leveraging vanilla JS to manipulate the DOM directly; this generally an antipattern in MV* frameworks, and very much so here. Instead, I would suggest doing managing it in your data model; something like this:
Rewrite your .map handler to return null if the card has a deleted property.
When the user clicks the trash button, do two things:
Make the request to the backend to delete it
Use a setState to add a deleted: true property to the clicked card
Now you will get a rerender that will omit the deleted card, and also make the request to the backend, all inside the React data model. Make sure that you handle complexity for:
How to handle the response
How to handle an error if the deletion fails at the backend
How to manage if a user quickly clicks many cards for deletion before any of the requests can complete.
The problem is that in the first render you have {cards.length} calls to hook "useState" within GeraCard, but after deletion of one card, you will have {cards.length-1} calls to hook "useState". As the React docs state:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function. By
following this rule, you ensure that Hooks are called in the same
order each time a component renders. That’s what allows React to
correctly preserve the state of Hooks between multiple useState and
useEffect calls.
You should extract the content of map callback into separate a component.
const GeraCards = (cards, cart = false) => {
return cards.map((v, i) =>
<GeraCard card={v} index={i} cart={cart} />
);
};
const GeraCard = ({ card, index, cart }) => {
const [show, setShow] = useState(true);
const v = card;
return (
<div key={index}>
{show ?
<div className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
setShow(false);
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div> :
null
}
</div>
);
}
Related
I'm developing a large fullstack application. A large part of the pages that we're creating are meant to create database objects, fetch their data, display data, and update data. As such, we have quite heavy, visual-based forms.
I would like to encapsulate different (controlled) form input types that we use (text, email, date, time, etc.) into nice, re-usable components. Importantly, they should be able to hold multiple states relating to a single input (error states, visual rendering states, etc.), and somehow expose its state, so we can grab form-input states and errors to judge whether or not we should submit the states to our API.
We are also using vanilla Bootstrap 5 in the project, and cannot use React Bootstrap, so we must comply by form validation and use in/valid-feedback classes and is-in/valid as well.
What I've tried is this:
custom hook with useState hooks that return an object containing states to export, in conjunction with a custom render component that takes as a prop the exported state.
ex:
const useText = ({initial}) => {
const [value, setValue] = useState(initial)
const [errors, setErrors] = useState([])
return({value, setValue, errors})
}
const TextRender = ({value, setValue, label, id, errors}) => {
return(
<div>
<label>{label}</label>
<input id={id} type="text" value={value} className={`form-control ${errors.length > 0 ? 'is-invalid' : ''}`} onChange={setValue}/>
<div className="invalid-feedback">
{errors.map(x => {
return <span>{x}</span>
})}
</div>
</div>
)
}
...
const Form = () => {
const firstName = useText({initial: 'Bill'})
return(
<TextRender {...firstName} label={"First Name"} id="firstName" />
)
}
This was great and compact, but would re-render the whole form a lot, and was not optimized when dozens of text inputs were added. This is especially true when this component is adapted to use a color input, as the onChange can fire MANY times a second.
Entirely self-contained components within custom hooks, which export states AND a render function
export const useColorInput = ({initial = '#FFFFFF', options = {labelTop: false}, id, tooltip, label, mode = 'onBlur'}) => {
const [color, setColor] = useState(initial)
const inputRef = useRef()
const onChange = (e) => {
if(inputRef){
inputRef.current.value = e.target.value
}
if(mode === 'onChange'){
setColor(e.target.value)
}
// setColor(e.target.value)
}
const onBlur = (e) => {
if(mode === 'onBlur'){
setColor(e.target.value)
}
}
function render(){
return(
<div className={`${options.labelTop === false ? 'd-flex flex-row-reverse align-items-center mb-2' : ''} `}>
{label &&
<label className="form-label flex-fill ms-1 mb-0" htmlFor={id}>{label}
{tooltip && <i class="fa-solid fa-circle-question text-muted ms-1" data-bs-html="true" data-toggle="tooltip" data-placement="top" title={tooltip}></i>}
</label>
}
{/* onChange={onChange} onBlur={onBlur} */}
<input type="color" ref={inputRef} className={`form-control form-control-color `} id={id} onChange={onChange} onBlur={onBlur} />
</div>
)
}
return({color, render, inputRef})
}
const colorA = useColorInput({initial: '#FFF', label: 'A', id: 'colorA'})
const colorB = useColorInput({initial: '#FFF', label: 'B', id: 'colorB'})
const colorD = useColorInput({initial: '#FFF', label: 'D', id: 'colorD'})
This is the most practical solution so far, as it further bundles the components using the render function, and I can render the same HTML/JSX twice with a single state, and the values and other states are returned from the hook so I am able to use the state for form submission.
Self-contained react functional component, which only returns the corresponding JSX rendering code
const ColorComponent = ({initial = '#FFFFFF', options = {labelTop: false}, id, tooltip, label, mode = 'onBlur', get, set}) => {
const [color, setColor] = useState(initial)
return(
<div className={`${options.labelTop === false ? 'd-flex flex-row-reverse align-items-center mb-2' : ''} `}>
{label &&
<label className="form-label flex-fill ms-1 mb-0" htmlFor={id}>{label}
{tooltip && <i class="fa-solid fa-circle-question text-muted ms-1" data-bs-html="true" data-toggle="tooltip" data-placement="top" title={tooltip}></i>}
</label>
}
{/* onChange={onChange} onBlur={onBlur} */}
<input type="color" className={`form-control form-control-color `} id={id} onChange={(e) => set(e.target.value)} value={get}/>
</div>
)
}
This is the most render-efficient so far, up-until we lift the state from the component to its parent form, which begins to bog down performance on state updates.
That being said, I need to be able to get these stateful variables because I need to update other JSX that use form values in a 'preview' window of the form.
It's also important to know that we also use non-custom components from libraries like antd and react-draft-wysiwyg, and we have some dynamic input methods, which allow the user to add, swap, remove items (each item having multiple different inputs per, such as an image, an alt-tag, etc...).
I have also used react-hook-form, but it started lacking in performance (perhaps I wasn't using it correctly?), and the custom hooks I've created in example (2) were inspired/based off how react-hook-form works, just simpler and uses states instead of refs, and it seems to outperform the way I've implemented react-hook-form for my specific use-case.
TL;DR: What is the best way to contain components which contain multiple states, which can be retrieved at the parent-level for form submission, but are also performant in terms of rendering?
SEMI-SOLUTION: I'm going to make a fool of myself, but in doing all of these implementations with hooks and memoization, I read that performance increases in production builds - I checked our production/QA build, and sure enough, the performance issues are entirely gone. react-hook-form does a really good job in our application, and it's pretty much due to development-build qualities that make it inefficient in performance.
I want to show a div related to an object property that comes true or false.
And I used a way but I'm not sure it's the best way or is it open the performance problems.
I'm checking that property in the loop that in return section for avoid extra array operation. But I think it will cause extra render.
The other option is checking that property in outside from return section. But this will be cause an extra array operation.
Which is the best way for me? I showed 2 different implementation below.
Option 1:
const RadioButtonList: FunctionComponent<RadioButtonListProps> = ({ items, changeFilter }) => {
const [showClearIcon, setShowClearIcon] = React.useState(false);
return (
<div className="radio-button-list">
{showClearIcon && <div className="clear-icon">clear</div>}
<ul>
{items.map(item => {
/* this is the area what I'm checking the property */
if (item.selected) {
setShowClearIcon(true);
}
return (
<li key={item.id}>
<label htmlFor={item.text} className="radio">
<span className="input">
<input type="radio" onClick={changeFilter} readOnly />
</span>
</label>
</li>
);
})}
</ul>
</div>
);
};
Option 2:
const RadioButtonList: FunctionComponent<RadioButtonListProps> = ({ items, changeFilter }) => {
const [showClearIcon, setShowClearIcon] = React.useState(false);
/* set in useEffect hook */
useEffect(() => {
if(items.some(item => item.selected)) {
setShowClearIcon(true);
}
}, [items]);
return (
<div className="radio-button-list">
{showClearIcon && <div className="clear-icon">clear</div>}
<ul>
{items.map(item => {
return (
<li key={item.id}>
<label htmlFor={item.text} className="radio">
<span className="input">
<input type="radio" onClick={changeFilter} readOnly />
</span>
</label>
</li>
);
})}
</ul>
</div>
);
};
It looks like showClearIcon doesn't need to be a state atom at all, but just a memoized value dependent on items.
const showClearIcon = React.useMemo(
() => items.some(item => item.selected),
[items],
);
Option 1 enqueues a state update in the render return, don't use it.
Use option 2 to correctly enqueue the update as a side effect. In React the render function is to be considered a pure function. Don't unconditionally enqueue state updates.
Regarding performance, iterating an array is O(n). Iterating an array twice is still O(n).
Suggestion
The showClearIcon "state" probably shouldn't be React state since it's easily derived from the items prop.
Identify the Minimal (but complete) Representation of UI State
Let’s go through each one and figure out which one is state. Ask three
questions about each piece of data:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
Because of this, just compute the showClearIcon value locally.
const showClearIcon = items.some(item => item.selected);
This can be memoized with the useMemo hook with dependency on items if necessary.
You can technically do it without useState and useEffect and without iterating over the array twice, check out the example below (it might not be neccessary but it's good to know that this is possible as well):
const RadioButtonList: FunctionComponent<RadioButtonListProps> = ({
items,
changeFilter,
}) => {
const renderItems = () => {
let showClearIcon = false;
let markup = (
<ul>
{items.map((item) => {
if (item.selected) {
showClearIcon = true;
}
return (
<li key={item.id}>
<label htmlFor={item.text} className="radio">
<span className="input">
<input type="radio" onClick={changeFilter} readOnly />
</span>
</label>
</li>
);
})}
</ul>
);
return (
<>
{showClearIcon && <div className="clear-icon">clear</div>}
{markup}
</>
);
};
return <div className="radio-button-list">{renderItems()}</div>;
};
Simply created a function that generates the markup.
So, what you can do if you want to purely look at performance, is install the react dev tools chrome extension, then in your test server (for example localhost:3000) you can use the profiler tab in chrome's f12 menu to check which operation takes more memory!
this is something you can use for all optimization questions
If your goal is to render html components, option 2 (useEffect) should be used, by keeping in mind that only the required dependencies are included in the dependency array (this is to avoid re-render and performance related issues). In your case, option 2 is correct, and its performance will be tuned by React itself.
Option 2 is better approach, but Option 2 you can also have little better coding as below to make code more better and simpler
const RadioButtonList: FunctionComponent<RadioButtonListProps> = ({ items, changeFilter }) => {
// as suggested by AKX
const showClearIcon = React.useMemo(
() => items.some(item => item.selected),
[items],
);
/* set in useEffect hook */
useEffect(() => {
if(items.some(item => item.selected)) {
setShowClearIcon(true);
}
}, [items]);
// for displaying row items
const generateRow = ({id, text}) => {
return (
<li key={item.id}>
<label htmlFor={item.text} className="radio">
<span className="input">
<input type="radio" onClick={changeFilter} readOnly />
</span>
</label>
</li>
);
}
return (
<div className="radio-button-list">
{showClearIcon && <div className="clear-icon">clear</div>}
<ul>
{items.map(item => generateRow(item))}
</ul>
</div>
);
};
I recently start learning React. I have problem when use dispatch in UseEffect, which is inside the child in the loop. I can't publish all project, but below piece of code:
On the home page online store products are displayed. In a separate component there is a small card that is displayed using the map loop:
<Row className='mt-20'>
{products.map(product => (
<ProductItem key={product._id} product={product} history={history} />
))}
</Row>
Child component code:
const ProductItem = ({ product, history }) => {
const dispatch = useDispatch()
const reviewList = useSelector(state => state.reviewList)
const { reviews } = reviewList
useEffect(() => {
let clean = false
if (!clean) dispatch(listReviewsDetailsByProductId(product._id))
return () => (clean = true)
}, [dispatch, product])
const productRate =
reviews.reduce((acc, item) => item.rating + acc, 0) / reviews.length || 0
return (
<Col xl='3' sm='6'>
<Card>
<Card.Body>
<div
className={'product-img position-relative ' + styles.wrapperImage}
>
<Link to={'/product/' + product.slug}>
{product.images[0] && (
<img
src={product.images[0].path}
alt=''
className='mx-auto d-block img-fluid'
/>
)}
</Link>
</div>
<div className='mt-4 text-center'>
<h5 className='mb-3 text-truncate'>
<Link to={'/product/' + product.slug} className='text-dark'>
{product.name}{' '}
</Link>
</h5>
{reviews && (
<div className='mb-3'>
<StarRatingsCustom rating={productRate} size='14' />
</div>
)}
<ProductPrice price={product.price} newPrice={product.newPrice} />
</div>
</Card.Body>
</Card>
</Col>
)
}
I use dispatch action to get reviews for a specific product from the server and then calculate the rating and display it. Unfortunately, it works every other time, the rating appears, then disappears. I would be grateful for your help!
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in SingleProduct (created by Context.Consumer)
in Route (at Root.jsx:96)
The problem is the clean variable which is not a part of your component's state. It exists only inside the scope of the useEffect callback and gets recreated every time that the effect runs.
What is the purpose of clean and when should we set it to true? The cleanup function of a useEffect hook only runs when the component unmounts so that would not be the right place to set a boolean flag like this.
In order to dispatch once per product, we can eliminate it and just rely on the dependencies array. I'm using product_.id instead of product so that it re-runs only if the id changes, but other property changes won't trigger it.
useEffect(() => {
dispatch(listReviewsDetailsByProductId(product._id))
}, [dispatch, product._id])
If this clean state serves some purpose, then it needs to be created with useState so that the same value persists across re-renders. You need to figure out where you would be calling setClean. This code would call the dispatch only once per component even if the product prop changed to a new product, which is probably not what you want.
const [clean, setClean] = useState(false);
useEffect(() => {
if (!clean) {
dispatch(listReviewsDetailsByProductId(product._id))
setClean(true);
}
}, [dispatch, product._id, clean, setClean])
I'm rendering a list of child components which contain a checkbox, and when that checkbox is clicked, I want to move that child component inside another div element.
Here's an image of what my app looks nice. I'd like to check the student names and move them up, under the "Present" sub-heading..
let ClassComp = (props) => {
const { teacher, subject, students, day } = props.classOf
const renderStudents = (students) => {
if (students && students.length > 0) {
return (
<div>
{students.map((student, index) =>
<StudentCheckbox key={index} student={student} handleCheckboxClick={handleCheckboxClick} />
)}
</div>
)
} else {
return <p style={{ margin: '10px' }} >No students registered.</p>
}
}
const handleCheckboxClick = (elId) => {
const presentStudentEl = document.getElementById('present-students')
// move StudentCheckbox element inside this element ^
}
return (
<div className="ui segment" style={segmentStyle} >
<div className="content">
<div className="ui medium header">{teacher} - {subject}</div>
<div className="ui divider"></div>
<div className="ui sub header">Students</div>
<div className="ui tiny header">Present:
<div id="present-students"></div>
</div>
<div className="ui tiny header">Absent:
<div id="absent-students">
{renderStudents(students)}
</div>
</div>
<div style={{ marginBottom: '30px' }}>
<button className="mini compact ui negative right floated button"
onClick={() => setModalVisible(true)}>Delete Class
</button>
<Link to={`/todaysclass/edit/${props.classId}`} className="mini compact ui right floated button">Edit Class</Link>
</div>
</div>
</div >
)
}
const mapStateToProps = (state, ownProps) => {
return { classOf: state.classes[ownProps.classId] }
}
export default connect(mapStateToProps, { deleteClass })(ClassComp)
and here's my child component:
const StudentCheckbox = (props) => {
const uniqId = idGenerator()
return (
<div className="field" style={{ margin: '5px' }}>
<div className="ui checkbox">
<input type="checkbox" id={uniqId} onChange={() => props.handleCheckboxClick(uniqId)} />
<label htmlFor={uniqId}>{props.student}</label>
</div>
</div>
)
}
In this case, you'll need a state for your component. Take a look in the docs:
https://reactjs.org/docs/state-and-lifecycle.html
So basically, besides props (which are "fixed"), you'll have a state, which will change when you check the items.
Your render method will use the state to place the items either in one div, or the other. So all you have to do is use setState to change the state and the render method will redraw the new one.
You're using redux to manage state. That's good. It helps properly manage/manipulate data.
In this case you're trying to decorate a view without data changes [in redux store] - that's not good, it doesn't even make sense in react.
Rendered components/view is only a [derived] View from a Model(state) - in MVC. Moving some element from one div to another in DOM (if you implement this) doesn't change the [base] state - after rerendering you'll loose these kind of changes.
UPDATE:
You should keep students' presence in the store (default false). You need a separate action (with params/payload: classId, studentId), call API (in action creator to save attendence) and reducer for change 'flag' in redux store.
Each student will have isPresent property. You can simply change your renderStudents to render both divs (additional boolean argument and apriopriate filtering at the beginning).
I have the following warning :
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor).
with React-redux-router that I understand, but do not know how to fix.
This is the component that is generating the warning.
const Lobby = props => {
console.log("props", props)
if (!props.currentGame)
return (
<div>
<input type="text" ref={input => (roomName = input)} />
<button
className="button"
onClick={() => {
props.createRoom(roomName.value)
}}
>
Create a room
</button>
</div>
)
else
return (
<div>
{props.history.push(`/${props.currentGame}[${props.username}]`)}
</div>
)
}
export default Lobby
What I'm doing here is that my component receives the currentGame property from the Redux store. This property is initialized as null.
When the user creates a game, I want to redirect him on a new URL generated by the server that I assign inside the property currentGame with a socket.io action event that is already listening when the container of the component Lobby is initialized.
However, since the currentGame property changes, the component is re-rendered, and therefore the line
{props.history.push(`/${props.currentGame}[${props.username}]`)}
generates a warning since the property currentGame now has a value, and the history property should not get modified during the re-render.
Any idea on how to fix it ?
Thanks!
You should not write props.history.push in render, instead use Redirect
const Lobby = props => {
console.log("props", props)
if (!props.currentGame)
return (
<div>
<input type="text" ref={input => (roomName = input)} />
<button
className="button"
onClick={() => {
props.createRoom(roomName.value)
}}
>
Create a room
</button>
</div>
)
else
return (
<div>
<Redirect to={`/${props.currentGame}[${props.username}]`} />
</div>
)
}
Do one thing, instead of writing the condition and pushing with history.push(), just put the code inside componentDidMount() if you are trying to do in the beginning.
componentDidMount(){
if(condition){
history.push('/my-url');
}
}