I have a store setup that has multiple arrays
I'm trying to search all arrays at once, via a textfield.
I can get this done, by calling a selector function on keyup, that filters the 4 arrays and pushes to a new array.
I've thought about merging all the arrays to one array before filtering, but I want to keep the results separate, as they are going to be displayed in categories.
Just trying to see if I can streamline the performance at all and if there's a more concise way of doing this, in case I need to do something similar with larger arrays.
my textField function:
this.renderer.listen(this.serachField.nativeElement, 'keyup', (event) => {
if (this.serachField.nativeElement.value.length < 3) { return; }
this.store.pipe(select(search(this.serachField.nativeElement.value)),take(1))
.subscribe(data => {
console.log(data);
});
})
The selector function:
export const search = (searchString: string): any => {
return createSelector(
appState,
(state: AppState) => {
let arr: any = [];
let s = searchString.toUpperCase();
const comics = state.comics.results.filter((item: Comic) => {
let title = item.title.toUpperCase();
console.log(title);
return title.includes(s);
});
const music = state.music.items.filter((item: Album) => {
let title = item.name.toUpperCase();
console.log(title);
return title.includes(s);
});
const movies = state.movies.results.filter((item: Movie) => {
let title = item.title.toUpperCase();
console.log(title);
return title.includes(s);
});
const games = state.games.filter((item: Game) => {
let title = item.title.toUpperCase();
console.log(title);
return title.includes(s);
});
arr.push(comics, music, movies, games);
return arr;
}
);
};
EDIT: After #GustavMH correct answer I had to slightly change the code to be a little more dynamic in terms of the array naming as follows
export const search = (searchString: string): any => {
return createSelector(
appState,
(state: any) => {
let s = searchString.toUpperCase();
const keys = [{state: "comics", item: "title", array: 'results'},
{state: "music", item: "name", array: 'items'},
{state: "movies", item: "title", array: 'results'},
{state: "games", item: "title", array: ''}]
return keys.map((key) => {
let arr = key.array ? state[key.state][key.array] : state[key.state];
return arr.filter((item: any) => {
const title = item[key.item].toUpperCase();
console.log(item);
return title.includes(s);
})})
}
);
};
This should implement the selector function with less code and make it more adaptable to kinds of data, if needed you can specify a more precise type in the filter function.
export const search = (searchString: string): any => {
return createSelector(
appState,
(state: AppState) => {
let s = searchString.toUpperCase();
const keys = [{state: "comics", item: "title"},
{state: "music", item: "name"},
{state: "movies", item: "title"},
{state: "games", item: "title"}]
return keys.map(key => state[key.state].results.filter((item: any) => {
const title = item[key.item].toUpperCase();
console.log(title);
return title.includes(s);
}))
}
);
};
Related
I want to apply server side search filter by text using redux toolkit.
I have two query builder methods in place. One for fetching all items and second for fetching only filtered data.
Query builder for fetching all items is
getAllBlogs: builder.query<BlogType[], void>({
queryFn: async () => {
const collectionRef = collection(Firestore, BLOG_COLLECTION)
const q = query(collectionRef, limit(1000))
const resp = await getDocs(q)
return {
data: resp.docs.map((doc) => doc.data() as BlogType),
}
},
providesTags: (result) => {
const tags: { type: 'Blogs'; id: string }[] = [
{ type: 'Blogs', id: 'LIST' },
]
if (result) {
result.forEach(({ id }) => {
tags.push({
type: 'Blogs',
id,
})
})
}
return tags
},
}),
This works fine and I'm getting the whole list through useGetAllBlogsQuery data.
Query builder for fetching filtered data is here: (Partially completed)
getBlogsByTitle: builder.query<BlogType[], string>({
queryFn: async (title) => {
const collectionRef = collection(Firestore, BLOG_COLLECTION)
const q = query(
collectionRef,
where('searchIndex', 'array-contains', title),
limit(1000),
)
const resp = await getDocs(q)
return {
data: resp.docs.map((doc) => doc.data() as BlogType), // Correct data
}
},
// I'm trying to only push the resultant items in state. This is not working
providesTags: (result) => {
const tags: { type: 'Blogs'; id: string }[] = []
if (result) {
result.forEach(({ id }) => {
tags.push({
type: 'Blogs',
id,
})
})
}
return tags
},
}),
I have react component looks like this where I'm calling these queries.
const Blogs: NextPage = () => {
const { data: blogs } = blogsApi.useGetAllBlogsQuery()
const [getBlogsByTitle] = blogsApi.useLazyGetBlogsByTitleQuery()
const debounced = useDebouncedCallback(async (value) => {
const { data } = await getBlogsByTitle(value)
console.log(data) // Correct data
}, 500)
return (
<div>
<InputText
onChange={(e) => debounced(e.target.value)}
/>
</div>
)}
The above code has two functionalities.
Fetch all the items on initial load.
Filter when debounced function is being called.
What I want is when getBlogsByTitle is called it will auto update the same state blogs in redux and we don't have to do much.
We are getting correct response in getBlogsByTitle but this query is not updating state with only its filtered response.
I'm new to redux-toolkit. Can someone help me out here where am I doing wrong ?
I've got the following data structure stored in a useState hook.
const [data, setData] = useState([
{
id: uniqid(),
title: "",
content:[
id: uniqid(),
title: "",
]
},
{
id: uniqid(),
title: "",
content:[
id: uniqid(),
title: "",
],
}
])
I've got a button where the user can add something to the content array, and I'm calling handleReport as below -
const handleAddReport = uniqueID =>{
const object = {
id: uniqid(),
title:"",
}
const formData = [...data];
formData.map(section=>{
section.content.map(report=>{
if(report.id === uniqueID){
section.content.push(object);
};
});
});
setForm(formData);
}
However, this isn't changing the form data at all. I'm not exactly sure how I could get it to work, any help would be appreciated! Thanks
you are not returning anything from the map.
const handleAddReport = uniqueID =>{
const object = {
id: uniqid(),
title:"",
}
const formData = [...data];
const newData = formData.map(section=> {
if(section.id === uniqueID){
section.content.push(object);
}
return section;
});
setForm(newData);
}
But instead of comparing the unqiueId another approach would be to pass the index of your data array. So that we can avoid map .
const handleAddReport = dataIndex =>{
const object = {
id: uniqid(),
title:"",
}
// deep clone the data
const clonedData = JSON.parse(JSON.stringify(data));
// since the data is cloned you can mutate it directly
clonedData[dataIndex].content.push(object);
setForm(clonedData)
}
Try this way
const onAddReport = (uniqid: number) => {
const obj = {
id: 3,
title: ''
};
const formData = [...data];
const mappedData = formData.map((data) => data.id === uniqid ? ({ ...data, content: [...data.content, obj] }) : data)
setData(mappedData);
}
What is wrong with the below function, and how can I fix it.
list() {
return this.bookRepo.all().then(books =>
Promise.all(books.map(book =>
Promise.all([
this.bookRepo.contents(book.id),
this.bookRepo.chapters(book.id)
]).then(([contents, chapters]) =>
Promise.all(chapters.map(chapter =>
Promise.all([
this.bookRepo.contents(chapter.id),
this.bookRepo.sections(chapter.id)
]).then(([contents, sections]) =>
Promise.all(sections.map(section =>
this.bookRepo.contents(section.id).then(contents => [
section, contents
])))
.then(sections => [chapter, contents, sections]))))
.then(chapters => [book, contents, chapters])))));
}
Problem is the return value of this function. I don't want to maintain it, write types for it, traverse it for rendering etc. It seems cumbersome, how can I abstract this or make it simpler?
This is my schema:
type BookId = string
type ChapterId = string
type SectionId = string
type ContentId = string
export type SourceId =
| ChapterId
| SectionId
| BookId
type Book = {
id: BookId,
name: string,
}
type Chapter = {
id: ChapterId,
bookId: BookId,
name: string
}
type Section = {
id: SectionId,
chapterId: ChapterId,
name: string
}
type Content = {
id: ContentId,
name: string,
sourceId: SourceId,
content: string
}
Answer: It's horrible and nobody would ever want to read or modify code written that way unless they like to torture themselves for a living.
The below is marginally more maintainable, principally through avoiding a christmas-tree of promises and giving things different names, although you'd have to get the types to align better with yours, and maybe hoist some of these loops steps into their own functions to finish the job.
type Id = string;
interface Entry {
id: Id;
}
interface Doc {}
interface BookRepo {
all: () => Promise<Array<Entry>>;
sections: (id: Id) => Promise<Array<Entry>>;
chapters: (id: Id) => Promise<Array<Entry>>;
contents: (id: Id) => Promise<Doc>;
}
class Lister {
constructor(readonly bookRepo: BookRepo) {}
async list() {
const { all, contents, sections, chapters } = this.bookRepo;
const allBooks = await all();
for (const bookRecord of allBooks) {
const bookContents = await contents(bookRecord.id);
const chapterRecords = await chapters(bookRecord.id);
const bookChapters = await Promise.all(
chapterRecords.map(async (chapterRecord) => {
const chapterContents = await contents(chapterRecord.id);
const chapterSectionRecords = await sections(chapterRecord.id);
const chapterSections = await Promise.all(
chapterSectionRecords.map(async (sectionRecord) => {
const sectionContents = await contents(sectionRecord.id);
return [sectionRecord, sectionContents];
})
);
return [chapterRecord, chapterContents, chapterSections];
})
);
return [bookRecord, bookContents, bookChapters];
}
}
}
Here I validate if my users status is true, and if they are, I put them in an array. The thing here is that next time it will validate, all those who already was true will be added to the same array. Can it be solved by filter instead of push, or should I take the validation in any other way?
import {
UPDATE_LIST_SUCCESS
} from './types'
var arr = []
export const fetchList = () => {
return (dispatch) => {
firebaseRef.database().ref().child('users')
.on('value', snapshot => {
snapshot.forEach(function (child) {
var data = child.val()
if (child.val().profile.status === true) {
arr.push(data)
}
})
dispatch({ type: UPDATE_LIST_SUCCESS, payload: arr })
})
}
}
You can do it like this:
import {
UPDATE_LIST_SUCCESS
} from './types'
export const fetchList = () => {
return (dispatch) => {
firebaseRef.database().ref().child('users')
.on('value', snapshot => {
var arr = snapshot.filter(function (child) {
return child.val().profile.status === true
}).map(function (child) {
return child.val();
});
dispatch({ type: UPDATE_LIST_SUCCESS, payload: arr })
})
}
}
So here is my not so pretty way of solving it, but it works.
import {firebaseRef} from '../firebase/firebase'
import {
UPDATE_LIST_SUCCESS
} from './types'
export const fetchList = () => {
return (dispatch) => {
const arrayToFilter = []
firebaseRef.database().ref().child('users')
.on('value', snapshot => {
let snap = snapshot.val()
// Get acces to the keys in the object i got from firebase
let keys = Object.keys(snap)
// iterate the keys and put them in an User object
for (var i = 0; i < keys.length; i++) {
let k = keys[i]
let name = snap[k].profile.name
let age = snap[k].profile.age
let status = snap[k].profile.status
let profile_picture = snap[k].profile.profile_picture
let users = {name: '', age: '', status: Boolean, profile_picture: ''}
users.name = name
users.age = age
users.status = status
users.profile_picture = profile_picture
// adding the user object to an array
arrayToFilter.push(users)
}
// filter and creates a new array with users depending if their status is true
let arr = arrayToFilter.filter(child => child.status === true)
dispatch({ type: UPDATE_LIST_SUCCESS, payload: arr })
})
}
}
I'm trying to learn some CycleJS and I ended up not knowing what to do exactly with this. The goal is to create inputs via configuration, instead of declaring them manually. The problem is I'm only getting rendered the last of the inputs from the array, instead of both. I'm assume the error is in view$ and how I'm dealing with the stream.My naive implementation is this:
Main.js
const sliderGunProps$ = xs.of({
value: 30,
id: 'gun'
});
const sliderCannonProps$ = xs.of({
value: 70,
id: 'cannon'
});
const propsConfig = [sliderGunProps$, sliderCannonProps$];
function view$(state$) {
return xs.fromArray(state$)
.map(state => {
return xs.combine(state.sliderVDom$, state.values)
.map(([sliderVDom, value]) =>
div([sliderVDom, h1(value)])
);
})
.flatten();
}
function model(actions$) {
return actions$.map((action) => {
const sliderVDom$ = action.DOM;
const sliderValue$ = action.value;
const values$ = sliderValue$.map(val => val);
return {
sliderVDom$: sliderVDom$,
values: values$
};
});
}
function intent(sources) {
return propsConfig.map(prop$ => Slider({
DOM: sources.DOM,
props$: prop$
}));
}
function main(sources) {
const actions$ = intent(sources);
const state$ = model(actions$);
const vdom$ = view$(state$);
const sink = {
DOM: vdom$
};
return sink;
}
Thanks!
I ended up figuring out how to solve it. The point was that I was not understanding how view$ handle the streams. The proper code:
function total(values) {
return xs.combine(...values)
.map(val => val.reduce((acc, x) => acc + x));
}
function view$(state$) {
const DOMElements = state$.map(slider => slider.sliderVDom$);
const values = state$.map(slider => slider.values);
const totalValue$ = total(values);
return xs.combine(totalValue$, ...DOMElements)
.map(([totalValue, ...elements]) => (
div([
...elements,
h1(totalValue)
])
));
}
function model(actions$) {
return actions$.map((action) => ({
sliderVDom$: action.DOM,
values: action.value.map(val => val)
}));
}
function intent(sources) {
return propsConfig.map(prop$ => Slider({
DOM: sources.DOM,
props$: prop$
}));
}