removing object in an array based on one of it's properties - javascript

I am trying to manipulate an array of components inside the state and I want to be able to remove each component based on it's id property. I cant figure out how to target the component when I pass the id dynamically each time I create the component.
constructor(props) {
super(props);
this.state = {
counters: 0, // counters count
countersArr: []
}}
render() {
let countersArrCop = this.state.countersArr;
let counterIndex;
onAddCounterHandler = () => {
const id = new Date().getTime()
console.log(this.state.countersArr.length)
console.log(counterIndex)
countersArrCop.push( // push new counter to the counters holder
<Counter
style={styles.countersContainer}
key={id}
id={id}
remove={() => removeCounterHandler(id)}
/>
)
this.setState({
countersArr: countersArrCop,
})
console.log(id)
}
// remove counter
removeCounterHandler = (id) => {
console.log(id)
const countersArr = this.state.countersArr.slice(); // Local copy to manipulate
this.setState({ countersArr: countersArr.filter(counter => counter.id !== id) });
}
return (
<View style={styles.container}>
<View style={styles.addBtnContainer}>
<Button
onPress={onAddCounterHandler}
title={'add Counter'}
/>
</View>
{
/* return each individual counter from the array by using 'map' */
}
<View style={styles.container}> {
this.state.countersArr.map((counter) => {
return counter;
})
}
</View>
</View>
);
UPDATED THE CODE *** id === undefined.. why?

first of all putting logical codes in your render() is highly discouraged. render should only render, any other task should be where they should be. Note that all the code in your render block gets called every time the component re-renders. So those handlers should be declared as methods within your component class with proper hooks from your render like maybe onClick() and don't forget to bind your this context like this to keep your this reference to your component class inside your methods:
<Button onClick={this.onAddCounterHandler.bind(this)} />
second, you're bloating your state with an array of components when you can just store the ids in an array:
onAddCounterHandler() {
const { countersArr } = this.state
const id = new Date().getTime()
this.setState({
countersArr: [...countersArr, id],
})
}
then just map that array and return the component like this:
{ this.state.countersArr.map(id=>
<Counter
style={styles.countersContainer}
key={id}
id={id}
remove={this.removeCounterHandler.bind(this, id)}
/>
) }
note that in jsx, this statement should be enclosed in braces {}
then on your filtering function:
removeCounterHandler(filteredID) {
const { countersArr } = this.state
const filtered = countersArr.filter(id=>id!==filteredID)
this.setState({ countersArr: filtered});
}
Lastly, NEVER ever put setState in your render
try it and let me know how it goes :)

You can do a function like this one:
removeCounterHandler (id) {
const countersArr = this.state.countersArr.slice(); // Local copy to manipulate
this.setState({ countersArr: countersArr.filter(counter => counter.id !== id) });
}

Related

How can I render an array of components in react without them unmounting?

