React onClick to affect only this iteration - reactjs

I have a page that uses an object that contains lists within lists. I have all the components showing the data correctly, but I'm trying to add a toggle button for each primary list item so you can show/hide their child lists. I had previously made something that would affect EVERY instance of the component when clicked, so when you click the expand button it would toggle the child lists of EVERY primary item.
React is new to me and I'm using this project partially as a learning tool. I believe this has to do with binding state to the specific instance of the component, but I'm not sure how or where to do this.
Here is the component:
const SummaryItem = props => {
const summary = props.object;
return(
<div className="summary_item">
{Object.entries(summary).map( item =>
<div>
Source: {item[0]} <br />
Count: {item[1].count} <br />
<button onClick={/*expand only this SummaryItemList component*/}>expand</button>
<SummaryItemList list={item[1].items} />
</div>)
}
</div>
);
}
I previously had a state hook that looked like:
const [isExpanded, setIsExpanded] = useState(false);
const toggle = () => setIsExpanded(!isExpanded);
And in my render function the button had the toggle function in the onClick:
<button onClick={toggle}>expand</button> and I had a conditional if(isExpanded) with two renders, one with the SummaryItemList component and one without.
Is there a better way to do this besides mapping the object, and how do I bind the state of the toggle to affect only the instance it's supposed to affect?

