I have a JSON file that I want to render in react with unknown keys and values (just their types):
{
"Day 1" :{
"Chest": {
"warmUp": ["parallel bar dips"],
"main": ["Bench Press", "Inclined Bench press", "Decline Bench press"],
"secondary": ["Dumbbell Flys", "Cable Crossover Flys", "Pec-deck Fly"]
},
"Biceps" : {
"Lola": ["Barbell Curl", "Preacher Curl"],
"bobo": ["Hammer Curls", "Cable Curl", "Dumbbell Curl"]
}
}
I want to Render them so they look like this
<h2>Day </h2>
<h4>Chest</h4>
<h5>warmUp</h5>
<ul>
<li>Bench Press</li>
<li>Inclined Bench press</li>
<li>Decline Bench press</li>
</ul>
<h5>secondary</h5>
<ul>
<li>Dumbbell Flys</li>
<li>Cable Crossover Flys</li>
<li>Pec-deck Fly</li>
</ul>
I have a solution https://codesandbox.io/s/cold-sun-vprkl?file=/src/App.js
Object.entries(program).map((arrays) => {
return arrays.map((renderArr, render_idx) => {
if (render_idx === 0) {
return render.push(<h2>{renderArr}</h2>);
} else {
for (const [muscleGroup, exerciseGroups] of Object.entries(renderArr)) {
render.push(<h4>{muscleGroup}</h4>);
for (const exerciseCategory in exerciseGroups) {
render.push(<h5>{exerciseCategory}</h5>);
exerciseGroups[exerciseCategory].map(
(exercise, exercise_idx) => {
return render.push(<li>{exercise}</li>);
}
);
;
}
}
}
});
});
but I want to know if there is a better, more elegant way to do this. Also, in my solution I couldn't wrap the ul tags around the list items.
I would split the logic between appropriate small components to improve readability and get closer to the first SOLID principle - single responsibility.
const data = {
'Day 1': {
Chest: {
warmUp: ['parallel bar dips'],
main: ['Bench Press', 'Inclined Bench press', 'Decline Bench press'],
secondary: ['Dumbbell Flys', 'Cable Crossover Flys', 'Pec-deck Fly'],
},
Biceps: {
Lola: ['Barbell Curl', 'Preacher Curl'],
bobo: ['Hammer Curls', 'Cable Curl', 'Dumbbell Curl'],
},
},
}
const Program = ({ program }) => {
const days = Object.entries(program)
return (
<section className="Program">
{days.map(([name, data], index) => (
<ProgramDay name={name} data={data} key={index} />
))}
</section>
)
}
const ProgramDay = ({ name, data }) => {
const parts = Object.entries(data)
return (
<div className="ProgramDay">
<h2>{name}</h2>
{parts.map(([name, data], index) => (
<ProgramPart name={name} data={data} key={index} />
))}
</div>
)
}
const ProgramPart = ({ name, data }) => {
const types = Object.entries(data)
return (
<div className="ProgramPart">
<h2>{name}</h2>
{types.map(([name, exercises], index) => (
<ProgramExercises name={name} exercises={exercises} key={index} />
))}
</div>
)
}
const ProgramExercises = ({ name, exercises }) => {
return (
<div className="ProgramExercises">
<h5>{name}</h5>
<ul>
{exercises.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
</div>
)
}
ReactDOM.render(<Program program={data} />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Your approach can work, but it's very un-React-like. A far more React-like approach would leverage JSX much more heavily (and use logic inside JSX), and it would also use multiple components and return instead of building arrays.
Imagine instead ...
const Main = () =>
Object.entries(program).map(arrays =>
arrays.map((renderArr, index) =>
{index === 0
? <h2>{muscleGroups}</h2>
: <MuscleGroups muscleGroups={muscleGroups} />
}
);
const MuscleGroups = ({ muscleGroups }) =>
Object.entries(muscleGroups).map(([muscleGroup, exerciseGroups]) =>
<MuscleGroup muscleGroup={muscleGroup} exerciseGroups={exerciseGroups}/>
);
const MuscleGroup = ({ muscleGroup, exerciseGroups }) =>
<>
<h4>{muscleGroup}</h4>
{Object.entries(exerciseGroups).map(([exerciseCategory, exerciseGroup]) =>
<ExcerciseGroup category={exerciseCategory} exerciseGroup={exerciseGroup}/>
)}
</>
const ExerciseGroup = ({ exerciseCategory, exerciseGroup }) =>
<>
<h5>{exerciseCategory}</h5>
<ul>
{exerciseGroup.map(exercise => <li>{exercise}</li>)}
</ul>
</>;
P.S. Apologies if there are any typos in there (it's hard to refactor lots of code outside an editor). But even if there are some, hopefully you can see the overall idea I'm trying to convey.
You don't have to use as many components as I did (eg. you could combine MuscleGroups and MuscleGroup if you wanted). Like anything in code, it's ultimately subjective as to what's the "best" way.
But in a larger sense, you should really have the same guiding principle as with regular Javascript code. Just as you wouldn't want one giant function that does everything in JS (you'd want several smaller, focused functions), you likewise want to break your React logic into several focused components.
Related
I'm picking up React and not sure if I'm doing this correctly. To preface the question I've read all about the React hooks; I understand them in isolation but am having trouble piecing them together in a real-life scenario.
Imagine I have a Parent component housing a list of Child components generated via a map function on the parent:
<Parent>
{items.map(i => <Child item={i} />)}
</Parent>
And say the Child component is just a simple:
function Child({item}) {
return <div>{item}</div>
}
However the Child component needs to update its view, and be able to delete itself. My question is - should I call useState(item) on the child so it internally has a copy of the item? In that case if I updated the item the items list in the parent wouldn't get updated right? To fix that I ended up having something that looks like:
<Parent>
{items.map(i =>
<Child
item={i}
updateItem={(index) => setItems( /* slice and concat items list at index */ )}
deleteItem={(index) => setItems( /* slice items list at index */ )}
/>)
}
</Parent>
And the Child component simply invokes updateItem and deleteItem as appropriate, not using any React hooks.
My question here are as follows:
should I have used useState in the child component?
should I have used useCallback on the updateItem/deleteItem functions somehow? I tried using it but it didn't behave correctly (the correct item in the Parent got removed but the state in the remaining rendered Child were showing values from the deleted Child for example.
My understanding is that this would be very inefficient because an update on 1 child would force all other children to re-render despite them not having been updated.
If done most properly and efficiently, what should the code look like?
Thanks for the pointers.
should I have used useState in the child component?
Usually duplicating state is not a good idea; so probably no.
should I have used useCallback on the updateItem/deleteItem functions
somehow
You might need it if you want to pass those callbacks to components wrapped in React.memo.
My understanding is that this would be very inefficient because an
update on 1 child would force all other children to re-render despite
them not having been updated
Yes your understanding is correct, but whether you would notice the slow down, depends on number of things such as how many child components there are, what each of them renders, etc.
If done most properly and efficiently, what should the code look like?
See below. Notice I added React.memo which together with useCallback should prevent those items from re rendering, props of which didn't change.
const Child = React.memo(function MyComponent({ item, update }) {
console.log('Rendered', item);
return (
<div
onClick={() => {
update(item);
}}
>
{item.name}
</div>
);
});
let itemsData = [
{ id: 0, name: 'item1' },
{ id: 1, name: 'item2' },
];
export default function App() {
let [items, setItems] = React.useState(itemsData);
let update = React.useCallback(
(item) =>
setItems((ps) =>
ps.map((x) => (x.id === item.id ? { ...x, name: 'updated' } : x))
),
[]
);
return (
<div>
{items.map((item) => (
<Child key={item.id} item={item} update={update} />
))}
</div>
);
}
Now if you click item1, console.log for item2 won't be called - which means item2 didn't rerender
No you don't have to create internal state. That's an anti pattern to create a local state just to keep a copy of props of the component.
You can keep your state on parent component in your case. Your child component can execute callbacks like you used,
for example,
const [items, _] = useState(initialItemArray);
const updateItem = useCallback((updatedItem) => {
// do update
}, [items])
const deleteItem = useCallback((item) => {
// do delete
}, [items])
<Child
data={item}
onUpdate={updateItem}
onDelete={deleteItem}
/>
Also note you shouldn't over use useCallback & useMemo. For example, if your list is too large and you use useMemo for Child items & React re renders multiple 100 - 1000 of list items that can cause performance issue as React now have to do some extra work in memo hoc to decide if your <Child /> should re render or not. But if the Child component contain some complex UI ( images, videos & other complex UI trees ) then using memo might be a better option.
To fix the issue in your 3rd point, you can add some unique key ids for each of your child components.
<Child
key={item.id} // assuming item.id is unique for each item
data={item}
onUpdate={(updatedItem) => {}}
onDelete={(item) => {}}
/>
Now react is clever enough not to re render whole list just because you update one or delete one. This is one reason why you should not use array index as the key prop
#Giorgi Moniava's answer is really good. I think you could do without useCallback as well and still adhere to best practices.
const {useEffect, useState} = React;
const Child = ({ item, update }) => {
const [rerender, setRerender] = useState(0);
useEffect(() => setRerender(rerender + 1), [item]);
useEffect(() => setRerender(rerender + 1), []);
return (
<div className="row">
<div className="col">{item.id}</div>
<div className="col">{item.name}</div>
<div className="col">{item.value}</div>
<div className="col">{rerender}</div>
<div className="col">
<button onClick={update}>Update</button>
</div>
</div>
);
}
const Parent = () => {
const [items, setItems] = useState([
{
id: 1,
name: "Item 1",
value: "F17XKWgT"
},
{
id: 2,
name: "Item 2",
value: "EF82t5Gh"
}
]);
const add = () => {
let lastItem = items[items.length - 1];
setItems([
...items,
{
id: lastItem.id + 1,
name: "Item " + (lastItem.id + 1),
value: makeid(8)
}
]);
};
const update = (sentItem) => {
setItems(
items.map((item) => {
if (item.id === sentItem.id) {
return {
...item,
value: makeid(8)
};
}
return item;
})
);
};
const makeid = (length) => {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
return (
<div className="parent">
<div className="header">
<h1>Parent Component</h1>
<h2>Items in Parent State</h2>
</div>
<div className="table">
<section>
<header>
<div className="col">ID</div>
<div className="col">NAME</div>
<div className="col">VALUE</div>
</header>
{items.map((item, i) => (
<div className="row" key={item + "-" + i}>
<div className="col">{item.id}</div>
<div className="col">{item.name}</div>
<div className="col">{item.value}</div>
</div>
))}
</section>
<div className="header">
<h1>Children</h1>
<h2>Based on Items state</h2>
</div>
<button onClick={add}>Add</button>
<section>
<header>
<div className="col">ID</div>
<div className="col">Name</div>
<div className="col">Value</div>
<div className="col">Re-render</div>
<div className="col">Update</div>
</header>
{items.map((item, i) => (
<Child
item={item}
key={"child-" + item + "-" + i}
update={() => update(item)}
/>
))}
</section>
</div>
</div>
);
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);
.parent {
font-family: sans-serif;
}
.header {
text-align: center;
}
section {
display: table;
width: 100%;
}
section > * {
display: table-row;
background-color: #eaeaea;
}
section .col {
display: table-cell;
border: 1px solid #cccccc;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Apologies if this is poorly written (first time posting here so feedback on how to better write posts welcome!)
I am using react map to iterate through lists of data.
{level1Folders.level2Folders.map(
(level2Folders, index) => {
return (
<li
id={level2Folders.folderLevel2Name}
key={level2Folders.folderLevel2Name + index}
>
<div
className="menu-item-folder-level-2"
onClick={() =>
hideMenuItem(
level2Folders.folderLevel2Name
)
}
>
<FaIcons.FaCaretRight />
{level2Folders.folderLevel2Name}
</div>
<ul
className="manuals d-none"
id={level2Folders.folderLevel2Name}
>
{level2Folders.manuals.map(
(manual, index) => {
return (
<li key={manual + index} id={manual}>
<div
onClick={() =>
handleExplorerItemClick(manual)
}
className="menu-item-manual"
>
{manual}
</div>
</li>
);
}
)}
I have a method hideMenuItem(menuItemId) which will hide items based on their id's, so the idea is to set the id = to the name of the item, so when the parent item is clicked the child elements will be hidden.
function hideMenuItem(menuItemId) {
console.log(menuItemId);
let x = document.getElementById(menuItemId);
if (x.classList.contains('d-block')) {
x.classList.add('d-none');
x.classList.remove('d-block');
} else {
x.classList.add('d-block');
x.classList.remove('d-none');
}
}
I have 5 uses of this - level2Folders.folderLevel2Name, the only one that won't work is when trying to enter this as a parameter in hideMenuItem(menuItemId), the value here is returned as the index of the item.
The point here is you want to toggle the item's child by using the element's classlist. It might be better if you change your approach and use react ways to achieve your goals. One of them is conditional styling where you can read here for the details.
For your case, let me show you one of many approach which using state in show and hide elements. Try absorp the concept and implement it at yours.
First, the data should have name which is the name of folder, showSubFolders which is property that will keep the boolean value, and subFolders that contains the sub folders details.
const folderData = [
{
name: "My Documents",
showSubFolders: false,
subFolders: [
{
name: "My Music",
icon: faMusic
},
{
name: "My Images",
icon: faImage
}
]
},
...
}
Then, set a folders state that will keep the folder's data in our component:
export default function OurComponent() {
const [folders, setFolders] = useState(folderData);
...
}
Since we use <FontAwesomeIcon icon={...} />, so we can render the main folder icon by using conditional icon selection which depends on folder's showSubFolders property value:
return (
...
<ul>
{folders.map((folder, index) => {
return (
<li>
<FontAwesomeIcon
icon={folder.showSubFolders ? faFolderOpen : faFolder}
/>
</li>
...
)
})}
</ul>
...
)
The next is toggle subFolders section by creating a toggleFolder method that use useCallback hooks and depends on folders state. We negate the value of showSubFolders property if the folder name equal to the argument name supplied.
const toggleFolder = useCallback(
(name) => {
const toggledFolders = folders.map((folder) => {
if (folder.name === name) {
folder.showSubFolders = !folder.showSubFolders;
}
return folder;
});
setFolders(toggledFolders);
},
[folders]
);
And we can call the toggleFolder method from our list item as follow:
return (
...
{folders.map((folder, index) => {
return (
<li key={index} style={{ cursor: "pointer" }}>
<span onClick={() => toggleFolder(folder.name)}>
<FontAwesomeIcon
icon={folder.showSubFolders ? faFolderOpen : faFolder}
/>
{folder.name}
</span>
...
</li>
...
)
})}
...
)
And when the folders state change, the component will re-render and we can use conditional render technique {folder.showSubFolders && ( ... ) here:
<li>
...
{folder.showSubFolders && (
<ul>
{folder.subFolders.map((subFolder, index) => {
return (
<li key={index}>
<FontAwesomeIcon icon={subFolder.icon} />
{subFolder.name}
</li>
);
})}
</ul>
)}
</li>
Of course, this is not the only way to achieve your goal, but it is more React Way in doing so.
And lastly, this is the final code:
I'm trying to build an example shopping cart and came across this example that doesn't seem to work here, but does show each product, price, add to cart button, and correctly tallies up the total when you add to the cart.
QUESTIONS
1) Will the use of concat cause any prototypal issues that I should worry about?
2) What is the point of doing this part of the code? Why are they setting props and children? Can this be omitted or refactored without this?
const Product = props => {
const { product, children } = props;
return (
<div className="products">
{product.name} ${product.price}
{children}
</div>
);
};
CODE
const Product = props => {
const { product, children } = props;
return (
<div className="products">
{product.name} ${product.price}
{children}
</div>
);
};
function App() {
const [products] = useState([
{ name: "Superman Poster", price: 10 },
{ name: "Spider Poster", price: 20 },
{ name: "Bat Poster", price: 30 }
]);
const [cart, setCart] = useState([]);
const addToCart = index => {
setCart(cart.concat(products[index]));
};
const calculatePrice = () => {
return cart.reduce((price, product) => price + product.price, 0);
};
return (
<div className="App">
<h2>Shopping cart example using React Hooks</h2>
<hr />
{products.map((product, index) => (
<Product key={index} product={product}>
<button onClick={() => addToCart(index)}>Add to cart</button>
</Product>
))}
YOUR CART TOTAL: ${calculatePrice()}
{cart.map((product, index) => (
<Product key={index} product={product}>
{" "}
</Product>
))}
</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Will the use of concat cause any prototypal issues that I should worry about?
No, an Array.concat() would simply return you a new Array reference, which is also correct while setting a state. And why would there be any prototypal issue? You aren't changing anything over the prototype.
What is the point of doing this part of the code? Why are they setting props and children?
const { product, children } = props;
You need product and children to display in your product page, you simply extract it from props, This way of extracting variables is called Destructering, it the same as:
const product = props.product;
const children= props.children;
I have a JSON object with multiple leves that I'm trying to iterate through to display like this, but I keep getting empty results.
<div>
Operation
**Speed: Slow
**Direction: Forward
**Status: Moving
</div>
<div>
Detail
**Type: Electric
**Drivetrain: AWD
</div>
JSON object
"operation": {
"speed": "slow",
"direction": "forward",
"status": "moving"
},
"detail": {
"type": "electric",
"drivetrain": "AWD"
}
The following works, but difficult to wrap each level in a DIV
const Bompush = (obj) => {
let output = []
Object.keys(obj).forEach(key => {
output.push(<div key={key}>{key}</div>)
Object.keys(obj[key]).forEach (value => {
output.push(<div key={value}>{value}</div>)
})
})
return output
}
This is the ideal way I'd like to do this but it returns empty
const Bom = (obj) => {
return (
<div className="outerContainer">
{
Object.keys(obj).forEach((key, item) => {
return (
<div className="category" key={key}>{key}
{
Object.keys(obj[key]).forEach (value => {
return (<div className="item" key={value}>{value}</div>);
})
}
</div>
);
})
}
</div>
);
}
My React component
export default class DiplayBom extends React.Component {
render() {
return (
Bom(this.props.myValues)
)
}
}
forEach doesn't collect the results. You want map:
Object.keys(obj[key]).map(value => (
<div className="item" key={value}>{value}</div>
))
You'd be better off pulling out the inner component (at the very least), because as simple as this is, it's already pretty hard to read. I'd consider restructuring the object, plus you're at the mercy of object property ordering, so you may not always get the results you'd prefer.
There's some refactoring and/or rethinking to be done.
Without deviating much from your original code, and completely untested, I'd probably start thinking more along these lines:
const Item = ({ value, item }) => (
<div className="item">{value}: {item}</div>
)
const Category = ({ cat, obj }) => {
return (
<div className="category">
{key}
{Object.keys(obj).map(val => <Item key={val} value={val} item={obj[val]} />)}
</div>
)
}
const Bom = obj => (
<div className="outerContainer">
{Object.keys(obj).map(cat => <Category key={cat} cat={cat} obj={obj[cat]} />)}
</div>
)
This is a bit of an open ended question, as I'm sure the way I'm going about this is incorrect. But I'm curious why React isn't re-rendering as I would expect. I suspect it has to do with the behavior of the useState hook paired with a functional component.
The code is in this CodeSandbox link, and code noted below:
function App() {
var foosList = ["foo1", "foo2", "foo3"];
const [selectedFoo, setSelectedFoo] = useState(-1);
const isSelected = i => i === selectedFoo;
return (
<div className="App">
<FoosWrapper>
<TitleSpan>Foos</TitleSpan>
<ListGroup>
{foosList.map((fooItem, i) => (
<ListGroupItem
key={fooItem}
active={isSelected(i)}
onClick={() => setSelectedFoo(i)}
>
{fooItem}
</ListGroupItem>
))}
</ListGroup>
</FoosWrapper>
<BarsWrapper>
<TitleSpan>Bars</TitleSpan>
<Bars foo={foosList[selectedFoo]} />
</BarsWrapper>
</div>
);
}
const Bars = props => {
const [pendingBar, setPendingBar] = useState("");
const [newBars, setNewBars] = useState([]);
const keyPress = e => {
if (e.key === "Enter") {
save(pendingBar);
}
};
const save = bar => {
newBars.push(bar);
setNewBars([...newBars]);
};
return (
<div>
<ListGroup>
<ListGroupItem key={props.foo}>{props.foo}</ListGroupItem>
</ListGroup>
<ListGroup>
{newBars.map(newBar => (
<ListGroupItem key={newBar}>{newBar}</ListGroupItem>
))}
</ListGroup>
<InputGroup>
<Input
placeholder="Add a bar"
onChange={e => setPendingBar(e.target.value)}
onKeyPress={keyPress}
/>
</InputGroup>
</div>
);
};
Broadly, there are two logical widgets: Foos and Bars. Foos on the left, Bars, on the right. I'd like to have a user select a 'foo', and a distinct list of bars associated with said 'foo' is displayed on the right. A user may add new bars to each respective 'foo'. Can think of foo having a parent relationship to bar.
The Bars component maintains a list of bars added by the user. My expectation is the Bars component would re-render the internal newBars collection when a new foo is selected. However, that state hangs around and is displayed regardless of what 'foo' is selected on the lefthand side.
That seems weird, but perhaps I'm not thinking of React functional components and hooks in the right way. Would love to understand why this behavior exists, and additional would love to hear proposed approaches that make more sense.
Any insight is greatly appreciated!
If you want the hierarchy to be reflected, then initialize the state of your bars so that it synchronizes with the state of your foos. Right now, your Bars is a single component maintaining its own state independently of App. Here's how I would approach this particular relationship.
function App() {
const foos = useMemo(() => ["foo1", "foo2", "foo3"], []);
const [bars, setBars] = useState(foos.map(() => []));
const [selectedIndex, setSelectedIndex] = useState(0);
const setBar = useCallback(
bar => {
setBars(bars => Object.assign(
[...bars],
{ [selectedIndex]: bar }
));
},
[setBars, selectedIndex]
);
const isSelected = useCallback(
index => index === selectedIndex,
[selectedIndex]
);
const foosList = useMemo(
() => foos.map((foo, index) => (
<ListGroupItem
key={foo}
active={isSelected(index)}
onClick={() => setSelectedIndex(index)}
>
{foo}
</ListGroupItem>
)),
[foos, isSelected, setSelectedIndex]
);
return (
<div className="App">
<FoosWrapper>
<TitleSpan>Foos</TitleSpan>
<ListGroup>{foosList}</ListGroup>
</FoosWrapper>
<BarsWrapper>
<TitleSpan>Bars</TitleSpan>
<Bars
foo={foos[selectedIndex]}
bars={bars[selectedIndex]}
setBars={setBar}
/>
</BarsWrapper>
</div>
);
}
function Bars({ foo, bars, setBars }) {
const [pendingBar, setPendingBar] = useState("");
const barsList = useMemo(
() => bars.map(bar => (
<ListGroupItem key={bar}>{bar}</ListGroupItem>
)),
[bars]
);
const save = useCallback(
bar => { setBars([...bars, bar]); },
[setBars, bars]
);
const change = useCallback(
event => { setPendingBar(event.target.value); },
[setPendingBar]
);
const keyPress = useCallback(
event => { if (event.key === "Enter") save(pendingBar); },
[pendingBar, save]
);
return (
<div>
<ListGroup>
<ListGroupItem key={foo}>{foo}</ListGroupItem>
</ListGroup>
<ListGroup>{barsList}</ListGroup>
<InputGroup>
<Input
placeholder="Add a bar"
onChange={change}
onKeyPress={keyPress}
/>
</InputGroup>
</div>
);
}
I may have gone a bit overboard with the memoization hooks, but that should give you at least an idea of what and how various values can be memoized.
Keep in mind that the second argument is the array of dependencies which determines whether the memoized value is recomputed or retrieved from cache. The dependencies of hooks are checked by reference, much like a PureComponent.
I also opted to initialize selectedIndex to 0 to avoid addressing the issue of how to handle the render function for Bars when no foo is selected. I'll leave that as an exercise to you.
If you want to show the bars for the selected foo, you need to structure your "bars" data accordingly. The simplest way to do this is to keep the "bars" data in the format of an object instead of an array. You can write the code as below.
const Bars = props => {
const [pendingBar, setPendingBar] = useState("");
const [newBars, setNewBars] = useState({});
const { foo } = props;
const keyPress = e => {
if (e.key === "Enter") {
save(pendingBar);
}
};
const save = bar => {
if (!foo) {
console.log("Foo is not selected");
return;
}
const bars = newBars[foo] || [];
bars.push(bar);
setNewBars({
...newBars,
[foo]: [...bars]
});
};
return (
<div>
<ListGroup>
<ListGroupItem key={props.foo}>{props.foo}</ListGroupItem>
</ListGroup>
<ListGroup>
{newBars[foo] &&
newBars[foo].map(newBar => (
<ListGroupItem key={newBar}>{newBar}</ListGroupItem>
))}
</ListGroup>
<InputGroup>
<Input
placeholder="Add a bar"
onChange={e => setPendingBar(e.target.value)}
onKeyPress={keyPress}
/>
</InputGroup>
</div>
);
};