Filtering Nested Arrays in React JS - javascript

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;
})
})

Related

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.

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>
</>
)
}
}

ReactJS instant Search with input

Im making my first react project. Im new in JS, HTML, CSS and even web app programming.
What i want to do it is a Search input label. Now its look like this:
Like you can see i have some list of objects and text input.
I Have two components, my ProjectList.js with Search.js component...
class ProjectsList extends Component {
render() {
return (
<div>
<Search projects={this.props.projects} />
<ListGroup>
{this.props.projects.map(project => {
return <Project project={project} key={project.id} />;
})}
</ListGroup>
</div>
);
}
}
export default ProjectsList;
... and ProjectList.js displays Project.js:
How looks Search.js (its not ended component)
class Search extends Component {
state = {
query: ""
};
handleInputChange = () => {
this.setState({
query: this.search.value
});
};
render() {
return (
<form>
<input
ref={input => (this.search = input)}
onChange={this.handleInputChange}
/>
<p />
</form>
);
}
}
export default Search;
My project have name property. Could you tell me how to code Search.js component poperly, to change displaying projects dynamically based on input in text label? for example, return Project only, if text from input match (i want to search it dynamically, when i start typing m... it shows all projects started on m etc).
How to make that Search input properly? How to make it to be universal, for example to Search in another list of objects? And how to get input from Search back to Parent component?
For now, in react dev tools whatever i type there i get length: 0
Thanks for any advices!
EDIT:
If needed, my Project.js component:
class Project extends Component {
state = {
showDetails: false
};
constructor(props) {
super(props);
this.state = {
showDetails: false
};
}
toggleShowProjects = () => {
this.setState(prevState => ({
showDetails: !prevState.showDetails
}));
};
render() {
return (
<ButtonToolbar>
<ListGroupItem className="spread">
{this.props.project.name}
</ListGroupItem>
<Button onClick={this.toggleShowProjects} bsStyle="primary">
Details
</Button>
{this.state.showDetails && (
<ProjectDetails project={this.props.project} />
)}
</ButtonToolbar>
);
}
}
export default Project;
To create a "generic" search box, perhaps you could do something like the following:
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
This revised version of Search takes some additional props which allows it to be reused as required. In addition to the projects prop, you also pass filterProject and onUpdateProjects callbacks which are provided by calling code. The filterProject callback allows you to provide custom filtering logic for each <Search/> component rendered. The onUpdateProjects callback basically returns the "filtered list" of projects, suitable for rendering in the parent component (ie <ProjectList/>).
The only other significant change here is the addition of visibleProjects to the state of <ProjectList/> which tracks the visible (ie filtered) projects from the original list of projects passed to <ProjectList/>:
class Project extends React.Component {
render() {
return (
<div>{ this.props.project }</div>
);
}
}
class ProjectsList extends React.Component {
componentWillMount() {
this.setState({ visibleProjects : [] })
}
render() {
return (
<div>
<Search projects={this.props.projects} filterProject={ (query,project) => (project == query) } onUpdateProjects={ projects => this.setState({ visibleProjects : projects }) } />
<div>
{this.state.visibleProjects.map(project => {
return <Project project={project} key={project.id} />;
})}
</div>
</div>
);
}
}
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
ReactDOM.render(
<ProjectsList projects={[0,1,2,3]} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I will assumes both your Search and ProjectList component have a common parent that contains the list of your projects.
If so, you should pass a function into your Search component props, your Search component will then call this function when the user typed something in the search bar. This will help your parent element decide what your ProjectsLists needs to render :
handleInputChange = () => {
this.props.userSearchInput(this.search.value);
this.setState({
query: this.search.value
});
};
And now, here is what the parent element needs to include :
searchChanged = searchString => {
const filteredProjects = this.state.projects.filter(project => project.name.includes(searchString))
this.setState({ filteredProjects })
}
With this function, you will filter out the projects that includes the string the user typed in their names, you will then only need to put this array in your state and pass it to your ProjectsList component props
You can find the documentation of the String includes function here
You can now add this function to the props of your Search component when creating it :
<Search userSearchInput={searchChanged}/>
And pass the filtered array into your ProjectsList props :
<ProjectsList projects={this.state.filteredProjects}/>
Side note : Try to avoid using refs, the onCHnage function will send an "event" object to your function, containing everything about what the user typed :
handleInputChange = event => {
const { value } = event.target
this.props.userSearchInput(value);
this.setState({
query: value
});
};
You can now remove the ref from your code

React Native - Cannot get item in array, or show items from array in list

I have been trying to debug this for hours now but to no avail. I wrote this script that basically when the component loads fetch information from my Firestore database. I then log one item from the array to the console, resulting in undefined. I don't understand why it can show me the full array but not one item. And then the entire Array resulting in the full array (partial image seen below).
The main goal of this component is to produce a list of the items as seen in the list component but nothing is shown. My first guess is that method for getting information from the database is asynchronous, but would this matter if the state is updated? Shouldn't the app re-render?
Here is a snippet of my code, and help would be greatly appreciated!
export default class ItemScreen extends Component {
constructor () {
super()
this.state = {
items: [],
}
}
componentDidMount() {
var items = []
firebase.firestore().collection("items").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
items.push(doc.data());
});
})
this.setState({items: items});
console.log(items[1])
console.log(items)
}
render() {
const { items } = this.state
return (
<View style={styles.container}>
<SearchBar
lightTheme
onChangeText={(text) => console.log(text)}
onClearText={() => console.log("cleared")}
placeholder='Type Here...'
containerStyle={{width: "100%"}}
/>
<List containerStyle={{width: "100%"}}>
{
items.map((l) => (
<ListItem
roundAvatar
avatar={{uri:l.image}}
key={l.name}
title={l.name}
/>
))
}
</List>
</View>
);
}
}
Try restructuring to something more like
var items = []
var self = this;
firebase.firestore().collection("items").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
items.push(doc.data());
});
self.setState({items: items});
console.log(items[1])
console.log(items)
})
The code at the bottom of your original method would run before the results are available.

