I have some data coming from Firebase and I am printing them into the app as lists. However, at first I just want to print a header for each list and when clicking on these headers their specific lists must be shown.
I am able to hide and show the lists by clicking, but this is happening for all lists and not only for the target one.
What I am doing is the classic way, to set a state as false (open) and make it toggles after a click. The component is shown if open is true and hidden if it is false. The onClick function is in Clientes, the first child.
I have a container based on class and two functional components.
To be more specific, the container just receive the data and send it to the Clientes component as an object of arrays. Each array is a list and for each one of them an Orcamentos component is created, lastly all items of each list are rendered inside of its respective Orcamentos.
How could I make just the target list opens after a click?
*Container ClientesControls:
class ClientesControls extends Component {
state = {
clientes: null,
retorno: false,
open: false
}
openOrcamentosHandler = () => {
let open = this.state.open;
this.setState({open: !open})
}
componentDidMount() {
axios.get('/clientes.json')
.then(res => {
this.setState({clientes: res.data, retorno: true})
})
.catch(error => console.log(error))
}
render() {
let inserirClientes = <div>Carregando...</div>
if (this.state.retorno) {
inserirClientes = (
Object.keys(this.state.clientes)
.map(key => <Clientes
key={Math.random()}
clienteInfo={this.state.clientes[key]}
open={this.state.open}
openHandler={this.openOrcamentosHandler}
/>)
)
}
return (
<div>
{ inserirClientes }
</div>
);
}
}
*Child Clientes:
const clientes = props => {
return (
<div>
{
Object.keys(props.clienteInfo)
.map(key => {
return (
<div key={Math.random()} onClick={props.openHandler}>
<Orcamentos orcamentosInfo={props.clienteInfo[key]} open={props.open}/>
</div>
)})
}
</div>
);
};
*Child Orcamentos:
const orcamentos = props => {
let nome = Object.keys(props.orcamentosInfo)
.map(k => props.orcamentosInfo[k].nomeCliente);
return (
<div>
<h4>{nome[0]}</h4>
{
Object.keys(props.orcamentosInfo)
.map(k => <p key={Math.random()} >{ props.open ? props.orcamentosInfo[k].data : null}</p>)
}
</div>
);
}
The child Orcamentos components need to control their own open state. The way you have structured it they are both taking the same state as a prop from the parent ClientesControls, and clicking either child component refers to the same handler which updates that state, so of course they are both being activated.
const orcamentos = props => {
const [open, setOpen] = React.useState(false);
let nome = Object.keys(props.orcamentosInfo)
.map(k => props.orcamentosInfo[k].nomeCliente);
return (
<div onClick={() => setOpen(!open)}>
<h4>{nome[0]}</h4>
{
Object.keys(props.orcamentosInfo)
.map(k => <p key={Math.random()} >{ open ? props.orcamentosInfo[k].data : null}</p>)
}
</div>
);
}
Related
I have a contentEditable component:
EditableComponent.js
const EditableComponent = (props) => {
return <p contentEditable>{props.children}</p>;
};
In the ParentComponent I can add EditableComponents to an array (someArr) with useState, and then I pass someArr and setSomeArray via props to another component (AllEditable) to render it:
ParentComponent.js
import EditableComponent from "./components";
import AllEditable from "./components";
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setContentArr((prevContentArr) => {
return [...prevContentArr, <EditableComponent />];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
Each rendered component (EditableComponent) have a span with the content 'X' that should delete the target onClick:
AllEditable.js
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.filter((_, idx) => idx !== index);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
);
})}
</div>
);
};
The problem:
It doesn't matter which component I'm trying to delete, it removes the last component (even in the Components section in the developer tools) and I'm pretty sure that the logic behind deleting (filter) works well.
I tried deleting the contentEditable attribute, and added some unique random text in each component and then it worked as expected!.
Things I tried
Creating a new array without the removed target
Nesting the components in someArr in objects with key: index, example: {idx: 0, content: <EditableComponent />}
Added a function - onDoubleClick for each EditableComponent to toggle the attribute contentEditable, true or false.
Replaced the element in EditableComponent to <textarea></textarea> instead of <p contentEditable></p>
Your problem is all your EditableComponent components have the same key (because you haven't declared key on them). React cannot identify which EditableComponent you want to delete. You can check this document.
I'd suggest you add a key attribute like below
<EditableComponent key={prevContentArr.length - 1}/>
For safer index reservation, you should use map instead filter
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.map((x, idx) => idx !== index ? x : null);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return content ? (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
) : null;
})}
</div>
);
};
const { useState } = React
const EditableComponent = (props) => {
return <p contentEditable>{props.children}</p>;
};
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.map((x, idx) => idx !== index ? x : null);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return content ? (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
) : null;
})}
</div>
);
};
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setSomeArr((prevContentArr) => {
return [...prevContentArr, <EditableComponent key={prevContentArr.length - 1}/>];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
ReactDOM.render(
<ParentComponent/>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Side note that keys with index values are not the best because your array keeps changing that would make key changes as well. You can use some unique key generator to handle that situation instead.
You shluld change the way you set keys, setting the element key to: "content-index" is confusing for react, because once you remove an item all the indexes will change, and therefore your keys.
So you need to find a way to have static keys for your elements. That way everytime they render, the key will be the same.
The logic is working correctly and in fact it is deleting the correct elements, however since you are using the index to identify elements, you will never see this since it will always appear that only the last one is removed (when the array updates, the indices will update too).
So if you had 0,1,2 and removed 1 now you have an array with indices 0,1
To test this you can place a reference to the index when rendering the content editable div, based on testing you can see that the correct element is in fact being removed:
import "./styles.css";
import React, { useState } from "react";
const EditableComponent = (props) => {
return (
<p contentEditable>
{props.children}
{props.idtfy}
</p>
);
};
const AllEditable = (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.filter((_, idx) => idx !== index);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return (
<div key={`content-${idx}`}>
<span>{idx}</span>
<span
onClick={() => {
deleteContentHandler(idx);
}}
>
X
</span>
<div>{content}</div>
</div>
);
})}
</div>
);
};
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setSomeArr((prevContentArr) => {
return [
...prevContentArr,
<EditableComponent idtfy={prevContentArr.length + 1} />
];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
That said, your key should probably point to some static value, but that will only impact rendering order, not which item is being clicked/closed.
[Solved] My input component is losing focus as soon as I press any key only when its value is controlled from outside the portal
NOTE: I am sorry. While writing this, I found the problem in my code, but I decided to post this anyway
[Reason] I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
Portal
const root = document.getElementById('root')
const modalRoot = document.getElementById('modal-root')
const Portal = ({ children, className, drawer = false }) => {
const element = React.useMemo(() => document.createElement('div'), [])
React.useEffect(() => {
element.className = clsx('modal', className)
modalRoot.appendChild(element)
return () => {
modalRoot.removeChild(element)
}
}, [element, className])
return ReactDOM.createPortal(children, element)
}
Modal
const Modal = (props) => {
const { children, show = false, close, className } = props
const backdrop = React.useRef(null)
const handleTransitionEnd = React.useCallback(() => setActive(show), [show])
const handleBackdropClick = React.useCallback(
({ target }) => target === backdrop.current && close(),
[]
)
const handleKeyUp = React.useCallback(
({ key }) => ['Escape'].includes(key) && close(),
[]
)
React.useEffect(() => {
if (backdrop.current) {
window.addEventListener('keyup', handleKeyUp)
}
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
return () => {
root.removeAttribute('inert')
document.body.style.overflow = 'auto'
window.removeEventListener('keyup', handleKeyUp)
}
}, [show, close])
return (
<>
{show && (
<Portal className={className}>
<div
ref={backdrop}
onClick={handleBackdropClick}
onTransitionEnd={handleTransitionEnd}
className={clsx('backdrop', show && 'active')}>
<div className="content">{children}</div>
</div>
</Portal>
)}
</>
)
}
Custom Textfield
const TextField = React.forwardRef(
({ label, className, ...props }, ref) => {
return (
<div className={clsx('textfield', className)}>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
</div>
)
}
)
I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
In Modal.jsx
...
React.useEffect(() => {
...
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
...
}, [show, close]) // as dependency
...
<Modal
show={show}
close={() => setShow(false)} // this was inlined
className="some-modal"
>
...
</Modal>
TAKEAWAY
Do not inline functions
Usually there is no reason to pass a function (pointer) as dependency
I have a parent component "Item" and a child component "Order".
Item has a button that toggles whether "Order" is displayed. If book is displayed, it passes the fetched details as props to the Order component, as well as the function for toggling if its open or closed.
Before adding the props to "Order", the toggle worked perfectly. After adding the props, the prop-handling works as it should, but now the function doesn't work. What am i doing wrong?
const Item = () => {
const [item, setItem] = useState('');
const [order, setOrder] = useState('');
//Api call to get item
const orderOpenClose = () => {
setOrder(!order);
};
return (
<>
<div onClick={orderOpenClose}>
<Button text="Order"></Button>
</div>
{order ? <Order acc={item} onChildClick={orderOpenClose}/> : ""}
</>
)
}
const Order = (props, { onChildClick }) => {
const { item } = props;
return (
<>
<div onClick={onChildClick}>
x
</div>
<p>{item.title}</p>
)
}```
This (props, { onChildClick }) is just not correct syntaxis, you can either destruct props or pass them as one object, but not both, so you can do either
const Book = ({acc, onChildClick })
or
const Book = (props) => {
const { acc,onChildClick } = props;
I have a functional component that is a modal. Inside I am doing a map on a array to render picture. I would like to add a div on the picture of one picture when clicking on it. However, it appears that the UI is not updated inside of the map even when using useState. Any idea on how to solve the issue?
Here is the code (I removed Style and things that were not important):
const CreateBoardModal = ({ closeModal, isModalOpen, searchResults, ...props }) => {
Modal.setAppElement('body')
const [movies, setMovies] = useState([])
const [searchResultsAdded, setSearchResultAdded] = useState({})
const addMovie = (movieId) => {
if (movies.includes(movieId)) {
console.log("Already in the list")
} else {
console.log("Added to the list!")
var movies_temp = movies
movies_temp.push(movieId)
setMovies(movies_temp)
}
var searchMoviesTemp = searchResultsAdded
searchMoviesTemp[movieId] = true
setSearchResultAdded(searchMoviesTemp)
console.log(searchResultsAdded) //Here everything is updated as expected
}
return (
<Modal
isOpen={isModalOpen}
onRequestClose={closeModal}
>
<div>
{searchResults.length > 0 &&
searchResults.map((movie, index) => {
return (
<div style={{ margin: 10 }} onClick={() => addMovie(movie.id)}>
<Movie
title={movie.original_title}
voteAverage={movie.vote_average}
posterPath={movie.poster_path}
></Movie>
{searchResultsAdded[movie.id] &&
<div>Added ✓</div> } {/*This is shown only when I hot reload the react app*/}
</div>
)
})}
</div>
</Modal >
)
}
export default CreateBoardModal;
I'm confused about react hot updating components.
I've got something like this:
const SingleEvent = ({ event }) => (
<>{event.status}</>
)
const EventDetails = ({ event, updateEvent }) => (
<button
onClick={async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} />
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
const Page = ({ initialEvents }) => {
const [events, setEvents] = useState(initialEvents || []);
const [selectedEvent, setSelectedEvent] = useState(null);
const updateEvent = (eventId, data) => {
setEvents(prevState => {
const eventIndex = prevState.findIndex(
element => element._id === eventId,
);
if (eventIndex === -1) {
return prevState;
}
prevState[eventIndex] = {
...prevState[eventIndex],
...data,
};
return prevState;
});
};
return <List events={events} updateEvent={updateEvent} selectedEvent={selectedEvent} />
}
In the <EventDetails /> component I'm updating one of the events (basically changing it's status). If API works fine, when I close the details (set the selectedEvent to null) everything is changing as it should. If I close the the details before getting the API response - nothing changed.
I've checked the updateEvent function, and it's performing the update, but the UI is not refreshed.
To be clear:
I Open the <EventDetails /> component, I'm pressing the button to update the event. The API should change its status. When I close the EventDetails I'm getting a List of <SingleEvent /> components. Every one of them displays the event.status.
If I close the EventDetails before getting response, status in SingleEvent is not updating. If I wait for the response everything works ok.
Since the component is unmounted before the data is fetched, its no longer able to update the response.
You can instead provide a function as props which performs the API requst and updates the status
const EventDetails = ({ handleClick }) => (
<button
onClick={handleClick}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
const handleClick = async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} handleClick={handleClick}/>
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
The problem was with the update method. Operating directly on prevState is not a good idea. After I changed the updateEvent function, everything works fine.
setEvents(prevState =>
prevState.map(event => {
if (event._id === eventId) {
return {
...event,
...updatedEvent,
};
}
return event;
})
);