JSX callback without passing individual props - javascript

I wonder if there is a way to replicate same usage of js callback's in JSX like:
[1,2,3].map(console.log) // Here map iterator takes care of passing values to iteratee.
Is there a similar way to pass component like that, ie. rather then this:
<List data={data} renderItem={item => <Post {...item} />} />
something like this:
<List data={data} renderItem={<Post/>} />

Ahh ok, just found out you can use components as plain js functions:
<List data={data} renderItem={Post} />

You can, but speaking from experience this leads to pain and bad design later on once these components grow and requirements change. The List component becomes more esoteric, and it's hard to find where things should be.
For instance, if you wanted to do something as simple as render a sponsored Post, the code becomes needlessly complex:
// Index.js
<List data={data} renderItem={Post} renderSponsoredItem={SponsoredPost} />
// List.js
function List({ data, renderItem, renderSponsoredItem }) {
<div className={styles.list}>
data.map(dataItem => {
if (dataItem.sponsored) {
return renderSponsoredItem(dataItem);
} else {
return renderItem(dataItem);
}
});
</div>
}
...and in a way, this ends up looking not like React at all, and doesn't harness the ease-of-use and readability thats possible with React.
An alternative could look something like:
// Index.js
<List>
{data.map(dataItem => {
if (dataItem.sponsored) {
return <SponsoredPost post={dataItem} />;
} else {
return <Post post={dataItem} />
}
})
</List>
// List.js
function List({ children }) {
<div className={styles.list}>
{children}
</div>
}
I've found the first form to lend itself to unreadable code and unworkable, unreadable components in the long term, and the latter to be much easier to read/work with.

Related

Should functions within React functional components be wrapped?

I have a functional component that has one function within it, renderMessages.
const MessageContainer = (props) => {
const renderMessages = () => {
return props.messages.map((message, index) => {
return(
<Message
key={index}
username={message.username}
message={message.message}
fromCurrentUser={message.fromCurrentUser}
/>);
})
}
return(
<div className='messages'>
{renderMessages()}
</div>
)
}
However, I realized that instead of wrapping renderMessages function on the map, I can just have:
const renderMessages = props.messages.map((message, index) => {
return(
<Message
key={index}
username={message.username}
message={message.message}
fromCurrentUser={message.fromCurrentUser}
/>);
})
}
And as a result, my final return would just contain
return(
<div className='messages'>
{renderMessages}
</div>
)
In a class-based component and within a render function, I'd use the last of the two. Which of the two is considered the best practice when using functional components, and why?
EDIT:
Which of the two is considered the best practice when using functional components, and why?
Best practices change with context - e.g. the team you're working on - so this is an opinion-based question out of the gate.
That being said, in my opinion, I wouldn't do either. I'd do (and I do) this:
const MessageContainer = (props) => {
return (
<div className='messages'>
{props.messages.map((message, index) => (
<Message
key={index}
username={message.username}
message={message.message}
fromCurrentUser={message.fromCurrentUser}
/>
))}
</div>
)
}
What's the purpose of the extra variable anyway?
While you're at it, don't use indexes for keys
The dirty secret about all those extra methods you stuck on your class components that encapsulated rendering logic is that they were an anti-pattern - those methods were, in fact, components.
EDIT #2
As pointed out in the other answer, the most performant solution for this specific use case is specifying the map function outside the functional component:
const renderMessage = (message,index) => (
<Message
key={index}
{...message}
/>
)
const MessageContainer = (props) => {
return (
<div classname='messages'>
{props.messages.map(renderMessage)}
</div>
);
}
But, you shouldn't prematurely optimize and I would advocate for the original solution I posted purely for simplicity/readability (but, to each their own).
Good job separating the mapping outside the component's return, because this way you'll be only calling the same function over and over again untill the .map is done iterating, but if you wrote it in the component's return, every time the .map iterate over the next item you'll be creating a new function.
Regarding the question, I'd recommend the second way, clean/readable code is always preferable.
P.S. try to use the unique message id instead of the index.

React key prop within wrapper