I think you maybe forgot to give each item an isExpanded, the best way to do this is to split up your items and item in different components (in the example below it List for items and Item for item).
const { useState } = React;
const Item = ({ name, items }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggle = () => {
setIsExpanded((s) => !s);
};
return (
<li>
{name}
{items && (
<React.Fragment>
<button onClick={toggle}>
{isExpanded ? '-' : '+'}
</button>
{isExpanded && <List data={items} />}
</React.Fragment>
)}
</li>
);
};
const List = ({ data }) => {
return !data ? null : (
<ul>
{Object.entries(data).map(([key, { items }]) => (
<Item key={key} items={items} name={key} />
))}
</ul>
);
};
const App = () => {
const data = {
A: {
items: {
AA1: { items: { AAA1: {}, AAA2: {} } },
AA2: { items: { AAA: {} } },
},
},
};
return <List data={data} />;
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

How to update state of elements from array.map() separately?

In my code, I am trying to retrieve background images depending on the background data on the object, however those backgrounds will also change depending on the state of the element.
First of all, my data array looks like this:
const Cube = [
{
name: "foo"
faces: [
{
data: [
[{bonus: "bar", bg: "bar2"}],
...
],
...
},
...
],
...
},
...
];
And this is how my App.js looks:
function App() {
const [cellState, setCellState] = useState("inactive");
return (
<div className="App">
<div id="cube-container">
{Cube[0].faces[0].data.map((row) => {
return row.map((cell, index) => {
return (
<img
src={require(`./assets/cube/Square_Cube_Icon/${cell.bg}${
cellState === "inactive" ? "_Unlit" : "_Partial"
}.png`)}
alt={`${cell.bg} background`}
className="cellItem"
onClick={() => setCellState("active")}
state={cellState}
key={index}
/>
);
});
})}
</div>
</div>
);
}
Question is, in the 4x4 grid output, if I click on any item, instead of changing the clicked elements background because of state change, it changes background of every cell, which should happen since the useState is shared between all of them.
How can I make it so every element of the map function has their own state that I can update separately?
Found the solution:
Separating it as a component and passing key and cell values as props, then creating and manipulating the state inside fixed the issue.Selected answer also solves the issue.
Here's the solution for future readers.
App.js
function App() {
return (
<div className="App">
<div id="cube-container">
{CubeOfTruth[0].faces[0].data.map((row) => {
return row.map((cell, index) => {
return <CubeItem key={index} cell={cell} />;
});
})}
</div>
</div>
);
}
CubeItem.js
function CubeItem({ cell, index }) {
const [cellState, setCellState] = useState("inactive");
return (
<img
src={require(`../assets/cube/Square_Cube_Icon/${cell.bg}${
cellState === "inactive" || cellState === "partial" ? "_Unlit" : ""
}.png`)}
alt={`${cell.bg} background`}
className="cellItem"
onClick={() => setCellState("active")}
state={cellState}
key={index}
/>
);
}
Here's an easy way to manage multiple states using an object with its keys representing your cells and the values representing whether it is active or inactive. This is also easy to generalize so if you add more cells then all you have to do is just add it to the initialState & rest of your code should work as is.
function App() {
// const [cellState, setCellState] = useState("inactive");
const initialState = {
bar1: "inactive",
bar2: "inactive",
bar3: "inactive",
bar4: "inactive",
}
const [cellState, SetCellState] = React.useState(initialState);
const handleSetCellState = (key) => {
setCellState({
...cellState,
[key]: "active",
});
};
return (
<div className="App">
<div id="cube-container">
{Cube[0].faces[0].data.map((row) => {
return row.map((cell, index) => {
return (
<img
src={require(`./assets/cube/Square_Cube_Icon/${cell.bg}${
cellState[cell.bg] === "inactive" ? "_Unlit" : "_Partial"
}.png`)}
alt={`${cell.bg} background`}
className="cellItem"
onClick={() => handleSetCellState(cell.bg)}
state={cellState[cell.bg]}
key={index}
/>
);
});
})}
</div>
</div>
);
}

React: How to prevent menu component from being remounted when switching pages

Let's say that we have a React app with two pages A and B using a shared menu component Menu.
Our app renders either page A or page B, like the example below:
const Menu = (props) => {
React.useEffect(()=>{
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button onClick={() => props.onClick('a')}>A</button>
<button onClick={() => props.onClick('b')}>B</button>
</div>
);
}
const PageA = (props) => {
const .. = useSomeHooksUsedByPageA();
return (
<div>
<Menu {...somePropsFromPageA} />
<div>Content of page A</div>
</div>
);
}
const PageB = (props) => (
const .. = useSomeHooksUsedByPageB();
<div>
<Menu {...somePropsFromPageB} />
<div>Content of page B</div>
</div>
);
const App = () => {
const [pageKey, setPageKey] = React.useState("a");
switch (pageKey)
{
case "a":
return <PageA key="1" onClick={setPageKey} />;
case "b":
return <PageB key="1" onClick={setPageKey} />;
}
return "true"
}
Now, every time we switch pages (from A to B, or B to A), the menu is remounted and a message is printed to the console.
Using this component hierarchy where the menu receives props from the page, is there any way to tell React not to remount the menu when we switch pages?
(A typical use-case could be that the menu has a scroll, and we want to keep the scroll position when navigating different pages.)
Help is greatly appreciated!
One potential solution for this problem is to move <Menu/> into the <App/> component, and render each page after the menu.
This provides a couple of benefits:
The Menu won't be re-rendered whenever the page changes.
The onClick function does not need to be passed through props on each page just to provide it to the <Menu/> component nested within.
const Menu = (props) => {
React.useEffect(() => {
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button onClick={() => props.onClick("a")}>A</button>
<button onClick={() => props.onClick("b")}>B</button>
</div>
);
};
const PageA = () => (
<div>
<div>Content of page A</div>
</div>
);
const PageB = () => (
<div>
<div>Content of page B</div>
</div>
);
const App = () => {
const [pageKey, setPageKey] = React.useState("a");
let page;
switch (pageKey) {
case "b":
page = <PageB key="2" />;
break;
default:
page = <PageA key="3" />;
break;
}
return (
<>
<Menu onClick={setPageKey} />
{page}
</>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
Edit
Further to #glingt's comment regarding the hierarchy and how this needs to function, Context might be a good candidate for the use case. If pages need to update the <Menu/> component's props, then using context to manage state between the menu and pages might be a better solution in terms of architecture. Instead of rendering many <Menu/> components inside of each child, only one <Menu/> can be rendered higher up in the tree. This results in the component mounting once rather than many times with each child. Effectively, context manages the state of the menu, and provides methods to update state to any children under the provider. In this case, both child pages and the menu can update and respond to state updates.
import "./styles.css";
import React, { useContext, useMemo, useState } from "react";
// Create an instance of context so we are able to update the menu from lower in the tree
const menuContext = React.createContext({});
// Add state to the context provider. Wrap props earlier in the tree with this component.
const MenuContext = ({ children }) => {
const [pageKey, setPageKey] = useState("a");
const value = useMemo(() => ({ pageKey, setPageKey }), [pageKey]);
return <menuContext.Provider value={value}>{children}</menuContext.Provider>;
};
// The menu component which will:
// 1. Update the menuContext when the user selects a new pageKey
// 2. Respond to updates made to the pageKey by other components (in this case pages)
const Menu = () => {
const { pageKey, setPageKey } = useContext(menuContext);
React.useEffect(() => {
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button
onClick={() => setPageKey("a")}
style={{ color: pageKey === "a" ? "blue" : "red" }}
>
A
</button>
<button
onClick={() => setPageKey("b")}
style={{ color: pageKey === "b" ? "blue" : "red" }}
>
B
</button>
</div>
);
};
// In each page, we are able to update a value that is consumed by the menu using setPageKey
const PageA = () => {
const { setPageKey } = useContext(menuContext);
return (
<div>
<div>Content of page A</div>
<button onClick={() => setPageKey("b")}>Go to page B</button>
</div>
);
};
const PageB = () => {
const { setPageKey } = useContext(menuContext);
return (
<div>
<div>Content of page B</div>
<button onClick={() => setPageKey("a")}>Go to page A</button>
</div>
);
};
const PageComponent = () => {
const { pageKey } = useContext(menuContext);
switch (pageKey) {
case "b":
return <PageB key="2" />;
default:
return <PageA key="1" />;
}
};
const App = () => (
<MenuContext>
<Menu />
<PageComponent />
</MenuContext>
);
ReactDOM.render(<App />, document.querySelector("#app"))

React: how to use spread operator in a function that toggles states?

I made an example of my question here:
EXAMPLE
I'm mapping an array of objects that have a button that toggles on click, but when clicking on the button every object is changed.
This is the code
export default function App() {
const [toggleButton, setToggleButton] = useState(true);
// SHOW AND HIDE FUNCTION
const handleClick = () => {
setToggleButton(!toggleButton);
};
return (
<div className="App">
<h1>SONGS</h1>
<div className="container">
{/* MAPPING THE ARRAY */}
{songs.map((song) => {
return (
<div className="song-container" key={song.id}>
<h4>{song.name}</h4>
{/* ON CLICK EVENT: SHOW AND HIDE BUTTONS */}
{toggleButton ? (
<button onClick={handleClick}>PLAY</button>
) : (
<button onClick={handleClick}>STOP</button>
)}
</div>
);
})}
</div>
</div>
);
}
I know I should be using spread operator, but I couldn't get it work as I spected.
Help please!
Of course every object will change because you need to keep track of toggled state for each button. Here is one way to do it:
import { useState } from "react";
import "./styles.css";
const songs = [
{
name: "Song A",
id: "s1"
},
{
name: "Song B",
id: "s2"
},
{
name: "Song C",
id: "s3"
}
];
export default function App() {
const [toggled, setToggled] = useState([]);
const handleClick = (id) => {
setToggled(
toggled.indexOf(id) === -1
? [...toggled, id]
: toggled.filter((x) => x !== id)
);
};
return (
<div className="App">
<h1>SONGS</h1>
<div className="container">
{songs.map((song) => {
return (
<div className="song-container" key={song.id}>
<h4>{song.name}</h4>
{toggled.indexOf(song.id) === -1 ? (
<button onClick={() => handleClick(song.id)}>PLAY</button>
) : (
<button onClick={() => handleClick(song.id)}>STOP</button>
)}
</div>
);
})}
</div>
</div>
);
}
There are many ways to do it. Here, if an id is in the array it means that button was toggled.
You can also keep ids of toggled buttons in object for faster lookup.
One way of handling this requirement is to hold local data into states within the Component itself.
I have created a new Button Component and manages the toggling effect there only. I have lifted the state and handleClick method to Button component where it makes more sense.
const Button = () => {
const [toggleButton, setToggleButton] = useState(true);
const click = () => {
setToggleButton((prevValue) => !prevValue);
};
return <button onClick={click}>{toggleButton ? "Play" : "Stop"}</button>;
};
Working Example - Codesandbox Link

Avoid re-creating body component on each render of react-modal

I'm using this lib to create a modal
I have 3 components: Table, Modal and List
Table has Modal (a custom React Modal), and the body of Modal will be List.
Now the problem is, List has some functions which change the states of Table, so when I do something that can make Table's state change, Table and Modal will be re-rendered when Modal is re-rendered, it re-creates a new List which leads to lost all stuffs I'm doing with List.
Here is a simple version of my app. link
Now I don't want List to be re-created each time Modal is re-rendered. Is there any way to archive that? (I don't want to create a modal myself or use global state management in this case)
import { useEffect, useMemo, useState } from "react";
import ReactModal from "react-modal";
ReactModal.setAppElement("#root");
const List = ({ onClick }) => {
useEffect(() => {
console.log("List is mounted");
}, []);
return <button onClick={onClick}>Click me!</button>;
};
const Modal = ({ state, body, isOpen }) => {
useEffect(() => {
console.log("Modal is re-rendered");
});
return (
<div
id="react modal wrapper"
style={{
display: `${isOpen ? "block" : "none"}`
}}
>
<ReactModal isOpen={isOpen}>
<div>
state is {state}
<br />
{body}
</div>
</ReactModal>
</div>
);
};
const Table = ({ state, onClick, isOpen }) => {
useEffect(() => {
console.log("Table is re-rendered");
});
const memorizedList = useMemo(() => <List onClick={onClick} />, []);
return (
<div>
state: {state}
<Modal state={state} body={memorizedList} isOpen={isOpen} />
</div>
);
};
const App = () => {
const [state, setState] = useState(1);
const onClick = () => setState((v) => v + 1);
return (
<div>
<button onClick={onClick}>Change state</button>
<Table state={state} onClick={onClick} isOpen={state % 2 === 0} />
</div>
);
};
export default App;

How do I make one component talk to another when they are generated with .map() in React?

I have multiple components with the same module using map().
list.map((data, index) => <MyComponent key={index} value={d}/>)
Then <p> in each of MyComponent changes colors from green to red when it is clicked.
const MyComponent = ({value}) => {
const [clicked, setClicked] = useState(false);
const buttonOnClick = () => {
setClicked(true);
}
return (
<div>
<p style={clicked ? {color: 'green'} : {color: 'red'}}>{value}</p>
<button onClick={buttonOnClick}>click</button>
</div>
);
};
In this case, I would like to turn color of <p> in other MyComponent red when one of them are clicked.
How can I check the <p> state of other MyComponent?
You need to pass a callback into your child component, and have your parent component to store and control the state. Here's an example:
const list = [1, 2, 3];
const MyComponent = ({ value, clickedValue, onClick }) => {
const style = { color: clickedValue === value ? 'green' : 'red' };
return (
<div>
<p style={style}>{value}</p>
{/* Callback with the value */}
<button onClick={() => onClick(value)}>click</button>
</div>
);
};
const App = () => {
const [clickedValue, setClickedValue] = React.useState();
const handleClick = value => {
setClickedValue(value);
};
return React.Children.toArray(
list.map(value => (
<MyComponent
value={value}
clickedValue={clickedValue}
onClick={handleClick}
/>
))
);
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Probably the easiest option would be to lift state up into the parent component: https://reactjs.org/docs/lifting-state-up.html

Resources