How to use a react component as a function? - reactjs

I'd like to open a materialui dialog and handle the result from javascript to make a simple Yes/no prompt.
Id like it to work something like this (just mockup code to explain)
<MyPromptComponent />
{
MyPromptComponent.show('Do you really want to?').then((result) => alert(result ? 'then do it' : 'walk away') );
}
So the question is; How (if) can I put functions in my component, that I can call from a reference?
If someone knows of an example where something similar is dont I'd appreciate it.
Edit : 11/10/2020
The "problem" with the state way of doing this is that I have to leave the function showing the prompt, having to store temporary variables outside the function. If I could do something like this the code would be much more readable:
{
let tempData = doAProcessForThisFunctionOnly();
let sureResult = confirmDialog.show('Are you sure?');
if(sureResult )
doSomeMoreWithTempData(tempData);
else
doSomeOtherStuff(tempData);
doSomeEndStuff(tempdata);
}
In react I have to do this
{
let tempData = doAProcessForThisFunctionOnly();
tempDataRef.current = tempData;
setShowDialog();
}
onYes = () => {
let workData = tempDataRef.current;
doSomeMoreWithTempData(workData );
doSomeEndStuff(workData)
}
onNo = () => {
let workData = tempDataRef.current;
doSomeOtherStuff(workData );
doSomeEndStuff(workData)
}
doSomeEndStuff = (workData) => {
//Do the stuff here
}
It really seems I need to jump in and out of a lot of functions just to get a simple confirmation and even using variables outside the functon (refs).
That really seems a big step backwards code-wise, to me.
The "vanilla" way of doing this would even let me use the same prompt-dialog component from many different functions. In reactit seems I need a separate confirm-dialog for each case as the "yes"/"no" events are hardcoded per case.

You should control everything with state:
export default function App() {
const [show, setShow] = useState(false);
return (
<div className="App">
<button onClick={() => setShow(true)}>Initiate</button>
<MyPromptComponent
title="Do you really want to?"
show={show}
onConfirm={() => {
setShow(false);
alert("Then do it")
}}
onCancel={() => {
setShow(false);
alert("Walk away")
}}
/>
</div>
);
}
const MyPromptComponent = ({ show, title, onConfirm, onCancel }) => {
return (
<React.Fragment>
{show &&
<div>
Lets pretend this is modal - {title}
<button onClick={() => onConfirm()}>Confirm</button>
<button onClick={() => onCancel()}>Cancel</button>
</div>
}
</React.Fragment>
);
};
Please see sandbox

Related

Remove text by clicking on it - React

I'm trying to start learning react but fail understanding basic logic.
I have a todo list page, which works fine with a strike-through, but if I try to change the strike through to REMOVE instead, my app disappears on click.
Here's my code, hopefully you can understand:
function Note({ notes, note, onClickSetter }) {
const { input, id } = note
const [strikeThrough, setStrikeThrough] = useState(false);
function onNoteClick(event) {
const { value, id } = event.target
//setStrikeThrough((prev) => !prev) - the strike through which is canceled right now
onClickSetter(prev => prev.filter(aNote => aNote.id !== id)) // why this doesn't work?
}
return (
<>
<h1 style={ strikeThrough ? {textDecoration: 'line-through'} : { textDecoration: 'none' }} id={id} onClick={onNoteClick}>{input}</h1>
</>
)
}
a little explanation on my props:
notes - literally the list of notes which comes from a useState on father component (we shouldn't touch this from my understanding of react)
note - self note information
onClickSetter - the other part of useState, the setter one.
So on another words, I have the notes which holds all notes, and onClickSetter which is in another words is setNotes - both part of useState
on top of that I have a note information, because this is a note component
the father component:
function Body() {
const [Notes, setNotes] = useState([])
return (
<div className='notes-body'>
<NewNote onClickSetter={setNotes}/>
{Notes.map((note) => { return <Note key={note.id} notes={Notes} note={note} onClickSetter={setNotes}/>})}
</div>
)
}
function NewNote({ onClickSetter }) {
const [input, setInput] = useState('')
function onInputChange(event) {
const { value } = event.target
setInput(value)
}
function onButtonClick(event) {
onClickSetter((prev) => {
try {
return [...prev, {input: input, id: prev[prev.length-1].id+1}]
}catch{
return [{input: input, id: 0}]
}
})
setInput('')
}
return (
<>
<Input placeholder="add new note" className='note-text' onChange={onInputChange} value={input}/>
<Button className='btn btn-primary add-note' onClick={onButtonClick} />
</>
)
}
The reason is that event.target.id is a string representing a number since all HTML attributes has the string type. Whilst in your data structure, the ID is a number. So, e.g. "1" vs 1. This can be hard to spot sometimes.
The easiest way to fix this is to add a parseInt to the right place to convert the string to a number:
onClickSetter((prev) => prev.filter((aNote) => aNote.id !== parseInt(id)))
However, I also want to mention (and this is more advanced stuff but I like to get people on the right track :) ) that really, you shouldn't pass the whole setter down into the child component, but instead a callback called something like onRemoveNote that accept the note id and the actual filtering/removal would happen in the parent component.
This would be better placement of concerns. For now though, the above will work and I can help you out on stack overflow chat if needed :).

