How to search data efficiently with React Native ListView - javascript

I am trying to implement a filter and search function that would allow user to type in keyword and return result(array) and re-render the row
This is the event arrays that being passed in into the createDataSource function
The problem I am having now is my search function can't perform filter and will return the entire parent object although I specifically return the indexed object.
Here's what I got so far
class Search extends Component {
state = { isRefreshing: false, searchText: '' }
componentWillMount() {
this.createDataSource(this.props);
}
componentWillReceiveProps(nextProps) {
this.createDataSource(nextProps);
if (nextProps) {
this.setState({ isRefreshing: false })
}
}
createDataSource({ events }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(events);
}
//return arrays of event from events
renderRow(event) {
return <EventItem event={event} />;
}
onRefresh = () => {
this.setState({ isRefreshing: true });
this.props.pullEventData()
}
setSearchText(event) {
let searchText = event.nativeEvent.text;
this.setState({ searchText })
var eventLength = this.props.events.length
var events = this.props.events
const filteredEvents = this.props.events.filter(search)
console.log(filteredEvents);
function search() {
for (var i = 0; i < eventLength; i++) {
if (events[i].title === searchText) {
console.log(events[i].title)
return events[i];
}
}
}
}
render() {
const { skeleton, centerEverything, container, listViewContainer, makeItTop,
textContainer, titleContainer, descContainer, title, desc, listContainer } = styles;
return(
<View style={[container, centerEverything]}>
<TextInput
style={styles.searchBar}
value={this.state.searchText}
onChange={this.setSearchText.bind(this)}
placeholder="Search" />
<ListView
contentContainerStyle={styles.listViewContainer}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
refreshControl={
<RefreshControl
refreshing={this.state.isRefreshing}
onRefresh={this.onRefresh}
title="Loading data..."
progressBackgroundColor="#ffff00"
/>
}
/>
</View>
)
}
}
As you can see from the image above, my code requires me to type in the full query text to display the result. And it displays all the seven array objects? why's that?

The syntax of Array.prototype.filter is wrong... it should take a callback that will be the item being evaluated for filtering.. if you return true it will keep it.
function search(event) {
return ~event.title.indexOf(searchText)
}
You could even make the inline like..
const filteredEvents = this.props.events.filter(event => ~event.title.indexOf(searchText))
For understanding my use of ~, read The Great Mystery of the Tilde.
Since filter returns a new array, you should be able to clone your dataSource with it. If you didn't use filter, you would have to call events.slice() to return a new array. Otherwise, the ListView doesn't pickup the changes.

Related

How to hide a specific element inside .map function in React?