While looking through our code base, I found code that looks a bit like this:
const Carousel = ({ items }) => {
return (
<CarouselOuter>
{items.map((item) => (
<CarouselItemWrapper>
<CarouselItem key={item.key}>
...
</CarouselItem>
</CarouselItemWrapper>
)}
</CarouselOuter>
);
}
Notice that the key prop is on CarouselItem, not CarouselItemWrapper, the component that's directly returned from items.map. This seems to work fine, and there are no warnings in the console, but it runs counter to every example I've seen using map in React.
I want know if there's a good argument (specifically in regards to performance) for rearranging the code with the key as shown below, or if this is just a stylistic choice:
const Carousel = ({ items }) => {
return (
<CarouselOuter>
{items.map((item) => (
<CarouselItemWrapper key={item.key}>
<CarouselItem>
...
</CarouselItem>
</CarouselItemWrapper>
)}
</CarouselOuter>
);
}
Side note: CarouselOuter, CarouselItem, and CarouselItemWrapper are all styled-components, but I doubt that's relevant.

Mapping from passed props in a functional component

I am building a website that is reliant on a json file for all of its information.
In my app.js the json info is showing properly when I console.log() it, but when I try and pass it to my functional components it is giving me undefined.
in app.js
<Route
exact
path="/puppies"
render={props => (
<Puppies {...props} propdata={this.state.propdata} />
)}
/>
This seems to be working fine, however when I try and map it inside the component it tells me that its undefined.
function Puppies(propdata) {
return <div>{propdata.puppies.map(puppies =>
<h1>{puppies.name}</h1>
)}</div>;
}
I have done this before but with a class component. So most likely I am making a mistake with the functional component.
The full code is viewable here:
https://github.com/Imstupidpleasehelp/Puppywebsite/tree/master/src
Thank you for your time.
You'll probably need to check that the data is null of undefined. You are passing a big object with data, I recommend to pass more specific props instead of a big object.
I like to prevent my data to be undefined in 2 ways:
lodash.get
Optional Chaining
Usage:
import _ from 'lodash';
function Puppies({ propdata }) {
const puppies = _.get(propdata, 'puppies', []);
return (
<div>
{puppies.map(puppies => <h1>{puppies.name}</h1>)}
</div>
);
}
or
function Puppies({ propdata }) {
const puppies = propdata?.puppies || [];
return (
<div>
{puppies.map(puppies => <h1>{puppies.name}</h1>)}
</div>
);
}
What you have as propdata is actually just an object containing all properties that you have passed in. You should use destructuring to get the actual propdata value.
Solution:
function Puppies({propdata}) {
return (
<div>
{propdata.puppies.map(puppies =>
<h1>{puppies.name}</h1>
)}
</div>
);
}
Since this is asynchronous request to get the data, your data is not readily available hence you need to handle that scenario.
function Puppies(propdata) {
return (
{
propdata.puppies.length>0 ? <div>
propdata.puppies.map((puppies)=>{
<h1>{puppies.name}</h1>
})
</div> :null
}
)

How to access ref that was set in render

Hi I have some sort of the following code:
class First extends Component {
constructor(props){super(props)}
myfunction = () => { this.card //do stuff}
render() {
return(
<Component ref={ref => (this.card = ref)} />
)}
}
Why is it not possible for me to access the card in myfunction. Its telling me that it is undefined. I tried it with setting a this.card = React.createRef(); in the constructor but that didn't work either.
You are almost there, it is very likely that your child Component is not using a forwardRef, hence the error (from the React docs). ref (in a similar manner to key) is not directly accesible by default:
const MyComponent = React.forwardRef((props, ref) => (
<button ref={ref}>
{props.children}
</button>
));
// ☝️ now you can do <MyComponent ref={this.card} />
ref is, in the end, a DOMNode and should be treated as such, it can only reference an HTML node that will be rendered. You will see it as innerRef in some older libraries, which also works without the need for forwardRef in case it confuses you:
const MyComponent = ({ innerRef, children }) => (
<button ref={innerRef}>
{children}
</button>
));
// ☝️ now you can do <MyComponent innerRef={this.card} />
Lastly, if it's a component created by you, you will need to make sure you are passing the ref through forwardRef (or the innerRef) equivalent. If you are using a third-party component, you can test if it uses either ref or innerRef. If it doesn't, wrapping it around a div, although not ideal, may suffice (but it will not always work):
render() {
return (
<div ref={this.card}>
<MyComponent />
</div>
);
}
Now, a bit of explanation on refs and the lifecycle methods, which may help you understand the context better.
Render does not guarantee that refs have been set:
This is kind of a chicken-and-egg problem: you want the component to do something with the ref that points to a node, but React hasn't created the node itself. So what can we do?
There are two options:
1) If you need to pass the ref to render something else, check first if it's valid:
render() {
return (
<>
<MyComponent ref={this.card} />
{ this.card.current && <OtherComponent target={this.card.current} />
</>
);
}
2) If you are looking to do some sort of side-effect, componentDidMount will guarantee that the ref is set:
componentDidMount() {
if (this.card.current) {
console.log(this.card.current.classList);
}
}
Hope this makes it more clear!
Try this <Component ref={this.card} />

How can I return multiple lines JSX in another return statement in React?

A single line works fine:
render: function () {
return (
{[1,2,3].map(function (n) {
return <p>{n}</p>
}}
);
}
But not for multiple lines:
render: function () {
return (
{[1,2,3].map(function (n) {
return (
<h3>Item {n}</h3>
<p>Description {n}</p>
)
}}
);
}
Try to think of the tags as function calls (see the documentation). Then the first one becomes:
{[1,2,3].map(function (n) {
return React.DOM.p(...);
})}
And the second one:
{[1,2,3].map(function (n) {
return (
React.DOM.h3(...)
React.DOM.p(...)
)
})}
It should now be clear that the second snippet doesn't really make sense (you can't return more than one value in JavaScript). You have to either wrap it in another element (most likely what you'd want, that way you can also provide a valid key property), or you can use something like this:
{[1,2,3].map(function (n) {
return ([
React.DOM.h3(...),
React.DOM.p(...)
]);
})}
With JSX syntactic sugar:
{[1,2,3].map(function (n) {
return ([
<h3></h3>, // note the comma
<p></p>
]);
})}
You don't need to flatten the resulting array. React will do that for you. See the following fiddle http://jsfiddle.net/mEB2V/1/. Again: Wrapping the two elements into a div/section will most likely be better long term.
It seems Jan Olaf Krems's answer about returning an array no longer applies (maybe since React ~0.9, as #dogmatic69 wrote in a comment).
The documentation says you need to return a single node:
Maximum Number of JSX Root Nodes
Currently, in a component's render,
you can only return one node; if you have, say, a list of divs to
return, you must wrap your components within a div, span or any other
component.
Don't forget that JSX compiles into regular JS; returning two
functions doesn't really make syntactic sense. Likewise, don't put
more than one child in a ternary.
In many cases you can simply wrap things in a <div> or a <span>.
In my case, I wanted to return multiple <tr>s. I wrapped them in a <tbody> – a table is allowed to have multiple bodies.
As of React 16.0, returning an array is apparently allowed again, as long as each element has a key: New render return types: fragments and strings
React 16.2 lets you surround a list of elements with <Fragment>…</Fragment> or even <>…</>, if you prefer that to an array: https://reactjs.org/docs/fragments.html
From React v16.0.0 onwards, it is possible to return multiple elements by wrapping them within an Array:
render() {
return (
{[1,2,3].map(function (n) {
return [
<h3>Item {n}</h3>.
<p>Description {n}</p>
]
}}
);
}
Also from React v16.2.0, a new feature called React Fragments is introduced which you can use to wrap multiple elements:
render() {
return (
{[1,2,3].map(function (n, index) {
return (
<React.Fragment key={index}>
<h3>Item {n}</h3>
<p>Description {n}</p>
</React.Fragment>
)
}}
);
}
As per the documentation:
A common pattern in React is for a component to return multiple
elements. Fragments let you group a list of children without adding
extra nodes to the DOM.
Fragments declared with the explicit <React.Fragment> syntax may have
keys. A use case for this is mapping a collection to an array of
fragments — for example, to create a description list:
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// Without the `key`, React will fire a key warning
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
key is the only attribute that can be passed to Fragment. In the
future, we may add support for additional attributes, such as event
handlers.
Also, you might want to return several list items in some helper function inside a React component. Just return an array of HTML nodes with the key attribute:
import React, { Component } from 'react'
class YourComponent extends Component {
// ...
render() {
return (
<ul>
{this.renderListItems()}
</ul>
)
}
renderListItems() {
return [
<li key={1}>Link1</li>,
<li key={2}>Link2</li>,
<li key={3} className="active">Active item</li>,
]
}
}
Updated
Use React Fragment. It's simple. Link to fragment documentation.
render() {
return (
<>
{[1,2,3].map((value) => <div>{value}</div>)}
</>
);
}
Old answer - obsolete
With React > 16 you can use react-composite.
import { Composite } from 'react-composite';
// ...
{[1,2,3].map((n) => (
<Composite>
<h2>Title {n}</h2>
<p>Description {n}</p>
</Composite>
))};
Of course, react-composite has to be installed.
npm install react-composite --save
It is simple by React fragment <></> and React.Fragment:
return (
<>
{[1, 2, 3].map(
(n, index): ReactElement => (
<React.Fragment key={index}>
<h3>Item {n}</h3>
<p>Description {n}</p>
</React.Fragment>
),
)}
</>
);
You can use createFragment here. See Keyed Fragments.
import createFragment from 'react-addons-create-fragment';
...
{[1,2,3].map((n) => createFragment({
h: <h3>...</h3>,
p: <p>...</p>
})
)}
(I am using ES6 and JSX syntax here.)
You first have to add the react-addons-create-fragment package:
npm install --save react-addons-create-fragment
The advantage over Jan Olaf Krems's solution: React does not complain about the missing key.
This happens when you are not on the current project folder or the folder you are currently on contains more than one project, you probably will get this error.
I had a similar error and once switched to a current project folder and run, the issue is gone.

Categories

Resources