I want to render a list of rows that can be added to and deleted dynamically. I have 2 components that handle adding and removing rows: ClassComponent and FunctionComponent. ClassComponent works as intended, but FunctionComponent only deletes the first row. The rows are stored as state and are updated using setState(). How can I make FunctionComponent logically equivalent to ClassComponent?
I'm using the library uuid to create unique keys.
import React from "react";
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
export default function App() {
return (
<div>
<ClassComponent />
<FunctionComponent />
</div>
);
}
class ClassComponent extends React.Component {
state = {
list: [{ id: uuidv4() }, { id: uuidv4() }],
};
handleDelete = (id) => {
this.setState((prevState) => ({
list: prevState.list.filter((row) => row.id !== id),
}));
};
handleAdd = () => {
this.setState((prevState) => ({
list: [...prevState.list, { id: uuidv4() }],
}));
};
render() {
const { list } = this.state;
return (
<>
<ul>
{list.map(({ id }) => (
<Row key={id} id={id} onClick={this.handleDelete}>
{id}
</Row>
))}
</ul>
<button onClick={this.handleAdd}>Add</button>
</>
);
}
}
const FunctionComponent = () => {
const [list, setList] = useState([{ id: uuidv4() }, { id: uuidv4() }]);
const handleDelete = (id) => {
console.log(id);
const copy = list.slice();
copy.splice(id, 1);
setList(copy);
};
const handleAdd = () => {
setList([...list, { id: uuidv4() }]);
};
return (
<>
<ul>
{list.map(({ id }) => (
<Row key={id} id={id} onClick={handleDelete}>
{id}
</Row>
))}
</ul>
<button onClick={handleAdd}>Add</button>
</>
);
};
const Row = ({ onClick, children, id }) => (
<li>
{children} <button onClick={() => onClick(id)}>Delete</button>
</li>
);
Is there any reason not to use array.prototype.filter() just like you've done in your Class Component? This seems more readable and ensures you avoid mutating state directly in fewer steps.
Here is what that would look like in your handleDelete function in FunctionComponent:
const handleDelete = (id) => {
setList(list.filter((row) => (
row.id !== id
)));
};
In your FunctionalComponent you need to give index of the entry to splice method. Try like below.
const handleDelete = (id) => {
const copy = list.slice();
// find the index
const index = copy.findIndex(({ id: ID }) => id === ID);
// do the deletiong using that index
copy.splice(index, 1);
setList(copy);
};
You have to review your usage of the splice Array prototype.
copy.splice(id, 1);
Splice need to be passed as first argument an index and at second argument a delete count as such:
splice(start, deleteCount)
and you are passing an id to it. Actually in your case you are passing undefined as you're calling the function without argument.
I guess you could use the index of the map function to make it work:
{list.map(({ id }, index) => (
<Row key={id} id={id} onClick={() => handleDelete(index)}>
{id}
</Row>
))}
In your case you were passing undefined as id and 1 as the deleteCount so it was always deleting the first item.
in the functional component, you did not pass the id!
const FunctionalComponent = () => {
const [list, setList] = useState([{ id: uuidv4() }, { id: uuidv4() }]);
const handleDelete = (id) => {
// get all object except the id
const newRows = list.filter((i) => i.id !== id);
setList(newRows);
};
const handleAdd = () => {
setList([...list, { id: uuidv4() }]);
};
return (
<>
<ul>
{list.map(({ id }) => (
<Row key={id} id={id} onClick={() => handleDelete(id)}>
{id}
</Row>
))}
</ul>
<button onClick={handleAdd}>Add</button>
</>
);
};
Related
In my project, I need to get selected items from a Flatlist and pass them to my parent component.
I created a local state like this:
const [myState, setMyState] = useState<IStateType[] | []>([])
Each time an item is selected I try to add it to my useEffect:
useEffect(() => {
const result = myState.filter((el) => el.id !== item.id)
if (isSelected) {
setMyState([
...result,
{
propOne: 0,
propTwo: 1,
id: item.id,
...
},
])
} else {
setMyState(result)
}
}, [isSelected])
But I would need to put mySate in the dependency of my useEffect to add each time the new items selected. If I add it to the useEffect dependency it causes an infinite loop ^^
How to add each new item to my array while listening to all the changes and without causing an infinite loop?
I believe the issue you're having it's because you're not separating the concerns of each component correctly, once you have to relay on the previous data every time, the useEffect can be tricky. But there are two solutions to your issue:
Make use of useState callback function:
The useState function can be used with a callback rather than a value, as follows:
useEffect(() => {
if (isSelected) {
setMyState(prevState => [
...prevState,
{
propOne: 0,
propTwo: 1,
id: item.id,
...
},
])
} else {
setMyState(result)
}
}, [isSelected])
Best structure of your components + using useState callback function
What I could see about your approach is that you (as you showed) seems to be trying to handle the isSelected for each item and the myState in the same component, which could be done, but it's non-ideal. So I propose the creation of two components, let's say:
<List />: Should handle the callback for selecting an item and rendering them.
<List />:
function List() {
const [myState, setMyState] = useState([]);
const isItemSelected = useCallback(
(itemId) => myState.some((el) => el.id === itemId),
[myState]
);
const handleSelectItem = useCallback(
(itemId) => {
const isSelected = isItemSelected(itemId);
if (isSelected) {
setMyState((prevState) => prevState.filter((el) => el.id !== itemId));
} else {
setMyState((prevState) => prevState.concat({ id: itemId }));
}
},
[isItemSelected]
);
return (
<div>
<p>{renderTimes ?? 0}</p>
{items.map((item) => (
<Item
item={item}
onSelectItem={handleSelectItem}
selected={isItemSelected(item.id)}
/>
))}
</div>
);
}
<Item />: Should handle the isSelected field internally for each item.
<Item />:
const Item = ({ item, selected = false, onSelectItem }) => {
const [isSelected, setIsSelected] = useState(false);
useEffect(() => {
setIsSelected(selected);
}, [selected]);
return (
<div>
<p>
{item.name} is {isSelected ? "selected" : "not selected"}
</p>
<button onClick={() => onClick(item.id)}>
{isSelected ? "Remove" : "Select"} this item
</button>
</div>
);
};
Here's a codesnack where I added a function that counts the renders, so you can check the performance of your solution.
Is there a way to do this?
I want to save a ChildComponent's state into an array state from the GrandParentComponent whenever I click a button from the ParentComponent?
Here's an overview of my components:
const GrandParentComponent = () => {
const [array, setArray] = useState([]);
return (
<div>
<ParentComponent array={array} setArray={setArray} />
</div>
);
};
const ParentComponent = ({ array, setArray }) => {
const ref = useRef(null);
const handleClick = () => {
ref.current.setArray();
};
return (
<div>
<button onClick={handleClick}>save</button>
{array.map((item) => (
<ChildComponent array={array} setArray={setArray} ref={ref} />
))}
</div>
);
};
const ChildComponent = forwardRef(({ array, setArray }, ref) => {
const [childState, setChildState] = useState("")
useImperativeHandle(ref, () => {
return {
setArray: () => {
setArray((array) => [{ ...childState }, ...array]);
}
};
});
return <div>ChildComponent</div>;
});
The problem I'm encountering is it only saves the value of the last ChildComponent. What I want is to save all of the values from the ChildComponents.
What am I doing it wrong here? Or is there a better or correct way of doing what I'm trying to do?
I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)
I really like react context, but I think it's missing something (or maybe I don't know how to do it)
Say I have a list of todos and it's corresponding provider as
const Home = () => (
<div className="container">
<TodosProvider>
<TodosList />
</TodosProvider>
</div>
)
const TodosList = () => {
const { todos } = useTodos();
return (
<>
{todos.map((todo, idx) => (
<SingleTodo />
))}
</>
)
}
And in another file
import { createContext, useContext, useState } from "react";
const TodosContext = createContext({});
export const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([{ text: 'a' }, { text: 'b' }, { text: 'c' }])
return (
<TodosContext.Provider value={{ todos }}>
{children}
</TodosContext.Provider>
)
}
export const useTodos = () => {
const todos = useContext(TodosContext)
return todos
}
How can I update a single todo inside the SingleTodo without:
1) Passing the map idx as a property to the SingleTodo and then from SingleTodo call a method of the TodosList provider passing the idx as a parameter
2) Giving an artificial id property to the todo. And then in TodosProvider update the todo that matches with that id.
The reasons for those restrictions are that:
1) Passing down the position of the todo in the rendering as a prop, invalidates the benefits of using context, which is to not have to do prop drilling
2) I don't think it's good to pollute the model with an artificial id just for state management.
I'd like to be able to create a SingleTodoContext and instantiate a SingleTodoProvider in each iteration of the loop
const TodosList = () => {
const { todos } = useTodos();
return (
<>
{todos.map((todo, idx) => (
<SingleTodoProvider key={idx} loadFrom={todo}>
<SingleTodo />
</SingleTodoProvider>
))}
</>
)
}
But that doesn't work because the provider would then need to store the loadFrom property as a state, and that would break the sync between the list todo, and the single todo.
So, how do I update a single item inside a list without prop drilling the position of the item in the list? I don't want to use Redux
You can pass methods for updating the values in context as part of your context. Here is an example based on your code (sort of all crammed together):
import React from "react";
import "./styles.css";
import { createContext, useContext, useState } from "react";
const TodosContext = createContext({});
export const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([
{ text: "a" },
{ text: "b" },
{ text: "c" }
]);
const selectTodo = (todo, idx) => {
console.log(
"do something with the todo here and then call setTodos, or something else?",
todo.text,
idx
);
// setTodos(prev => /* Do something here to update the list */)
};
return (
<TodosContext.Provider value={{ selectTodo, todos }}>
{children}
</TodosContext.Provider>
);
};
export const useTodos = () => {
const todos = useContext(TodosContext);
return todos;
};
const Home = () => (
<div className="container">
<TodosProvider>
<TodosList />
</TodosProvider>
</div>
);
const SingleTodo = ({ todo, onClick }) => (
<div>
{todo.text} <button onClick={() => onClick(todo)}>Click Me!</button>
</div>
);
const TodosList = () => {
const { selectTodo, todos } = useTodos();
return todos.map((todo, idx) => (
<SingleTodo onClick={todo => selectTodo(todo, idx)} todo={todo} key={idx} />
));
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Home />
</div>
);
}
Hope that helps!
I have this prop in one of my components, which I'd like to pass to one other component and use as a variable there:
const VerContinentToolbar = (props) => {
return (
<Menu className="nav-icon">
<CountryList displayFields={["countryName"]} />
</Menu>
);
};
If I console.log(props.props), I get this:
https://imgur.com/a/HnRHDcH
This is the value that I want to pass to useCountries.
The receiving component looks like this:
const useCountries = (props) => {
const [countries, setCountries] = useState([])
useEffect(() => {
firebase
.firestore()
.collection("Africa")
.onSnapshot((snapshot) => {
const newCountries = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}))
setCountries(newCountries)
})
}, [])
return countries
}
const CountryList = ({ displayFields = [] }) => {
const countries = useCountries();
console.log(countries)
return (
<div className="countries">
{countries.map(country => (
<div key={country.id}>
<div className="entry">
{displayFields.includes("continent") && (
<div>Name of continent: {country.continent}</div>
)}
{displayFields.includes("revName") && (
<div>{country.revName}</div>
)}
{displayFields.includes("countryName") && (
<div><Link to={"./Jumbotron"}>{country.countryName}</Link></div>
)}
{displayFields.includes("dest1") && (
<div>Destination 1: {country.dest1}</div>
)}
{displayFields.includes("dest2") && (
<div>Destination 2: {country.dest2}</div>
)}
{displayFields.includes("dest3") && (
<div>Destination 3: {country.dest3}</div>
)}
{displayFields.includes("beerPrice") && (
<div>Beer price: {country.beerPrice}</div>
)}
{displayFields.includes("foodPrice") && (
<div>Food price: {country.foodPrice}</div>
)}
{displayFields.includes("hostelPrice") && (
<div>Hostel price: {country.hostelPrice}</div>
)}
{displayFields.includes("review") && <div>Review: {country.review}</div>}
{displayFields.includes("imgUrl") && <img src={country.url} alt="no-img" />}
</div>
</div>
))}
</div>
);
};
I have tried and tried to understand context, but I cannot really wrap my head around it. I'd like to put the value from props into ".collection("Africa")", but I have no clue how to. I've read the documentation for it, but I'm too thick headed to understand work it out properly.
If there's other solutions, I'm all ears.
What the props that I'd like to pass on is: I've created one component for each continent. In each component, I call the VerContinentToolbar, passing the prop/name of the continent, like this:
const Africa = ({}) => {
return (
<div>
<VerContinentToolbar props={["Africa"]} />
This means, that
const VerContinentToolbar = (props) => {
return (
<Menu className="nav-icon">
<CountryList displayFields={["countryName"]} />
</Menu>
);
};
Holds the word "Africa".
I'd like to pass on "Africa", to my component, which fetches the collection of the continent (in this case: Africa).
I want to pass the information that the prop helds to this:
const useCountries = (props) => {
const [countries, setCountries] = useState([])
useEffect(() => {
firebase
.firestore()
.collection("Africa") <---- I WANT PROP FROM VERCONT HERE
.onSnapshot((snapshot) => {
const newCountries = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}))
setCountries(newCountries)
})
}, [])
return countries
}
To clarify this even more, I've attached two pictures. Asia: Here, I want to render the collection "Asia", to display each review that belongs to a country within the continent of Asia.
https://imgur.com/a/bZhQAwz
So far, I've created "Imagination land" and attached it to "Africa" in Firebase. I want to show this, only in the Africa component. And obviously, I want to show all the countries with the Asia tag in the Asia component.
Thanks
EDIT:
Problem is solved.
import React, { Component, useState, useEffect } from 'react'
import { Link } from 'react-router-dom';
import firebase from '../config'
import './Countries.css'
import props from './Navbar/VerContinentToolbar';
const useCountries = continent => {
const [countries, setCountries] = useState([]);
console.log('continent', continent) // <--- Get it as your first argument
console.log(continent.toString())
useEffect(() => {
firebase
.firestore()
.collection(continent.toString())
.onSnapshot((snapshot) => {
const newCountries = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}))
setCountries(newCountries)
})
}, [])
return countries
}
const CountryList = ({ continent, displayFields = [] }) => {
const countries = useCountries(continent); // <--- Pass it in here
return (
<div className="countries">
{countries.map(country => (
<div key={country.id}>
<div className="entry">
{displayFields.includes("continent") && (
<div>Name of continent: {country.continent}</div>
)}
{displayFields.includes("revName") && (
<div>{country.revName}</div>
)}
{displayFields.includes("countryName") && (
<div><Link to={"./Jumbotron"}>{country.countryName}</Link></div>
)}
{displayFields.includes("dest1") && (
<div>Destination 1: {country.dest1}</div>
)}
{displayFields.includes("dest2") && (
<div>Destination 2: {country.dest2}</div>
)}
{displayFields.includes("dest3") && (
<div>Destination 3: {country.dest3}</div>
)}
{displayFields.includes("beerPrice") && (
<div>Beer price: {country.beerPrice}</div>
)}
{displayFields.includes("foodPrice") && (
<div>Food price: {country.foodPrice}</div>
)}
{displayFields.includes("hostelPrice") && (
<div>Hostel price: {country.hostelPrice}</div>
)}
{displayFields.includes("review") && <div>Review: {country.review}</div>}
{displayFields.includes("imgUrl") && <img src={country.url} alt="no-img" />}
}
</div>
</div>
))}
</div>
);
};
export default CountryList
const VerContinentToolbar = (props) => {
return (
<Menu className="nav-icon">
<CountryList
continent={props.continent}
displayFields={["countryName"]}
/>
</Menu>
);
};
Thanks for all your help.
Pass it as a prop (continent) from your continent component
const Africa = () => {
return (
<div>
<VerContinentToolbar continent="Africa" />
</div>
);
};
In <VerContinentToolbar />, get the prop and pass it down to <CountryList />. Rather than passing props down to grandchildren components you can use the context API for that or Redux for that.
const VerContinentToolbar = props => {
console.log('continent prop is now here', props.continent)
return (
<Menu className="nav-icon">
<CountryList continent={props.continent} displayFields={["countryName"]} />
</Menu>
);
};
In <CountryList />, get the prop (continent) and pass it into your useCountries hook.
const CountryList = props => {
const countries = useCountries(props.continent); // <--- Pass it in here
console.log(countries)
return (
<div className="countries">
{countries.map(country => (
{/* ...code */}
))}
</div>
);
};
And finally in your hook get it as the first argument
const useCountries = continent => {
const [countries, setCountries] = useState([]);
console.log('continent', continent) // <--- Get it as your first argument
// ...code
return countries
}
As I understand your question to be, you want to within CountryList, call the useCountries hook to specify the collection you want.
const useCountries = (collection) => {
const [countries, setCountries] = useState([])
useEffect(() => {
firebase
.firestore()
.collection(collection) // <-- pass collection argument here
.onSnapshot((snapshot) => {
const newCountries = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}))
setCountries(newCountries)
})
}, [])
return countries
}
In CountryList pass the value you want to useCountries
const countries = useCountries('Africa');