Setting React key prop to dynamic components array after instantiating - javascript

I have a method that returns an array of components that can be comletely different:
renderComponents() {
const children = [];
children.push(this.renderComponent1());
children.push(this.renderComponent2());
if (something) {
children.push(this.renderComponent3());
}
return children;
}
But of course I'm getting an error Each child in an array or iterator should have a unique "key" prop.. I've tried to set key like this:
children.forEach((child, i) => {
Object.defineProperty(child.props, 'key', { value: i });
});
But as it turns out React prevents extension of props so I've received Cannot define property key, object is not extensible.
So my question is next: is it possible to set key prop to each component in an array after instantiating of those components?
UPD: The real code is next (it renders a pagination with ranges like this [1]...[5][6][7][8][9]...[100]):
renderPaginationButton(page) {
const { query, currentPage } = this.props;
return (
<Link
className={classNames(styles.link, { [styles.active]: page === currentPage })}
to={routes.searchUrl({ ...query, page })}
>
{page}
</Link>
);
}
renderPaginationSeparator() {
return (
<div className={styles.separator}>...</div>
);
}
renderPaginationRange(from, amount) {
const { pagesCount } = this.props;
const result = [];
for (let i = Math.max(from, 1); i < Math.min(from + amount, pagesCount); i++) {
result.push(this.renderPaginationButton(i));
}
return result;
}
renderPagination() {
const { currentPage, pagesCount } = this.props;
if (pagesCount <= 1) {
return;
}
const children = this.renderPaginationRange(currentPage - 2, 5);
if (currentPage > 4) {
children.unshift(
this.renderPaginationButton(1),
this.renderPaginationSeparator()
);
}
if (pagesCount - currentPage > 4) {
children.push(
this.renderPaginationSeparator(),
this.renderPaginationButton(pagesCount)
);
}
return (
<div className={styles.pagination}>
{children}
</div>
);
}

To answer your question directly, you can use React.cloneElement to add props onto an already instantiated component.
But that's not what you should do in this case.
In your case, you should have renderPaginationButton() to return a <Link> element with key= prop already put in.

Related

Passing Parameters of a Function React

In my application, I want to conditionally render something, so I made a function getItem which I want to call in my custom Tooltip, const CustomTooltip.
Seen in the code below, I want to pass payload in const CustomTooltip = ({ active, payload, label} to function getState({payload}
I try to do this by {getState(payload, "A")} However, when I do so, I get this error:
Type 'Payload<ValueType, NameType>[]' has no properties in common with type 'TooltipProps<ValueType, NameType>'
Note: I am new to React
const numberStates = 3;
function getState({payload}: TooltipProps<ValueType, NameType>, state: string ){
if(payload){
for(let i = 0; i < numberStates; i++){
if(payload[i].dataKey == state){
return <p>{ payload[i] } : { payload[i].value }</p>
}
}
}
return null;
}
const CustomTooltip = ({ active, payload, label}: TooltipProps<ValueType, NameType>) => {
if(active && payload && payload.length){
return (
<div className = "custom-tooltip">
{getState(payload, "A")}
{getState(payload, "B")}
{getState(payload, "C")}
</div>
);
}
return null;
}
You are passing payload which is of type: Payload<ValueType, NameType>[]
But in getState function you expect TooltipProps<ValueType, NameType>
Change code to this:
function getState(payload: Payload<ValueType, NameType>[], state: string) {
if (payload) {
for (let i = 0; i < numberStates; i++) {
if (payload[i].dataKey === state) {
return (
<p>
{payload[i]} : {payload[i].value}
</p>
);
}
}
}
return null;
}

Executing a loop in React class component

I'm building a pagination component and I'm struggling to execute a for loop so I can dynamically generate the pages. I initially had a function component, but I want to switch it to a class component so I can manage state in it. (I know, I can use hooks, but Im practicing class components at the moment).
I initially added the for loop in the render method but it is executing the loop twice because the component ir rendering twice. Then, I tried componentDidMount() but it doesn't do anything... then used componentWillMount() and it worked. However, I know this could be bad practice.
Any ideas? See below the component with componentDidMount()
import React, { Component } from 'react';
import styles from './Pagination.module.css';
class Pagination extends Component {
state = {
pageNumbers: [],
selected: '',
};
componentDidMount() {
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
this.state.pageNumbers.push(i);
}
}
classActiveForPagineHandler = (number) => {
this.setState({ selected: number });
};
render() {
return (
<div className={styles.PaginationContainer}>
<nav>
<ul className={styles.PageListHolder}>
{this.state.pageNumbers.map((num) => (
<li key={num}>
<a
href="!#"
className={
this.state.selected === num
? styles.Active
: styles.PageActive
}
onClick={() => {
this.props.paginate(num);
// this.props.classActiveForPagineHandler(num);
}}
>
{num}
</a>
</li>
))}
</ul>
</nav>
</div>
);
}
}
export default Pagination;
You better push all the numbers into array and then update pageNumbers state. this.state.pageNumbers.push(i); does not update state directly, you need use setState after your calculation completes.
componentDidMount() {
const { pageNumbers = [] } = this.state
const { totalDogs, dogsPerPage } = this.props
for (let i = 1; i <= Math.ceil(totalDogs / dogsPerPage); i++) {
pageNumbers.push(i);
}
this.setState({ pageNumbers })
}
Demo link here
you should not update state like this :
this.state.pageNumbers.push(i);
do this:
this.setState((s) => {
return {
...s,
pageNumbers: [...s.pageNumbers, i]
}
})
Do not mutate state directly in react component. Use setState for all updates.
componentDidMount() {
const pageNumbers = [];
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
pageNumbers.push(i);
}
this.setState({ pageNumbers });
}
Alternatively, you can simplify the code using Array.from for this case.
componentDidMount() {
this.setState({
pageNumbers: Array.from(
{ length: Math.ceil(this.props.totalDogs / this.props.dogsPerPage) },
(_, i) => i + 1
),
});
}