Is this an OK way to handle 2 similar components?

I have two similar React components and the way that I handled them is by using the first component in the other one. It works, but not really sure if this is a bad practice or not.
const Component1({titles, handleClick, isSpecial}) => {
const toggle = (index) => {
handleClick(index)
}
return (
{titles.map((title, index) => (
<button onClick={() => toggle(index)}>Click me</button>
))}
)
}
import Component1
const Component2({titles, updateTitle}) => {
const alteredTitles = titles.map((title) => {
const alteredTitle = title.toUppercase();
return alteredTitle;
}
const handleClick = (index) => {
const alteredTitle = alteredTitles[index];
updateTitle(alteredTitle);
}
return (
<Component1 titles={alteredTitles} handleClick={handleClick} isSpecial={true}
)
}
Is this a poor way to do this? My other options of my current knowledge would be to duplicate most of the functionality of Component1 into Component2 or to add conditionals into Component1. The way I have done it makes my code feel more clean I guess, but I feel like it is the wrong way to go about it, but don't really know why.

React + Api+ Context

I have a simple React app. On the 'home' page you can search movies from an API and add a movie to a list of favorited. I'm using Context to store which movies are on the list and pass it to the 'favorites' page where those items are rendered. It works well up to a point.
Once on the 'favorites' page, when I remove a movie, I would like the page to then show the updated elements. Instead, I have the elements I already had there plus the elements from the updated list.
So let's say my favorited movies were 'spiderman', 'batman' and 'dracula'. when I remove 'dracula' from the list, I suddenly have the cards of 'spiderman', 'batman, 'dracula', 'spiderman'(again) and 'batman'(again).
When I reload the 'favorites' page, it all works as intended. I just would like for it to be updated correctly upon removing the movie.
Any advice?
Here is the code for the Home page, Favorite page, DataContext and the Card component
import React, { createContext, useState, useEffect } from "react";
export const DataContext = createContext();
function DataContextProvider({ children }) {
const [favorited, setFavorited] = useState([]);
useEffect(() => {
const savedMovies = localStorage.getItem("movies");
if (savedMovies) {
setFavorited(JSON.parse(savedMovies));
}
}, []);
useEffect(() => {
localStorage.setItem("movies", JSON.stringify(favorited));
}, [favorited]);
function addToFavorites(id) {
setFavorited((prev) => [...prev, id]);
}
function removeFromFavorited(id) {
const filtered = favorited.filter(el => el != id)
setFavorited(filtered)
}
return (
<DataContext.Provider value={{ favorited, addToFavorites, removeFromFavorited}}>
{children}
</DataContext.Provider>
);
}
export default DataContextProvider;
function Favorites(props) {
const ctx = useContext(DataContext);
const [favoriteMovies, setFavoriteMovies] = useState([]);
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
for (let i = 0; i < savedMovies.length; i++) {
axios
.get(
`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
)
.then((res) => {
setFavoriteMovies((prev) => [...prev, res.data]);
});
}
}, [ctx.favorited]);
return (
<>
<Navbar />
<main>
<div className="favorites-container">
{favoriteMovies.map((movie) => {
return <Card key={movie.id} movie={movie} />;
})}
</div>
</main>
</>
);
}
function Home(props) {
const [moviesData, setMoviesData] = useState([]);
const [numOfMovies, setNumOfMovies] = useState(10);
const [search, setSearch] = useState(getDayOfWeek());
const [spinner, setSpinner] = useState(true);
const [goodToBad, setGoodToBad] = useState(null);
function getDayOfWeek() {
const date = new Date().getDay();
let day = "";
switch (date) {
case 0:
day = "Sunday";
break;
case 1:
day = "Monday";
break;
case 2:
day = "Tuesday";
break;
case 3:
day = "Wednesday";
break;
case 4:
day = "Thursday";
break;
case 5:
day = "Friday";
break;
case 6:
day = "Saturday";
break;
}
return day;
}
function bestToWorst() {
setGoodToBad(true);
}
function worstToBest() {
setGoodToBad(false);
}
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
axios
.get(
`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${search}`
)
.then((res) => {
setMoviesData(res.data.results);
//console.log(res.data.results)
setSpinner(false);
setGoodToBad(null);
});
}, [search]);
return (
<>
<Navbar />
<main>
<form>
<input
type="text"
placeholder="Search here"
id="search-input"
onChange={(e) => {
setSearch(e.target.value);
setNumOfMovies(10);
}}
/>
{/* <input type="submit" value="Search" /> */}
</form>
<div className="sorting-btns">
<button id="top" onClick={bestToWorst}>
<BsArrowUp />
</button>
<button id="bottom" onClick={worstToBest}>
<BsArrowDown />
</button>
</div>
{spinner ? <Loader /> : ""}
<div>
<div className="results">
{!moviesData.length && <p>No results found</p>}
{moviesData
.slice(0, numOfMovies)
.sort((a,b) => {
if(goodToBad) {
return b.vote_average - a.vote_average
} else if (goodToBad === false){
return a.vote_average - b.vote_average
}
})
.map((movie) => (
<Card key={movie.id} movie={movie} />
))}
</div>
</div>
{numOfMovies < moviesData.length && (
<button className="more-btn" onClick={() => setNumOfMovies((prevNum) => prevNum + 6)}>
Show More
</button>
)}
</main>
</>
);
}
export default Home;
function Card(props) {
const ctx = useContext(DataContext);
return (
<div
className={
ctx.favorited.includes(props.movie.id)
? "favorited movie-card"
: "movie-card"
}
>
<div className="movie-img">
<img
alt="movie poster"
src={
props.movie.poster_path
? `https://image.tmdb.org/t/p/w200/${props.movie.poster_path}`
: "./generic-title.png"
}
/>
</div>
<h2>{props.movie.original_title}</h2>
<p>{props.movie.vote_average}/10</p>
<button
className="add-btn"
onClick={() => ctx.addToFavorites(props.movie.id)}
>
Add
</button>
<button
className="remove-btn"
onClick={() => ctx.removeFromFavorited(props.movie.id)}
>
Remove
</button>
</div>
);
}
export default Card;
As mentioned before a lot of things cold be improved (you might want to check some react tutorial beginners related to best practices).
Anyway the main issue your app seems to be your callback after you get the response from the API (so this part):
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
for (let i = 0; i < savedMovies.length; i++) {
axios
.get(
`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`
)
.then((res) => {
setFavoriteMovies((prev) => [...prev, res.data]);
});
}
here you are calling setFavoriteMovies((prev) => [...prev, res.data]); but you actually never reset your favoriteMovies list.
So in your example favoriteMovies is ['spiderman', 'batman', 'dracula']. Then the useEffect callback executes with the array unchanged.
So you are making the requests just for 'spiderman' and 'batman' but your favoriteMovies array is ['spiderman', 'batman', 'dracula'] when the callback is entered (and this is why you end up appending those values to the existing ones and in the end your favoriteMovies == ['spiderman', 'batman', 'dracula', 'spiderman', 'batman'] in your example).
How to fix?
Quick fix would that might be obvious would be to reset the favoriteMovies at the beggining of useEffect. But that would be a extremly bad ideea since setting the state many times is terrible for performance reasons (each setState callback triggers a re-render) as well as for redability. So please don't consider this.
What I would suggest though would be to get all the values in the useEffect callback, put all the new favorite movies data in a variable and at the end of the function change the state in one call with the full updated list.
There are multiple ways to achieve this (async await is the best imo), but trying to alter the code as little as possible something like this should also work:
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const savedMovies = ctx.favorited;
const favoriteMoviesPromises = [];
for (let i = 0; i < savedMovies.length; i++) {
favoriteMoviesPromises.push(
axios
.get(`https://api.themoviedb.org/3/movie/${savedMovies[i]}?api_key=${key}&language=en-US`)
.then((res) => res.data)
);
}
Promise.all(favoriteMoviesPromises).then((newFavoriteMovies) =>
setFavoriteMovies(newFavoriteMovies)
);
});
Please note I wasn't able to test this code since I don't have an exact reproduction of the error (so it might need some small adjustments). This code sample is rather a direction for your problem :)
Edit regarding the comment:
Despite the state issue, I would really recommend working on code cleanliness, efficiency and readability.
Examples (I put a few examples in code snippets to avoid a really long comment):
1. `function getDayOfWeek`:
First thing is that you don't need the `day` variable and all the break statement.
You could just return the value on the spot (this would also stop the execution of the function).
So instead of
case 0:
day = "Sunday";
break;
you could have
case 0:
return "Sunday";
Going even further you don't need a switch case at all. You could just create an array
`const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', "Saturday"]`
and just return daysOfWeek[date].
This would result in shorter and easier to read code.
2. Also this code is not really consistent. For example you have
onChange={(e) => {
setSearch(e.target.value);
setNumOfMovies(10);
}}
but also `onClick={bestToWorst}` which is just `function bestToWorst() { setGoodToBad(true) }`.
If this is not reusable you could just use `onClick={() => setGoodToBad(true)}`.
But even if you really want to keep the bestToWorst callback for whatever reason you could at least write and inline function
(something like `const bestToWorst = () => setGoodToBad(true)` and use it the same).
Anyway... From thoose 2 cases (bestToWorst and `Search here` onChange function),
the second one make more sense to be defined outside.
3. The next part is really hard to read and maintain:
{!moviesData.length && <p>No results found</p>}
{moviesData
.slice(0, numOfMovies)
.sort((a,b) => {
if(goodToBad) {
return b.vote_average - a.vote_average
} else if (goodToBad === false){
return a.vote_average - b.vote_average
}
})
.map((movie) => (
<Card key={movie.id} movie={movie} />
))}
Also this code doesn't belong in html.
You should at least put the slice and sort parts in a function.
Going further `if(goodToBad)` and `else if (goodToBad === false)` are also not ideal.
It would be best to use a separate function an example would be something like:
const getFormattedMoviesData = () => {
let formattedMoviesData = moviesData.slice(0, numOfMovies)
if(!goodToBad && goodToBad !== false) return formattedMoviesData;
const getMoviesDifference = (m1, m2) => m1.vote_average - m2.vote_average
return formattedMoviesData.sort((a,b) => goodToBad ? getMoviesDIfference(b,a) : getMoviesDIfference(a,b)
4. DataContext name doesn't suggest anything.
I would propose something more meaningfull (especially for contexts) like `FavoriteMoviesContext`.
In this way people can get an ideea of what it represents when they come across it in the code.
Additionally the context only contains `favorited, addToFavorites, removeFromFavorited`.
So rather than using
`const ctx = useContext(DataContext);`
you could just use
`const {favorited, addToFavorites, removeFromFavorited} = useContext(DataContext);`
and get rid of the ctx variable in your code
Regarding the api:
If the search api returns all the movie data you need you can take it from there and use it in the favorites.
Alternatively it would be great to have an endpoint to return a list of multiple movies
(so send an array of id's in the request and receive all of them).
But this is only possible if the backend supports it.
But otherwise, since the api might contain hundreds of thousands or even millions, having them all stored on the frontside state would be an overkill
(you can in some cases have this type lists stored in a redux state or a react context and filter them on frontend side.
But it won't be efficient for such a big volume of data).
Small conclusion: ignoring the state part there aren't big issues in the code (and for a personal project or for learning might be decent). But if someone else has to work on in or you have to come back on this code after a month might become a nightmare. (especially since it seems like the codebase is not very small)
And people trying to understand your code might find it hard as well (including when you are posting it on stack overflow). I highlighted just a few, but it should point in the right direction, I hope.
First of all, you should review the way you manage the favorite movies and that of what you want to do with them in your app. If you need to make a page to display the list of favorites, I would rather save in localstorage the necessary information for the list (cover, title, year, id, etc) without having to save the whole movie object. This will prevent you from having to call the API for each movie which will be very bad in terms of performance on your application. Also, it will prevent you from having to create another state on the Favorites page so it will solve your problem automatically (I think your problem came from the duplicate state you have).

ReactJS - Alternative to passing useState between files

Background
I have a file that presents my main page Dash.js
This presents some data from an API on a "card", from two other files List.js and ListLoading.js
I have an additional "card" which I can trigger open with default useState value of 1, and the onClick works to close, as you will see in the dash.js file.
Current Code
//Dash.js
function Dash(props) {
//control additional card
const [openCard, setopenCard] = React.useState(0);
const closeCard = () => {
setopenCard(0);
}
//set API repo
const apiUrl = (`http://example.com/api/`);
axios.get(apiUrl, {
withCredentials: true }).then((res) =>{
setAppState({ loading: false, repos: res.data.emails });
});
return (
{(openCard>0 &&
<Card>
<Cardheader onClick={() => closeCard()}>
Click here to close
</Cardheader>
<Cardbody>
Some data here
</Cardbody>
</Card>
)
|| null
}
<Card>
<ListLoading isLoading={appState.loading} repost={appState.repos} />
<Card>
);
}
//List.js
const List = (props) => {
const { repos } = props;
if (!repos || repos.length === 0) return <p>No data available</p>;
for (var key in repos) {
return (
{repos.map((repo) => {
return (
<p className='repo-text max-width' >ID:{repo.id}{" "}Value:{repo.value} </p>
);}
)}
);}
};
export default List;
//ListLoading.js
function WithListLoading(Component) {
return function WihLoadingComponent({ isLoading, ...props }) {
if (!isLoading) return <Component {...props} />;
return (
<p style={{ textAlign: 'center', fontSize: '30px' }}>
Fetching data may take some time, please wait
</p>
);
};
}
export default WithListLoading;
Desired Outcome
I want to set the the value for openCard.useState() to the repos.id.
e.g. onClick={() => openCard({repos.id})}
The complication of this is that I need to retrieve that code from List.js and pass it to the useState for the openCard, which is in Dash.js.
I am still fairly new to react so this is proving a little tricky to work out how to do.
What I've tried
I have looked into useContext, but either it has confused me or I am right to think this would not work for what I am trying to do.
I have looked into redux, however this seems like that may be overkill for this solution.
I have tried a series of passing the different constants via import/export however I now understand that useState is not designed to work this way and should really be used within the function/class where it is contained.
So any thoughts to remedy would be greatly appreciated!
So, just to restate what I understood your issue to be:
You have a parent component that renders a list of objects and can render a detail card of one of the object.
You want to have a single item in your list of objects be able to tell the parent "please open card 123".
Now to look at the options you considered:
Redux I agree Redux is overkill for this. Redux is usually only necessary if you need complex, possibly async reading and writing to a single shared datasource across the whole scope of your application. For a little UI interaction like this, it is definitely not worth setting up Redux.
React Context Context relies on a Provider component, which you wrap some chunk of your app in. Any component below that Provider can then use useContext to reach into the memory of that Provider. You can store anything in there that you could store in a component, from a single state variable up to a more complex useReducer setup. So, in a way, this basically does what you were hoping to do with static variables passing the state around. This is the right solution if you were going to be using this state value across a wide variety of components.
Props are probably the right way to go here - since you have a parent who wants to get messages from a child directly you can give the child a callback function. This is the same as the onClick function you can give a button, except here you can pass your list a onShowCard function.
In your Dash:
<ListLoading
isLoading={appState.loading} repost={appState.repos}
onShowCard={(cardId) => setopenCard(cardId)} />
At the end of the List:
{repos.map((repo) => {
return (
<button key={repo.id} className='repo-text max-width' onClick={() => { props.onShowCard(repo.id) }>
ID:{repo.id}{" "}Value:{repo.value}
</button>
);}
)}
You can pass on the function to update state to ListLoading component which will be forwarded to List component assuming it is wrapped by thee HOC WithListLoading.
Inside List you can then attach and onClick on the element to pass on the id of the clicked element
function Dash(props) {
//control additional card
const [openCard, setopenCard] = React.useState(0);
const closeCard = () => {
setopenCard(0);
}
//set API repo
const apiUrl = (`http://example.com/api/`);
axios.get(apiUrl, {
withCredentials: true
}).then((res) =>{
setAppState({ loading: false, repos: res.data.emails });
});
const handleOpen = id => {
setopenCard(id);
}
return (
{(openCard>0 &&
<Card>
<Cardheader onClick={() => closeCard()}>
Click here to close
</Cardheader>
<Cardbody>
Some data here
</Cardbody>
</Card>
)
|| null
}
<Card>
<ListLoading isLoading={appState.loading} repost={appState.repos} handleOpen={handleOpen} />
<Card>
);
}
const List = (props) => {
const { repos, handleOpen } = props;
if (!repos || repos.length === 0) return <p>No data available</p>;
for (var key in repos) {
return (
{repos.map((repo) => {
return (
<p className='repo-text max-width' onClick={() => props.handleOpen(repo.id)} >ID:{repo.id}{" "}Value:{repo.value} </p>
);}
)}
);}
};
export default List;

Allow only one of the many modal dialog to be open at a time in React

I try to create a small bug tracking app with React 17. I have BugComponent where users can easily set priority, status, system, and some other properties by clicking on the related icon and select the value from a small popup dialog. It means I have 5-6 small modal dialogs for each bug and I have 20 bugs by default on the page.
With my current implementation, I have a component for displaying and changing priority like this:
export const PrioritySelector = ({priority}) => {
const [open, setOpen] = useState(false);
const [prio, setPrio] = useState(priority);
const handleOnClick = () => {
setOpen(!open);
}
const handleOnSelect = (selectedPriority) => {
setPrio(selectedPriority);
setOpen(false);
}
return (
<div>
<PriorityIcon priority={prio} onClick={handleOnClick} />
<PriorityOptionSelector open={open} onSelect={handleOnSelect} originalValue={prio}/>
</div>
);
}
I also have similar selectors for status and plannedEndDate.
The main BugComponent looks like this:
export const BugComponent = ({bug}) => {
return (
<div className="bug">
<StatusSelector status={bug.status} />
<div>{bug.text}</div>
<PrioritySelector priority={bug.priority} />
<DateSelector plannedEndDate={bug.plannedEndDate} />
<CommentIcon numberOfComments={bug.commentCount} />
</div>
)
}
It works, but the problem is the following:
If I open the dialog with clicking on the icon and then click on other icon of the same bug or even another bug without selecting a value then this modal dialog remains open. So it can happen that a lot of dialogs are open at a time.
Using vanilla JavaScript I would write something like this before open the new dialog:
document.querySelectorAll('.modal-selector.open').classList.remove('open');
How can I close dialogs before opening a new one in React?
You want to keep the information about what modal is currently open in a state variable above the BugComponent.
Do a state variable like this:
const NO_MODAL_EXPANDED = {bugId: "", componentType: ""};
const [expandedItem, setExpandedItem] = useState(NO_MODAL_EXPANDED);
// Then you pass these props down to the BugComponent:
<BugComponent
bug={bug}
hasExpandedItem={expandedItem.bugId === bug.bugId}
expandedItemType={expandedItem.componentType}
setExpandedItem={setExpandedItem}
closeModal={() => setExpandedItem(NoModalExpanded)}
/>;
In BugComponent you do:
export const BugComponent = ({
bug,
hasExpandedItem,
expandedItemType,
setExpandedItem,
closeModal
}) => {
const isExpanded = (name) => hasExpandedItem && expanedItemType === name;
return (
<div className="bug">
<StatusSelector
status={bug.status}
isExpanded={isExpanded("status")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "status"})}
/>
<div>{bug.text}</div>
<PrioritySelector
priority={bug.priority}
isExpanded={isExpanded("priority")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "priority"})}
/>
<DateSelector
plannedEndDate={bug.plannedEndDate}
isExpanded={isExpanded("date")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "date"})}
/>
<CommentIcon
numberOfComments={bug.commentCount}
isExpanded={isExpanded("comment")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "comment"})}
/>
</div>
);
};
And in the modal components you do:
export const PrioritySelector = ({
priority,
isExpanded,
expandItem,
closeItem,
}) => {
const [prio, setPrio] = useState(priority);
const handleOnClick = () => {
if (isExpanded) {
closeItem();
} else {
expandItem();
}
};
const handleOnSelect = (selectedPriority) => {
setPrio(selectedPriority);
closeItem();
};
return (
<div>
<PriorityIcon priority={prio} onClick={handleOnClick} />
<PriorityOptionSelector
open={isExpanded}
onSelect={handleOnSelect}
originalValue={priority}
/>
</div>
);
};
Read through it and see if you understand it. The core thing you need to understand is: you want to express the state in as few variables as possible. The only thing you want to know is: what component, if any, is expanded? Instead of distributing this state over a lot of components, you keep the state in the parent component that is the smallest common denominator.

Resources