Can React selectors return jsx? - reactjs

When I write my React applications I keep all state, props calculations in separate functions. I call them selectors, though I'm not sure if those are selectors. By definition, selectors are functions which return subsets of state or props. I have several questions concerning this issue:
1) Can selectors return jsx objects?
For example, I would like to populate <Item /> into another component
selectorItem = (state, props) => {
const { items } = props;
const ItemsJSX = items.map((item) => {
return (
<Item
item={item}
key={item.id}
/>
)
});
return {
items: ItemsJSX
}
}
class Page extends Component {
render() {
return (
<List
{...selectorItem(this.state, this.props)}
/>
)
}
}
Is this a valid selector?
If not, how can I populate JSX into another component using selectors?
2) Should I write selector per component (selector returns object with multiple props) or per prop (separate selector per prop)?
selectorItemsComments = (state, props) => {
const { items } = props;
const { comments } = props;
const ItemsJSX = items.map((item) => {
return (
<Item
item={item}
key={item.id}
/>
)
});
const CommentsJSX = comments.map((comment) => {
<Comment
comment={comment}
key={comment.id}
/>
});
return {
items: ItemsJSX,
comment: CommentsJSX
}
}
versus
selectorItems = (state, props) => {
const { items } = props;
const ItemsJSX = items.map((item) => {
return (
<Item
item={item}
key={item.id}
/>
)
});
return {
items: ItemsJSX
}
}
selectorComments = (state, props) => {
const { comments } = props;
const CommentsJSX = comments.map((comment) => {
return (
<Comment
comment={comment}
key={comment.id}
/>
)
});
return {
comment: CommentsJSX
}
}
Thank you

These are not selectors the are just functions returning components
TL;DR
The short answer is yes you can return Components from functions.
Full Answer
In your simpler first example you should just return the component array from your selectorItem (I have renamed this to renderItems) function:
const renderItems = (items) => items.map((item) =>
<Item
item={item}
key={item.id}
/>
);
const renderComments = (comments) => comments.map((comment) =>
<Comment
item={comment}
key={comment.id}
/>
);
class Page extends Component {
render() {
const {items, comments} = this.props;
return (
<List>
{renderItems(items)}
{renderComments(comments)}
</List>
);
}
}
I would recommend converting renderItems to a Component and then you could do something like:
<List>
<Items items={items} />
<Comments comments={comments} />
</List>

If I understand correctly, your selector is still a React component. This pattern is called "Smart vs Dumb" component, or Presentational and Container Components.
Is HOCs (Higher Order Component) what you want ?

Related

TypeError: products.map is not a function. how do I fix this

I've looked at similar questions, but haven't found an answer that actually works. Here is my code for GridView:
const GridView = ({ products }) => {
return (
<Wrapper>
<div className="products-container">
{products.map((product) => {
return <Product key={product.id} {...product} />
})}
</div>
</Wrapper>
)
}
here is my code for ProductList:
const ProductList = () => {
const {filtered_products:products} = useFilterContext();
return <GridView products={products}>games list</GridView>
}
here is my code for FilteredContext:
const initialState = {
filtered_products:[],
all_products:[]
}
const FilterContext = React.createContext()
export const FilterProvider = ({ children }) => {
const {products} = useProductsContext();
const[state,dispatch] = useReducer(reducer, initialState)
useEffect(() => {
dispatch({type:LOAD_PRODUCTS, payload:products})
},[products])
return (
<FilterContext.Provider value={{...state}}>
{children}
</FilterContext.Provider>
)
}
Use conditional rendering, in case of products is null or undefined then you might get this error.
const GridView = ({ products }) => {
return (
<Wrapper>
<div className="products-container">
{products && products.map((product) => {
return <Product key={product.id} {...product} />
})}
</div>
</Wrapper>
)
}
also, make sure that products is an array.
add Elvis operator (? question mark is Elvis operator) after the products for map, this operator check if products isn't null, run the map code and if it is null won't run the map
const GridView = ({ products }) => {
return (
<Wrapper>
<div className="products-container">
{products?.map((product) => {
return <Product key={product.id} {...product} />
})}
</div>
</Wrapper>
)
}
refer to this link for more info about what is Elvis operator?
In the ProductList you should be passing filtered_products instead of products to the GridView
const ProductList = () => {
const {filtered_products} = useFilterContext();
return <GridView products={filtered_products}>games list</GridView>
}
Additionally make sure the product variable is defined before using map on it. You can do that using conditional rendering https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator
const GridView = ({ products }) => {
return (
products && <Wrapper>
<div className="products-container">
{products.map((product) => {
return <Product key={product.id} {...product} />
})}
</div>
</Wrapper>
)
}

Unable to update props in child component