React rendering both components defined in ternary conditional

Render function:
if (filteredChildren.length === 0) {
return <tbody>No projects match filter</tbody>;
}
return (
TBody({
children: filteredChildren.map(project => (
<ProjectsRow key={ project.project_id }>
{project}
</ProjectsRow>
))
})
);
This code, by the looks of it.. should never return both branches, but yet this is what I see:
And oddly enough, when I actually have some filtervalue:
To complete this nonsense, let's simply remove one line from our code.
if (filteredChildren.length === 0) {
// return <tbody>No projects match filter</tbody>; Begone!
}
return (
TBody({
children: filteredChildren.map(project => (
<ProjectsRow key={ project.project_id }>
{project}
</ProjectsRow>
))
})
);
Now everything works, no doubled up components, but no helpful message when no projects are found:
UPDATE:
A few parents up where the filterValue is state:
class FilterCard extends Component {
constructor(props: any) {
super(props);
this.state = { filterValue: `` };
}
handleInput = (): void =>
this.setState({ filterValue: this.refs.filterInput.value });
render() {
const { children } = this.props;
return (
<Row>
<input
ref="filterInput"
onChange={this.handleInput}
type="text"
/>
{ Children.map(children, child => {
return cloneElement(child, {
filterValue: this.state.filterValue
});
})}
</Row>
);
}
}
Direct parent:
export default function ProjectsTable({
sort,
order,
router,
children,
filterValue
}: ProjectsTableProps): React.Element {
return (
<Table>
<ProjectsColumns sort={ sort } order={ order } />
<ProjectsRows router={ router } filterValue={ filterValue }>
{ children }
</ProjectsRows>
</Table>
);
}
I should try filtering the children in one of the parent components.. no need to pass the filter value down just to filter it there.
Also as a crazy side note, when inspect using Chrome elements panel, that node with the text above the table.... isn't even there!!! Ghost element...!
UPDATE 2:
Filtering in the above component fixes my issue but I would LOVE to know what's causing the error the way I had it before.. I'm guessing React does some weird stuff when working inside table elements.
Working code:
export default function ProjectsTable({
sort,
order,
router,
children,
filterValue
}: ProjectsTableProps): React.Element {
const filteredChildren = children.filter(filterProjects(filterValue));
if (!filteredChildren.length) {
return <div>No projects.</div>;
}
return (
<Table>
<ProjectsColumns sort={ sort } order={ order } />
<ProjectsRows router={ router }>
{ filteredChildren }
</ProjectsRows>
</Table>
);
}

Categories

Resources