I have an array of React components that receive props from a map function, however the issue is that the components are mounted and unmounted on any state update. This is not an issue with array keys.
Please see codesandbox link.
const example = () => {
const components = [
(props: any) => (
<LandingFirstStep
eventImage={eventImage}
safeAreaPadding={safeAreaPadding}
isActive={props.isActive}
onClick={progressToNextIndex}
/>
),
(props: any) => (
<CameraOnboarding
safeAreaPadding={safeAreaPadding}
circleSize={circleSize}
isActive={props.isActive}
onNextClick={progressToNextIndex}
/>
),
];
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={`component-key-${index}`} isActive={isActive} />;
})}
</div>
)
}
If I render them outside of the component.map like so the follow, the component persists on any state change.
<Comp1 isActive={x === y}
<Comp2 isActive={x === y}
Would love to know what I'm doing wrong here as I am baffled.
Please take a look at this Codesandbox.
I believe I am doing something wrong when declaring the array of functions that return components, as you can see, ComponentOne is re-rendered when the button is pressed, but component two is not.
You should take a look at the key property in React. It helps React to identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
I think there are two problems:
To get React to reuse them efficiently, you need to add a key property to them:
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={anAppropriateKeyValue} isActive={isActive} />;
})}
</div>
);
Don't just use index for key unless the order of the list never changes (but it's fine if the list is static, as it appears to be in your question). That might mean you need to change your array to an array of objects with keys and components. From the docs linked above:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
I suspect you're recreating the example array every time. That means that the functions you're creating in the array initializer are recreated each time, which means to React they're not the same component function as the previous render. Instead, make those functions stable. There are a couple of ways to do that, but for instance you can just directly use your LandingFirstStep and CameraOnboarding components in the map callback.
const components = [
{
Comp: LandingFirstStep,
props: {
// Any props for this component other than `isActive`...
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
// Any props for this component other than `isActive`...
onNextClick: progressToNextIndex
}
},
];
then in the map:
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
There are other ways to handle it, such as via useMemo or useCallback, but to me this is the simple way — and it gives you a place to put a meaningful key if you need one rather than using index.
Here's an example handling both of those things and showing when the components mount/unmount; as you can see, they no longer unmount/mount when the index changes:
const {useState, useEffect, useCallback} = React;
function LandingFirstStep({isActive, onClick}) {
useEffect(() => {
console.log(`LandingFirstStep mounted`);
return () => {
console.log(`LandingFirstStep unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onClick}>LoadingFirstStep, isActive = {String(isActive)}</div>;
}
function CameraOnboarding({isActive, onNextClick}) {
useEffect(() => {
console.log(`CameraOnboarding mounted`);
return () => {
console.log(`CameraOnboarding unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onNextClick}>CameraOnboarding, isActive = {String(isActive)}</div>;
}
const Example = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const progressToNextIndex = useCallback(() => {
setCurrentIndex(i => (i + 1) % components.length);
});
const components = [
{
Comp: LandingFirstStep,
props: {
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
onNextClick: progressToNextIndex
}
},
];
return (
<div>
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById("root"));
.active {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Map of refs, current is always null

I'm creating a host of a bunch of pages, and those pages are created dynamically. Each page has a function that I'd like to call at a specific time, but when trying to access a ref for the page, the current is always null.
export default class QuizViewPager extends React.Component<QuizViewPagerProps, QuizViewPagerState> {
quizDeck: Deck | undefined;
quizRefMap: Map<number, React.RefObject<Quiz>>;
quizzes: JSX.Element[] = [];
viewPager: React.RefObject<ViewPager>;
constructor(props: QuizViewPagerProps) {
super(props);
this.quizRefMap = new Map<number, React.RefObject<Quiz>>();
this.viewPager = React.createRef<ViewPager>();
this.state = {
currentPage: 0,
}
for (let i = 0; i < this.quizDeck!.litems.length; i++) {
this.addQuiz(i);
}
}
setQuizPage = (page: number) => {
this.viewPager.current?.setPage(page);
this.setState({ currentPage: page })
this.quizRefMap.get(page)?.current?.focusInput();
}
addQuiz(page: number) {
const entry = this.quizDeck!.litems[page];
var ref: React.RefObject<Quiz> = React.createRef<Quiz>();
this.quizRefMap.set(page, ref);
this.quizzes.push(
<Quiz
key={page}
litem={entry}
index={page}
ref={ref}
pagerFocusIndex={this.state.currentPage}
pagerLength={this.quizDeck?.litems.length!}
setQuizPage={this.setQuizPage}
navigation={this.props.navigation}
quizType={this.props.route.params.quizType}
quizManager={this.props.route.params.quizType === EQuizType.Lesson ? GlobalVars.lessonQuizManager : GlobalVars.reviewQuizManager}
/>
)
}
render() {
return (
<View style={{ flex: 1 }}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={this.viewPager}
scrollEnabled={false}
>
{this.quizzes}
</ViewPager>
</View >
);
}
};
You can see in addQuiz() I am creating a ref, pushing it into my map, and passing that ref into the Quiz component. However, when attempting to access any of the refs in setQuizPage(), the Map is full of refs with null current properties.
To sum it up, the ViewPager library being used isn't actually rendering the children you are passing it.
If we look at the source of ViewPager (react-native-viewpager), we will see children={childrenWithOverriddenStyle(this.props.children)} (line 182). If we dig into the childrenWithOverriddenStyle method, we will see that it is actually "cloning" the children being passed in via React.createElement.
It is relatively easy to test whether or not the ref passed to these components will be preserved by creating a little demo:
const logRef = (element) => {
console.log("logRef", element);
};
const ChildrenCreator = (props) => {
return (
<div>
{props.children}
{React.Children.map(props.children, (child) => {
console.log("creating new", child);
let newProps = {
...child.props,
created: "true"
};
return React.createElement(child.type, newProps);
})}
</div>
);
};
const App = () => {
return (
<div className="App">
<ChildrenCreator>
<h1 ref={logRef}>Hello World</h1>
<p>It's a nice day!</p>
</ChildrenCreator>
</div>
);
}
ReactDOM.render(<App />, '#app');
(codesandbox)
If we look at the console when running this, we will be able to see that the output from logRef only appears for the first, uncopied h1 tag, and not the 2nd one that was copied.
While this doesn't fix the problem, this at least answers the question of why the refs are null in your Map. It actually may be worth creating an issue for the library in order to swap it to React.cloneElement, since cloneElement will preserve the ref.

Filtering Nested Arrays in React JS

I am attempting to filter a list of conversations by participant names. The participant names are properties inside of a participant list and the participant list is contained within a list of conversations.
So far, I have approached the problem by attempting to nest filters:
let filteredConvos = this.props.convos.filter((convo) => {
return convo.conversation.conversation.participant_data.filter(
(participant) => {
return participant.fallback_name.toLowerCase().indexOf(
this.state.searchTerm.toLowerCase()) !== -1;
})
})
This appears to work, insofar as I can confirm (i.e. I put a whole bunch of console.logs throughout an expanded version of the above) that as the searchTerm state is updated, it returns matching the participant and the matching convo. However, filteredConvos is not correctly rendered to reflect the newly filtered array.
I am new to Javascript, React, and Stack Overflow. My best assessment is that I am incorrectly passing my filtered array items back to filteredConvos, but I honestly don't have a enough experience to know.
Any assistance is deeply appreciated.
Further context:
The data source I'm working with is a JSON file provided by
google of an account's Hangouts chat.
HangoutSearch.js:
class HangoutSearch extends Component {
constructor(props) {
super(props);
this.state = {
searchTerm: ''
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({
searchTerm: e.target.value
});
}
render() {
let filteredConvos = this.props.convos.filter((convo) => {
return convo.conversation.conversation.participant_data.filter(
(participant) => {
return participant.fallback_name.toLowerCase().indexOf(
this.state.searchTerm.toLowerCase()) !== -1;
})
})
return(
<div>
<Form>
<Form.Control
placeholder='Enter the name of the chat participant'
value={this.state.searchTerm}
onChange={this.handleChange} />
</Form>
<HangoutList filteredConvos={filteredConvos}/>
</div>
);
}
}
export default HangoutSearch;
HangoutList.js
class HangoutList extends Component {
render() {
return(
<ListGroup>
{this.props.filteredConvos.map((convo) => {
return (
<ListGroup.Item key={convo.conversation.conversation.id.id}>
{convo.conversation.conversation.participant_data.map(
(participant) => {
return (
<span key={participant.id.gaia_id}>
{participant.fallback_name}
</span>
)
}
)}
</ListGroup.Item>
)
})}
</ListGroup>
);
}
}
export default HangoutList;
The inner .filter always returns an array, which are truthy in Javascript. You could use .some instead:
let filteredConvos = this.props.convos.filter((convo) => {
return convo.conversation.conversation.participant_data.some((participant) => {
return participant.fallback_name.toLowerCase().indexOf( this.state.searchTerm.toLowerCase()) !== -1;
})
})

What is the best way to handle sorting data that is passed from props?

It is considered bad practice to assign props to a components state. But sometimes this seems necessary so that you can mutate that state and have the component rerender.
Currently my source of data is passed in from props, then I want to assign it to state in my constructor so that it defaults. Then when an button is clicked, I want to sort / filter that data then set state and have the component re-render.
class UsersList extends Component {
constructor(props){
super(props)
this.state = {
users: props.users
}
}
filterByLastName = (lastName) => {
const { users } = this.state
const filtered = users.map(u => u.lastName === lastName)
this.setState({ users: filtered })
}
render(){
const { users } = this.state
return(
<>
<button onClick={this.filterByLastName("Smith")}>Filter</button>
<ul>
{
users.map(u => (
<>
<li>{u.firstName}</li>
<li>{u.lastName}</li>
</>
)
)}
</ul>
</>
)
}
}
The problem with doing this is the if the components props change i.e. this.props.users, it will cause a rerender but the constructor wont get called so the component will still have the old list of users assigned to it's props.users value. You could use componentDidUpdate (or another life cycle method) but this seems messy and over complicated.
If your data comes from props, it seems bad practice to assign it to state as you end up with 2 sources of truth.
What is the best way around this?
This is what getDerivedStateFromProps is for. It calculates derived state on each component update.
It's necessary to keep all and filtered users separate because the list of filtered users needs to be reset on filter change.
const filterByLastName = (users, lastName) => users.map(u => u.lastName === lastName);
class UsersList extends Component {
static getDerivedStateFromProps(props, state) {
return {
users: state.filter ? props.users : filterByLastName(props.users, state.filter)
}
}
render(){
const { users } = this.state
return(
<>
<button onClick={() => this.setState({ filter: "Smith" })}>Filter</button>
<ul>
{
users.map(u => (
<>
<li>{u.firstName}</li>
<li>{u.lastName}</li>
</>
)
)}
</ul>
</>
)
}
}
The source of triggering the render is some sort of "active the filter" action, right? I would move filterByLastName logic to the beginning of the render method, then have a nameFilter var at state and make filterByLastName method set it instead.
This way you can check nameFilter state var to apply the filtering on the incoming prop at render, so render will only occur when filter is changed and you keep a single source of truth with no need to save the prop into state.
Solution on posted code
Not tested but I guess it could be something like this:
class UsersList extends Component {
state = {
nameFilter: null
}
filterByLastName = lastName => this.setState({ nameFilter: lastName })
render(){
const { users } = this.props
const { nameFilter } = this.state
const filtered = nameFilter ? users.filter(u => u.lastName === nameFilter) : users
return(
<>
<button onClick={() => this.filterByLastName("Smith")}>Filter</button>
<ul>
{
filtered.map(u => (
<>
<li>{u.firstName}</li>
<li>{u.lastName}</li>
</>
)
)}
</ul>
</>
)
}
}

Return JSX from component method to render method

I'm still new to React. I'm trying to render a jsx under a condition defined in another method under my class component like so:
isWinner = () => {
let userVotesCount1 = this.state.user1.userVotesCount;
let userVotesCount2 = this.state.user2.userVotesCount;
if (userVotesCount1 > userVotesCount2) {
userVotesCount1++;
this.setState({ user1: { userVotesCount: userVotesCount1 } });
return (
<h3>Winner</h3>
);
}
userVotesCount2++;
this.setState({ user2: { userVotesCount: userVotesCount2 } });
return (
<h3>Loser</h3>
);}
and i'm calling this method inside the render method
<Dialog
open={open}
onRequestClose={this.onClose}
>
<div>
<isWinner />
</div>
</Dialog>
already tried to use replace <isWinner /> for {() => this.isWinner()}and I never get the return from the method. What I am doing wrong? Since I'm dealing with state here I wouldn't know how to do this with outside functions. For some reason this function is not being called ever. Please help!
You're almost there. What you want to do is use the method to set a flag, and then use the flag in the render method to conditionally render.
constructor(props) {
...
this.state = {
isWinner: false,
...
}
}
isWinner() {
...,
const isWinner = __predicate__ ? true : false;
this.setState({
isWinner: isWinner
});
}
render() {
const { isWinner } = this.state;
return isWinner ? (
// jsx to return for winners
) : (
// jsx to return for lossers
)
}

Categories

Resources