This is my parent Component having state ( value and item ). I am trying to pass value state as a props to child component. The code executed in render method is Performing toggle when i click on button. But when i call the list function inside componentDidMount, Toggle is not working but click event is performed.
import React, { Component } from 'react'
import Card from './Components/Card/Card'
export class App extends Component {
state = {
values : new Array(4).fill(false),
item : [],
}
toggleHandler = (index) => {
console.log("CLICKED");
let stateObject = this.state.values;
stateObject.splice(index,1,!this.state.values[index]);
this.setState({ values: stateObject });
}
list = () => {
const listItem = this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})
this.setState({ item : listItem });
}
componentDidMount(){
// if this is not executed as the JSX is render method is executed everything is working fine. as props are getting update in child component.
this.list();
}
render() {
return (
<div>
{/* {this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})
} */}
{this.state.item}
</div>
)
}
}
export default App
This is my child Component where the state is passed as props
import React from 'react'
const Card = (props) => {
return (
<div>
<section>
<h1>Name : John Doe</h1>
<h3>Age : 20 </h3>
</section>
{props.show ?
<section>Skills : good at nothing</section> : null
}
<button onClick={props.toggleHandler} >Toggle</button>
</div>
)
}
export default Card
I know the componentDidMount is executed only once. but how to make it work except writing the JSX directly inside render method
make a copy of the state instead of mutating it directly. By using [...this.state.values] or this.state.values.slice()
toggleHandler = (index) => {
console.log("CLICKED");
let stateObject = [...this.state.values]
stateObject = stateObject.filter((_, i) => i !== index);
this.setState({ values: stateObject });
}
Also in your render method, this.state.item is an array so you need to loop it
{this.state.item.map(Element => <Element />}
Also directly in your Render method you can just do
{this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})}
In your card component try using
<button onClick={() => props.toggleHandler()}} >Toggle</button>
Value should be mapped inside render() of the class component in order to work
like this:
render() {
const { values } = this.state;
return (
<div>
{values.map((data, index) => {
return (
<Card
key={index}
show={values[index]}
toggleHandler={() => this.toggleHandler(index)}
/>
);
})}
</div>
);
}
check sandbox for demo
https://codesandbox.io/s/stupefied-spence-67p4f?file=/src/App.js

How to pass comp A variable to prop.children which in turn are the result of an array.map within the comp A

An example will explain better then I can ever formulate it
In below code, how to pass the index to Child when Child is passed in as a prop
const ComponentA = (props) => {
return (
<div>
{ data.map((item,index) => {
return (
<Child index={index} />
);
})
}
</div>
)
}
So my component A would look like this, and I want to pass the index to (each of) the children
const ComponentA = (props) => {
return (
<div>
{ data.map((item,index) => {
return (
{props.children}
);
})
}
</div>
)
}
thanks
Use React.cloneElement:
return <div>{React.cloneElement(props.children, { index: index })}<\div>
https://reactjs.org/docs/react-api.html#cloneelement
How to pass props to {this.props.children}

How to pass props to components described in const in ReactJS

I'm using the react-sortable-hoc library to sort my list of items. More than just listing I need to run a functionality when clicking the single item.
Listing and sorting and everything is working fine. How can I pass a props that should be called clickFunction() which consoles the name when I click the name listed through SortableItem?
import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc';
const SortableItem = SortableElement(({value}) => <li>{value.first_name}</li>);
const SortableList = SortableContainer(({items}) => {
return (
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} value={value} />
))}
</ul>
);
});
class Details extends React.Component {
clickFunction(name) {
console.log(name)
}
onSortEnd({oldIndex, newIndex}) {
this.setState({
testlist: arrayMove(this.state.testlist, oldIndex, newIndex),
});
};
render() {
return (
<div>
<SortableList items={this.state.testlist} onSortEnd={this.onSortEnd.bind(this)} pressDelay="200" />
</div>
)
}
}
You can pass the function from Details component and receive it in props of the SortableList and SortableItem like
import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc';
const SortableItem = SortableElement(({value, clickFunction}) => <li onClick={() => clickFunction(value)}>{value.first_name}</li>);
const SortableList = SortableContainer(({items, clickFunction}) => {
return (
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} clickFunction={clickFunction} value={value} />
))}
</ul>
);
});
class Details extends React.Component {
clickFunction(name) {
console.log(name)
}
onSortEnd({oldIndex, newIndex}) {
this.setState({
testlist: arrayMove(this.state.testlist, oldIndex, newIndex),
});
};
render() {
return (
<div>
<SortableList items={this.state.testlist} onSortEnd={this.onSortEnd.bind(this)} pressDelay="200" clickFunction={this.clickFunction} />
</div>
)
}
}
This is an example of passing props to SortableContainer:
const SortableList = SortableContainer(({items, props, handleClickFunction, fieldSelector}) => {
const subComponentList = [];
items.map((value, index) =>
subComponentList.push(
<SortableItem
key={`item-${index}`}
index={index}
value={value}
props={props}
handleClickFunction={handleClickFunction}
fieldSelector={fieldSelector}
/>)
);
return (
<ul>{subComponentList}</ul>
);
});

Is having a lot of render methods within a components bad practice?

I had written a component and there are some other instances of small components within that components. Is it a bad practice to use a renderxxx for each instance? thank you
It depends. If you are using some kind of logic for rendering your components, then you should go with dedicated renderX methods. But if all you are doing is to return the components, you should simply describe it with JSX.
Without logic
const MyComponent = ({ options }) => (
<div>
<SelectList options={options} />
</div>
);
With logic
const MyComponent = ({ options }) => {
const renderOptions = () => {
if (options.length < 5) {
return <RadioButtonGroup options={options} />;
}
return <SelectList options={options} />;
};
return (
<div>
{renderOptions()}
</div>
);
};
Another approch would be wrapping another component around every renderX method.
const OptionsRenderer = ({ options }) => {
if (options.length < 5) {
return <RadioButtonGroup options={options} />;
}
return <SelectList options={options} />;
};
const MyComponent = ({ options }) => (
<div>
<OptionsRenderer options={options} />
</div>
);

Resources