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');
Related
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>
</>
);
};
I want to add to Chip an startIcon={<Icon />}
when click on a Chip.
The state of the icon is managed by chipsState.
In this code,
the state of all chips would change.
How can I change only the chipsState of the element that is clicked?
In this code, the state of all chips will change.
How can I change only the chipsState of the element that is clicked?
const Modal:React.FC<Props>= (props) => {
const {modalData} = props;
const [chipsState, setChipsState] = useState(false);
const onChipClick = (element:any) => {
setChipsState(chipsState => !chipsState);
}
return (
<div>
{
modalData.symtoms.map((element:any, index:number) => (
<div key={index}>
<Chip onClick={() => onChipClick(element)} startIcon={chipsState && <Icon />}>{element.description}</Chip>
</div>
))}
</div>
);
}
export default Modal;
To handle local state (and better testing), you should create a new custom Chip component with dedicated chipState.
interface CustomChipProps {
description: string
}
const CustomChip = (props: CustomChipProps) => {
const [chipState, setChipState] = useState(false);
return <Chip onClick={() => setChipState(prev => !prev)} startIcon={chipState && <Icon />}>{props.description}</Chip>;
}
const Modal:React.FC<Props>= (props) => {
const {modalData} = props;
return (
<div>
{
modalData.symtoms.map((element:any, index:number) => (
<div key={index}>
<CustomChip description={element.description} />
</div>
))}
</div>
);
}
export default Modal;
You can achieve your desired output by changing chipState state from boolean to object.
So first let's change to object state instead of boolean
const [chipsState, setChipsState] = useState({});
Now we will change onChipClick function to change value of selected chip state
const onChipClick = (element:any) => {
setChipsState({...chipsState, chipsState[element]: !chipsState[element]});
}
And finally we will read correct value of each chipsState element.
<Chip onClick={() => onChipClick(element)} startIcon={chipsState[element] && <Icon />}>{element.description}</Chip>
You can try like the following
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import { Grid, Row } from "react-flexbox-grid";
const ChipSet = ({ symtomsData }) => {
const data = symtomsData.map((symtom) => ({ ...symtom, isSelcted: false }));
const [chipSets, setChipSets] = useState(data);
const onSelectChipSet = useCallback(
(e, index) => {
const updatedChipSets = chipSets.map((chip, i) =>
i === index ? { ...chip, isSelcted: e.target.checked } : chip
);
setChipSets(updatedChipSets);
},
[chipSets]
);
console.log("chipSets", chipSets);
return (
<div>
<h1>Symtoms Data</h1>
{chipSets.map((x, i) => (
<div key={i}>
<label>
<input
onChange={(e) => onSelectChipSet(e, i)}
type="checkbox"
value={x.isSelcted}
/>
{x.description}
</label>
</div>
))}
</div>
);
};
class App extends React.Component {
render() {
const symtomsData = [
{
description: "mild"
},
{
description: "cold"
}
];
return (
<Grid>
<Row>
<ChipSet symtomsData={symtomsData} />
</Row>
</Grid>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
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 am using react context, and all it contains at the moment are 3 items: contacts and editingContact, and editContact:
interface ContactsContextProps {
contacts: Contact[];
editingContact: Contact;
editContact: (contact: Contact) => () => void // being lazy and this is from an onClick
}
const ContactsContext = React.createContext<Partial<ContactsContextProps>>({
editContact: (contact: Contact) => () => {}
})
const ContactsProvider: React.FunctionComponent = props => {
const [contacts, setContacts] = useState<Contact[]>();
const [editingContact, setEditingContact] = useState<Contact>();
React.useEffect(() => {
// fetch contacts, and setContacts(contacts)
}, [])
const editContact = React.useCallback((contact: Contact) => {
return function() {
setEditingContact(contact);
}
})
return (
<ContactsContext.Provider
value={{
editingContact,
editContact,
contacts
}}
>
{props.children}
</ContactsContext.Provider>
)
}
Here's how it is being used:
const ContactsList: React.FunctionComponent<{
contacts: Contact[];
}> = React.memo(props => {
return (
<>
{props.contacts.map(contact => (
<Card key={contact.id} contact={contact} />
))}
</>
);
});
const Wrapper: React.FunctionComponent = () => {
const contactsCtx = React.useContext(ContactsContext);
return (
<>
<Box className={styles.main}>
<Header />
{contactsCtx.contacts && <ContactsList contacts={contactsCtx.contacts} />}
</Box>
{contactsCtx.editingContact && <EditContactModal />}
</>
);
};
The <Card /> only has an edit button right now, which calls contactsContext.editContact(). However, each time this is called, all the Cards re-render. I placed a console.log('card') in each Card, and it logs card 10 times (I have 10 contacts right now).
What am I doing wrong?
There has been a discussion in a React Github issue, basically there is 3 possible solutions for this:
Option 1 (Preferred): Split contexts that don't change together
Option 2: Split your component in two, put memo in between
Option 3: One component with useMemo inside
You should check the link for examples about it.