Gatsby function returns undefined

I have a file where I try to determine which data should be used in a Gatsby template. I get an array that contains child pages in return, these child pages may contain other child pages. I want to support up to three levels of child pages.
I have a template where I use my paginator (component to find the correct pages), I look for correct pages to render bypassing the slug via pageContext from gatsby-node.js
Template (minus imports)
const projectsSubPages = ({ data, pageContext }) => {
return (
<Layout>
<Menu parentPage={pageContext.parentSlug} />
{data.allSanityProjects.edges.map((childNode) =>
<>
{childNode.node.childPages.length > 0 &&
<Paginator
pageData={childNode.node.childPages}
parentPage={pageContext.parentSlug}
key={childNode.node._id}
/>
}
</>
)}
</Layout>
);
};
export const query = graphql`
{
allSanityProjects {
edges {
node {
childPages {
_rawBlockContent
title
slug
childPages {
slug
title
childPages {
title
slug
childPages {
slug
title
_key
}
_key
}
_key
}
_key
}
_key
}
}
}
}
`;
export default projectsSubPages;
My paginator component (minus imports)
const subPageLevelFinder = ({ pageData, parentPage }) => {
const SubLevels = () => {
let pageLevel = "test";
if (pageData.slug === parentPage) {
pageLevel = pageData.slug
}
if (pageData.childPages && pageData.childPages.length > 0) {
pageData.childPages.map((secondLevel) => {
if (secondLevel.slug === parentPage) {
pageLevel = secondLevel.slug
return (pageLevel)
} else if (pageData.childPages.childPage && pageData.childPages.childPages.length > 0) {
secondLevel.childPages.map((thirdLevel) => {
if (thirdLevel.slug === parentPage) {
pageLevel = thirdLevel.slug
return (pageLevel)
}
})
} else {
return (
pageLevel = "No page level found"
)
}
})
}
return (
pageLevel
)
}
return (
<>
{console.log(SubLevels())}
{SubLevels()}
</>
)
};
See this gist for the return of the GraphQL query and gatsby-node.js https://gist.github.com/AndreasJacobsen/371faf073a1337b6879e4fd6b860b26f
My goal is to run a component that has a template in my paginator and passing the data this template should use from the SubLevels function, but this function returns the first set let value every time. So all of my if-statements fail, I can't figure out where the issue is, I've tried changing the if parameters several times, but this seems to fit the GraphQL query
It turns out that the error came from my trying to access array elements in a multi dimentional array.
So the array I got back had three elements, all with a slug. I tried to access the slug but in order to get that slug I had to loop through the elements.
See attached solution that works (but is not very efficient), notice that this solution has a map function at the very top level; this solved the issue.
import React from "react";
import SubPageTemplate from "./subPageTemplate";
import { Link } from "gatsby";
import { useStaticQuery, graphql } from "gatsby";
const BlockContent = require("#sanity/block-content-to-react");
const subPageLevelFinder = ({ pageData, parentPage, childSlug }) => {
const subLevels = () => {
let pageLevel = null;
pageData.map((mappedData) => {
if (mappedData.slug === childSlug) {
pageLevel = mappedData;
return pageLevel;
} else {
if (mappedData.childPages && mappedData.childPages.length > 0) {
if (mappedData.slug === childSlug) {
return (pageLevel = mappedData);
} else {
mappedData.childPages.map((secondLevel) => {
if (secondLevel.slug === childSlug) {
pageLevel = secondLevel;
return pageLevel;
} else if (
mappedData.childPages.childPage &&
mappedData.childPages.childPages.length > 0
) {
secondLevel.childPages.map((thirdLevel) => {
if (thirdLevel.slug === childSlug) {
pageLevel = thirdLevel;
return pageLevel;
}
});
}
});
}
} else {
return false;
}
}
});
return pageLevel;
};
return <>{subLevels() && <SubPageTemplate pageLevel={subLevels()} />}</>;
};
export default subPageLevelFinder;

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.

How to search data efficiently with React Native ListView

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.

Categories

Resources