I am looking for a way to hide a div once the button thats in it is clicked and continue to show all other div's.
I've tried using the setState method, however when setting it to false with onClick() all of my objects disappear.
class App extends React.PureComponent {
state: {
notHidden: false,
}
constructor(props: any) {
super(props);
this.state = {search: '', notHidden: true};
this.hideObject = this.hideObject.bind(this)
}
hideThisDiv() {
this.setState({notHidden: false})
}
async componentDidMount() {
this.setState({
objects: await api.getObjects()
});
}
render = (objects: Object[]) => {
return ({Object.map((object) =>
<div key={index} className='class'>
<button className='hide' type='button' onClick={() => hideThisDiv()}>Hide</button>
<p>object.text</p>
</div>}
render() {
const {objects} = this.state;
return (<main>
<h1>Objects List</h1>
<header>
<input type="search" onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{objects ? this.render(objects) : null}
</main>)
}
);
The data is a data.json file filled with many of these objects in the array
{
"uuid": "dsfgkj24-sfg34-1r134ef"
"text": "Some Text"
}
Edit: Sorry for the badly asked question, I am new to react.
Not tested, just a blueprint... is it what you want to do?
And yes I didn't hide, I removed but again, just an idea on how you can hide button separately, by keeping in state which ones are concerned.
function MagicList() {
const [hidden, hiddenSet] = useState([]);
const items = [{ id:1, text:'hello'}, { id:2, text:'from'}, { id:3, text:'the other sided'}]
const hideMe = id => hiddenSet([...hidden, id]);
return {
items.filter( item => {
return hidden.indexOf(item.id) !== -1;
})
.map( item => (
<button key={item.id} onClick={hideMe.bind(this, item.id)}>{item.text}</button>
))
};
}
Edition
const hideMe = id => hiddenSet([...hidden, id]);
It is just a fancy way to write:
function hideMe(id) {
const newArray = hidden.concat(id);
hiddenSet(newArray);
}
I suggest using a Set, Map, or object, to track the element ids you want hidden upon click of button. This provides O(1) lookups for what needs to be hidden. Be sure to render your actual text and not a string literal, i.e. <p>{object.text}</p> versus <p>object.text</p>.
class MyComponent extends React.PureComponent {
state = {
hidden: {}, // <-- store ids to hide
objects: [],
search: "",
};
// Curried function to take id and return click handler function
hideThisDiv = id => () => {
this.setState(prevState => ({
hidden: {
...prevState.hidden, // <-- copy existing hidden state
[id]: id // <-- add new id
}
}));
}
...
render() {
const { hidden, objects } = this.state;
return (
<main>
...
{objects
.filter((el) => !hidden[el.uuid]) // <-- check id if not hidden
.map(({ uuid, text }) => (
<div key={uuid}>
<button
type="button"
onClick={this.hideThisDiv(uuid)} // <-- attach handler
>
Hide
</button>
<p>{text}</p>
</div>
))}
</main>
);
}
}

Filtering an Array within an Array in React

import React, { Component } from "react"
import {
StaticQuery,
grahpql,
Link
} from "gatsby"
import {
StyledFilter,
StyledLine
} from "./styled"
class Filter extends Component {
render() {
const { data } = this.props
const categories = data.allPrismicProjectCategory.edges.map((cat, index) => {
return (
<a
key={index}
onClick={() => this.props.setFilterValue(cat.node.uid)}
>
{cat.node.data.category.text}
</a>
)
})
return (
<StyledFilter>
<div>
Filter by<StyledLine />
<a
// onClick={() => {this.props.filterProjects("all")}}
>
All
</a>
{categories}
</div>
<a onClick={this.props.changeGridStyle}>{this.props.gridStyleText}</a>
</StyledFilter>
)
}
}
export default props => (
<StaticQuery
query={graphql`
query {
allPrismicProjectCategory {
edges {
node {
uid
data {
category {
text
}
}
}
}
}
}
`}
render={data => <Filter data={data} {...props} />}
/>
)
<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>
I am working on a React App with Gatsby and Prismic that has a project page. By default it lists all projects but at the page's top appears a filter to select by category (just a bunch of <a> tags).
My Page consists of a <Filter /> component as well as several <GridItem /> components I am mapping over and load some props from the CMS.
The part I am struggling with is the filtering by category.
When my page component mounts it adds all projects into my filteredItems state.
When a user is clicking on a filter at the top it set's my default filterValue state from "all" to the according value.
After that I'll first need to map over the array of projects and within that array I'll need to map over the categories (each project can belong to multiple categories).
My idea is basically if a value (the uid) matches my new this.state.filterValue it returns the object and add's it to my filteredItems state (and of course delete the one's not matching this criteria).
This is what my page component looks like (cleaned up for better readability, full code in the snippet at the bottom):
class WorkPage extends Component {
constructor(props) {
super(props)
this.state = {
filterValue: "all",
filteredItems: []
}
this.filterProjects = this.filterProjects.bind(this)
}
filterProjects = (filterValue) => {
this.setState({ filterValue: filterValue }, () =>
console.log(this.state.filterValue)
)
// see a few of my approaches below
}
componentDidMount() {
this.setState({
filteredItems: this.props.data.prismicWork.data.projects
})
}
render() {
const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
const item = node.project_item.document["0"].data
const categories = node.project_item.document["0"].data.categories.map(cat => {
return cat.category_tag.document["0"].uid
})
return (
<GridItem
key={index}
categories={categories}
moreContentProps={moreContentProps}
/>
)
})
return (
<LayoutDefault>
<Filter
filterProjects={this.filterProjects}
/>
{projectItems}
</LayoutDefault>
)
}
}
I tried so many things, I can't list all of them, but here are some examples:
This approach always returns an array of 10 objects (I have 10 projects), sometimes the one's that don't match the this.state.filterValue are empty objects, sometimes they still return their whole data.
let result = this.state.filteredItems.map(item => {
return item.project_item.document["0"].data.categories.filter(cat => cat.category_tag.document["0"].uid === this.state.filterValue)
})
console.log(result)
After that I tried to filter directly on the parent item (if that makes sense) and make use of indexOf, but this always console logged an empty array...
let result = this.state.filteredItems.filter(item => {
return (item.project_item.document["0"].data.categories.indexOf(this.state.filterValue) >= 0)
})
console.log(result)
Another approach was this (naive) way to map over first the projects and then the categories to find a matching value. This returns an array of undefined objects.
let result = this.state.filteredItems.map(item => {
item = item.project_item.document["0"].data.categories.map(attachedCat => {
if (attachedCat.category_tag.document["0"].uid === this.state.filterValue) {
console.log(item)
}
})
})
console.log(result)
Other than that I am not even sure if my approach (having a filteredItems state that updates based on if a filter matches the according category) is a good or "right" React way.
Pretty stuck to be honest, any hints or help really appreciated.
import React, { Component } from "react"
import { graphql } from "gatsby"
import LayoutDefault from "../layouts/default"
import { ThemeProvider } from "styled-components"
import Hero from "../components/hero/index"
import GridWork from "../components/grid-work/index"
import GridItem from "../components/grid-item/index"
import Filter from "../components/filter/index"
class WorkPage extends Component {
constructor(props) {
super(props)
this.state = {
filterValue: "all",
filteredItems: [],
isOnWorkPage: true,
showAsEqualGrid: false
}
this.filterProjects = this.filterProjects.bind(this)
this.changeGridStyle = this.changeGridStyle.bind(this)
}
changeGridStyle = (showAsEqualGrid) => {
this.setState(prevState => ({
showAsEqualGrid: !prevState.showAsEqualGrid,
isOnWorkPage: !prevState.isOnWorkPage
}))
}
filterProjects = (filterValue) => {
this.setState({ filterValue: filterValue }, () =>
console.log(this.state.filterValue)
)
let result = this.state.filteredItems.filter(item => {
return (item.project_item.document["0"].data.categories.toString().indexOf(this.state.filterValue) >= 0)
})
console.log(result)
}
componentDidMount() {
this.setState({
filteredItems: this.props.data.prismicWork.data.projects
})
}
render() {
const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
const item = node.project_item.document["0"].data
const categories = node.project_item.document["0"].data.categories.map(cat => {
return cat.category_tag.document["0"].uid
})
return (
<GridItem
key={index}
isSelected="false"
isOnWorkPage={this.state.isOnWorkPage}
isEqualGrid={this.state.showAsEqualGrid}
projectURL={`/work/${node.project_item.uid}`}
client={item.client.text}
tagline={item.teaser_tagline.text}
categories={categories}
imageURL={item.teaser_image.squarelarge.url}
imageAlt={item.teaser_image.alt}
/>
)
})
return (
<ThemeProvider theme={{ mode: "light" }}>
<LayoutDefault>
<Hero
introline="Projects"
headline="Art direction results in strong brand narratives and compelling content."
/>
{/* {filteredResult} */}
<Filter
filterProjects={this.filterProjects}
changeGridStyle={this.changeGridStyle}
gridStyleText={this.state.showAsEqualGrid ? "Show Flow" : "Show Grid"}
/>
<GridWork>
{projectItems}
</GridWork>
</LayoutDefault>
</ThemeProvider>
)
}
}
export default WorkPage
export const workQuery = graphql`
query Work {
prismicWork {
data {
page_title {
text
}
# All linked projects
projects {
project_item {
uid
# Linked Content
document {
type
data {
client {
text
}
teaser_tagline {
text
}
teaser_image {
url
alt
xlarge {
url
}
large {
url
}
medium {
url
}
squarelarge {
url
}
squaremedium {
url
}
squaresmall {
url
}
}
categories {
category_tag {
document {
uid
data {
category {
text
}
}
}
}
}
}
}
}
}
}
}
}
`
<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>
So there are at least two things.
In your filterProjects() you're first setting state.filterValue and then you use it in filteredItems.filter(). That might not work, because React does not execute setState() immediately always, to optimize performance. So you're probably filtering against the previous value of state.filterValue. Instead just use filterValue, which you pass into filterProjects().
setFilterValue = (filterValue) => {
this.setState({filterValue}) // if key and variable are named identically, you can just pass it into setState like that
}
// arrow function without curly braces returns without return statement
filterProjects = (projects, filterValue) =>
projects.filter(item => item.project_item.document[0].data.categories.toString().includes(filterValue))
You should return the result from filterProjects(), because you need to render based on the filteredItems then, of course. But actually it's not necessary to put the filter result into state. You can apply the filterProjects() on the props directly, right within the render(). That's why you should return them. Also separate setState into another function which you can pass into your <Filter/> component.
And a recommendation: Use destructuring to make your code more readable. For you and anyone else working with it.
render() {
const { projects } = this.props.data.prismicWork.data // this is
const { filterValue } = this.state // destructuring
if (projects != undefined) {
this.filterProjects(projects, filterValue).map((node, index) => {
// ...
// Filter component
<Filter filterProjects={this.setFilterValue} />
That way you trigger a rerender by setting the filterValue, because it
resides in this.state, and the render function depends on
this.state.filterValue.
Please try that out and tell me if there is another problem.

React Native - Unable to pass CheckBox value to another screen

I'm here to ask what's your idea to properly pass a CheckBox value to other screen?
Example, if a user checks a CheckBox then proceed to the Next Screen the value of the CheckBox should be displayed in that screen.
But in my code, my console.log gives an output of false(I don't understand why) and once I get to the next screen the state doesn't really pass because it's display is blank.
Here's my code
export default class tables extends Component {
constructor(props){
super(props)
this.state = {
...
check: {},
tbl_Merge: []
}
}
proceed_TO_Category = () => {
this.props.navigation.navigate('Category', {
userName : this.state.userName,
DineIn : this.state.DineIn,
tbl : this.state.tbl,
tbl_2nd : this.state.tbl_2nd,
tbl_Merge : this.state.tbl_Merge
});
console.log("CHECK ======> "+ this.state.tbl_Merge);
}
checkBox_Test = (table_NO) => {
const tbl_Merge = this.state.tbl_Merge;
const checkCopy = {...this.state.check}
if (checkCopy[table_NO]) {
checkCopy[table_NO] = false;
} else {
checkCopy[table_NO] = true;
}
this.setState({ check: checkCopy });
this.setState({ tbl_Merge: table_NO == this.state.tbl_Merge });
}
render() {
return(
<View>
....
<Flatlist
....
<CheckBox
value = { this.state.check[item.tbl_id] }
onChange = {() => this.checkBox_Test(item.tbl_id) }
/>
....
/>
....
<View>
<TouchableOpacity
onPress = {() => this.proceed_TO_Category()}>
<Text>Categories</Text>
</TouchableOpacity>
</View>
<View/>
)
}
}
Screen shot of Console.log in my proceed_TO_Category.
checkBox_Test = (table_NO) => {
const tbl_Merge = this.state.tbl_Merge;
const checkCopy = {...this.state.check}
if (checkCopy[table_NO]) {
checkCopy[table_NO] = false;
} else {
checkCopy[table_NO] = true;
}
this.setState({ check: checkCopy, tbl_Merge: table_NO == this.state.tbl_Merge }); // 1. Edit
}
Edit: We should not use setState consecutively. Because setState is async func. If you want to call a function after setState process with the help of callback function,setState(update, callback);
you defined tbl_Merge as an array, but while you set it to state, you set it as a boolean.

Array.splice works on both state sub-arrays

This is my simple react component. I set rooms firstly in state in componentWillReceiveProps and then on submit I do set rooms to data in state as well.
Also on submit I do an api call by passing single object from the rooms and when response come then I do slice that object from the data (but not from rooms) until the data length is equal to 0.
Now problem is when I do slice from the data then it slices the rooms elements as well.
class EditRoom extends Component {
constructor() {
super()
this.state = {
rooms: [],
data: []
}
}
componentWillMount() {
const { fetchRooms } = this.props
fetchRooms()
}
componentWillReceiveProps(np) {
const { rooms, setNick, setNickName } = np
if (this.props.rooms !== rooms) {
console.log('ppppppppppppp')
this.setState({ rooms })
}
if (setNick) {
const data = this.state.data
data.splice(0, 1)
this.setState({ data }, () => {
if (data.length === 0) {
console.log('pppp542545455453864')
} else {
const room = _.first(this.state.data)
setNickName(room)
}
})
}
}
handleInputChange(e, i) {
const { rooms } = this.state
rooms[i].nickname = e.target.value
this.setState({ rooms })
}
onSaveClick() {
const { setNickName } = this.props
this.setState({ data: this.state.rooms }, () => {
const room = _.first(this.state.data)
setNickName(room)
})
}
render() {
const { rooms } = this.state
return (
<div>
<main id="EditRoom">
{rooms &&
rooms.map((room, i) => {
return (
<div className="barcode-box" key={i} style={{ backgroundColor: this.getRandomColor(i) }}>
<div className="edit-room-name">
<input
type="text"
className="form-control"
style={{ color: '#ffffff' }}
name="cardNumber"
placeholder="Nickname"
value={_.get(room, 'nickname') || ''}
onChange={e => this.handleInputChange(e, i)}
/>
</div>
</div>
)
})}
</main>
</div>
)
}
}
What I am missing here?
Thank you !!!
You should not modify this.state directly, e.g. using array mutating methods like splice. Instead of this, make a copy from this.state.data sub-array, modify and pass it to setState().
Something like this:
const data = this.state.data.slice() // make a copy of data
data.splice(0, 1) // modify a copy
this.setState({ data }, ...) // pass to setState
[Update] Explanation of why changing one sub-array of state affects on another:
Arrays (as all objects) in JS are passed by reference. So if you do a simple assignment like arr2 = arr1, splice method will mutate the original array too. That's also true for nested arrays (objects) like in your case. data sub-array is stored with rooms together in state. So mutating data will affect rooms sub-array too.

React setState doesn't re-render after fetching data

This code works if data already fetched.
But doesn't work if I refresh the page and doesn't rerender element.
I'm also using Next JS if it's worth mentioning.
class Books extends Component {
constructor(props) {
super(props);
this.state = {
book: []
}
this.renderBooks= this.renderBooks.bind(this);
}
renderBooks() {
let item;
let items = [];
return new Promise((resolve, reject) => {
this.props.ids.forEach(address => {
firebase.database().ref(`/books/${address}`)
.on('value', snap => {
item = snap.val();
});
items.push(item);
})
resolve(items);
});
}
async componentDidMount() {
try {
let res = [];
res = await this.renderBooks();
console.log(res);
this.setState({ book: res });
} catch (error) {
console.log(error);
this.setState(prevState => {
return { book: 'err' }
});
}
}
render() {
return (
<div>
{ <List grid={{ gutter: 16 }}
dataSource={ this.state.book }
renderItem={ item => (
<List.Item>
<Card title={ !!item && item.title }>
...Book content...
</Card>
</List.Item>
)} />
}
</div>
);
}
}
export default Books;
Is there anything to know about setState and fetching data that I missed here?
PS. Edited constructor to book: [].
You cannot initialize book with a promise. Instead you can have a solution like below.
Add a conditional rendering to you render method so it will know when to render book. Also you don't need to return new Promise in this case.
class Books extends Component {
constructor(props) {
super(props);
this.state = { books: null }
}
componentDidMount() {
this.renderBooks()
}
renderBooks() {
this.props.ids.forEach(address => {
firebase.database().ref(`/books/${address}`)
.on('value', snap => {
this.setState({books: [...(this.state.books || []), snap.val()] });
});
});
}
render() {
return (
this.state.books ?
<div>
{ <List grid={{ gutter: 16 }}
dataSource={ this.state.books }
renderItem={ item => (
<List.Item>
<Card title={ !!item && item.title }>
...Book content...
</Card>
</List.Item>
)} />
}
</div>
: 'Initializing'
);
}
}
export default Books;
Promises are basically async functions that are resolved when it's time.
So when you do
var item, items = []; // <---- Step 1
this.props.ids.forEach(address => {
firebase.database().ref(`/books/${address}`)
.on('value', snap => {
item = snap.val(); // <--- Step 3
});
});
items.push(item); // <----- Step 2
});
The steps are like this. So you were doing items.push(item) before item was assigned a new value which is snap.val(). And that makes item undefined.
I guess the second result you have is thanks to caching. If the internet connection is SOOOO FAST Step 3 might be earlier than Step 2, but that's a bad assumption. That's why the second time you get the result correct.
In this answer's case, instead of having an items array, the snap.val() is added to this.state.books. But this makes it a bit heavyweight. Because every time a query on('value') is called, the setState method will be triggered and the component will be rendered again. If there were 1000 ids the state would change 1000 times.
That's why instead of getting the data one by one I would suggest you to get all the data at once. Try to google something like 'retrieve multiple data from firebase javascript'. Unfortunately I don't know much about firebase so cannot help there.
You are initializing this.state.book with a promise. Try setting it to null instead:
this.state = {
book: null
}

Categories

Resources