Good practice when organizing React functional component code (functions, splitting...) - javascript

Is there a better solution when it comes to splitting code into sub components, and to writing functions called in render ? For example would it be better to have
const CustomRow = ({ row = {}, ...props }) => {
const cells = [];
for (const value of Object.values(row)) {
cells.push(value);
}
return (
<TableRow>
{/* Some content */}
{cells.map((cell, index) => (
<TableCell key={index++}>{cell}</TableCell>
))}
{/* More content */}
</TableRow>
);
};
Or
const CustomRow = ({ row = {}, ...props }) => {
const getCells = () => {
const cells = [];
let index = 0;
for (const value of Object.values(row)) {
cells.push(<TableCell key={index++}>{cell}</TableCell>);
}
return cells;
}
return (
<TableRow>
{/* Some content */}
{getCells()}
{/* More content */}
</TableRow>
);
};
And more generally, how good or bad is it to split such components ? Assuming I have a bigger CustomTable component, which I've broken down in CustomRow, CustomHeaders, ...
I'm not really sure to know the reasonable limit to start exploding big components into small chunks; I've started splitting methods and Component as soon as they tend to become long enough to cause confusion (which can be quick), but I don't know if there are strong opinions that go against this ?
Splitting code seems to give more readability and a clear overview, but I don't have many experienced colleagues regarding React, so I'm unsure about what others do. I know there is a lot of freedom regarding code organization but I want to know more about others habits

Related

React performance issue with drag and drop

I'm struggling with react performance issue while adding an element to the box. For this, I use React Rnd library.
I have a map that renders items when there is a new item inside the array:
children.map((children, index) => (
<Box
key={children.id}
isPreview={false}
index={index}
slot={name}
{...children}
/>
)),
Box component is Rnd component from the library, and it is actually big one.
<Rnd
style={{
//2 lines off css
}}
minHeight={MIN_SIZE}
minWidth={MIN_SIZE}
enableResizing={isResizingEnabled}
disableDragging={condition}
size={size}
position={Position}
lockAspectRatio={isAspectRatioLocked}
onResizeStart={onResizeStart}
onDragStop={(e, newPosition) => {
onDragStop(newPosition)
}}
onResizeStop={(e, dir, ref, delta, newPosition) =>
onResizeStop(ref, newPosition)
}
resizeHandleComponent={createResizeHandles(isInCollision)}
dragGrid={grid}
resizeGrid={grid}
bounds="parent"
>
<StyledDiv
onClick={() => {
dispatch(actions.setEditMode({...properties}))
}}
isBeingCropped={isCroppingEnabled}
isPreview={isPreview}
isEditable={isEditable}
isInCollision={isInCollision}
isEditStartable={isEditStartable}
>
{children}
</StyledDiv>
</Rnd>
And the problem is when I add 4 elements to this box, it took sometimes 2-4 seconds...
Any idea how it could be solved?
Is there any simple solution to make it faster, or do I have to investigate each function/hook and optimize it with some useCallback, useMemo, or something?
Yes, you should start from avoiding unnecessary re-renders, then you can look into optimizing each function. As this is a drag and drop component, i believe most props will change often whiles you drag an element, as a result make sure that each re-render is important before actually re rendering the component.
Here are a few places to start from.
Pass only what is needed to the box
Instead of spreading the children to the Box, pass only what is need to it, this will avoid any unnecessary re renders in case there are props in there that the Box do not care about.
children.map((children, index) => (
<Box
key={children.id}
isPreview={false}
index={index}
slot={name}
/>
)),
Use Memoization to avoid re renders
For times when the objects haven't moved, you may want to avoid any re renders, as a result memoize the Components.
Use useCallback for functions
This will prevent the callback from being assigned everytime the parent is re rendered. The biggest culprit i see is the onClick handler which depends on the actions, as a result will render the box again and again when the action changes, even though the box doesn't depend on all the actions
const { setEditMode } = dispatch;
const handleDragStop = useCallback(
(e, newPosition) => {
onDragStop(newPosition);
},
[onDragStop]
);
const handleResizeStop = useCallback(
(e, dir, ref, delta, newPosition) => {
onResizeStop(ref, newPosition);
},
[onResizeStop]
);
const handleOnClick = useCallback(() => {
dispatch(setEditMode({ ...properties }));
}, [onResizeStop, setEditMode, properties]);
<Rnd
style={
{
//2 lines off css
}
}
minHeight={MIN_SIZE}
minWidth={MIN_SIZE}
enableResizing={isResizingEnabled}
disableDragging={condition}
size={size}
position={Position}
lockAspectRatio={isAspectRatioLocked}
onResizeStart={onResizeStart}
onDragStop={handleDragStop}
onResizeStop={handleResizeStop}
resizeHandleComponent={createResizeHandles(isInCollision)}
dragGrid={grid}
resizeGrid={grid}
bounds="parent"
>
<StyledDiv
onClick={handleClick}
isBeingCropped={isCroppingEnabled}
isPreview={isPreview}
isEditable={isEditable}
isInCollision={isInCollision}
isEditStartable={isEditStartable}
>
{children}
</StyledDiv>
</Rnd>;
Checkout this article I wrote here if you need any further explanation

In React components, is it "bad practice" to refer to child components outside of the render/return?

I'm scaffolding a component rebuild. It previously had 2 breakpoints but will now have 4. The 2 current breakpoints are piled into a single component with a grand-master ternary showing one or the other of full JSX... yeah, 2 sets of mostly similar JSX output, it's a big component using only 1 half or the other.
I'm going to break out the 2 existing breakpoints into sub components, with the 3rd and 4th sizes as well, and I'm trying to work out a system that will choose which of the 4 components to show.
Here's a copy paste of what I have concepted on stackblitz.com
export default function App() {
const chooser = 2;
const enumb = {
[0]: <Slap />,
[1]: <Bammy />,
[2]: <Weeds />,
};
const between = (x, min, max) => {
return x >= min && x <= max;
};
const show = !between(chooser, 0, 2) ? 0 : chooser;
return (
<div>
<h1>Hello StackBlitz!</h1>
{enumb[show]}
{show}
</div>
);
}
const Bammy = () => {
return <div>component Bammy</div>;
};
const Weeds = () => {
return <div>component Weeds</div>;
};
const Slap = () => {
return <div>component Slap</div>;
};
Note: Change the value of chooser to display the chosen component. show and between() are a sanity check limiter to fallback to a valid value.
So my main questions is about the {enumb[show]} down in the return statement:
Is it "bad practice" to make reference to (or include) child components outside of the return/render of the parent component? (While this doesn't look too bad right here, the props tree is pretty big enough to make it "complex" reading.)
Is there a performance/state/rerender issue with this method of rendering child components??
The 2ndary question is: what would be the best way to do the enumb reference/decision down in the return? (btw I'd like to avoid switch/case cuz it just seems verbose.)
There's no penalty for doing so. Best practices are determined by your co-workers.
Here's an example showing there's no performance hit
If I was to evaluate your code in code review (I know it's probably purely for demonstration purposes) then I would tell you to move the static stuff outside your render method, like this
const enumb = {
[0]: Slap,
[1]: Bammy,
[2]: Weeds
};
const between = (x, min, max) => {
return x >= min && x <= max;
};
export default function App() {
const chooser = 2; // let's pretend chooser is a prop passed into App
const show = !between(chooser, 0, 2) ? 0 : chooser;
const Cmp = enumb[show];
return (
<div>
<h1>Hello StackBlitz!</h1>
<Cmp />
{show}
</div>
);
}

problem mixing material-ui elements and code in react javascript

I constantly have issues trying to use code and material-ui elements in react jsx code. Here's a code snippet:
const icols = 0;
const makeTableRow = (
x,
i,
formColumns,
handleRemove,
handleSelect) =>
<TableRow key={`tr-${i}`}>
{formColumns.map((y, k) => (
y.displayColumn ? (<TableCell key={`trc-${k}`}>{x[y.name]}</TableCell>) : null), <-comma added for next line
y.displayColumn ? (cols+=1) : null)
)}
<TableCell>
<IconButton onClick={() => handleSelect(i)} ><EditIcon fontSize='small'/></IconButton>
<IconButton onClick={() => handleRemove(i)} ><DeleteForeverIcon fontSize='small' /></IconButton>
</TableCell>
</TableRow>
I am getting a jsx parsing error, when I add this line above:
y.displayColumn ? (cols+=1) : null)
If I remove the comma at the EOL above it, I still get an error. Basically I can't get a map to exec more than one statement.
If I take out the line and the EOL comma above it, everything works but I don't get a displayed column count, which I require.
I've tried using simple if/else which I am more comfortable with, but I have NEVER been able to get if/else to work in a jsx function. I want to only create a tablecell for a column w/displayColumn flag set to true, and I want a total count of the displayed columns, so I can use it later on (cols).
Is there a way to accomplish this with an if/else statement? Then I can have more than 1 statement in the if clause. The ternary operator only allows 1 statement, and I can't find anywhere what maps limitations are.
Thanks in advance for your help!
You can do something like this. You can open the open the arrow function body in map and put return JSX and do the cols increment there. Instead of having two ternary operator checks for the same condition, we can have just one conditional statement.
<TableRow key={`tr-${i}`}>
{
formColumns.map((y, k) => {
if (y.displayColumn) {
cols += 1;
return <TableCell key={`trc-${k}`}>{x[y.name]}</TableCell>
}
return null
})
}
<TableCell>
<IconButton onClick={() => handleSelect(i)} ><EditIcon fontSize='small'/></IconButton>
<IconButton onClick={() => handleRemove(i)} ><DeleteForeverIcon fontSize='small' /></IconButton>
</TableCell>
</TableRow>
Basically I can't get a map to exec more than one statement.
You can't execute more than one expression inside a arrow function definition, instead use regular declarated functions
{formColumns.map((y, k) => {
y.displayColumn ? (cols+=1) : null;
// Return what you want to render
return y.displayColumn ? (<TableCell key={`trc-${k}`}>{x[y.name]}</TableCell>) : null
}}
There are only two types of arrow function
arrow_function = () => "i will be returned"
// This way you declare only one expression after the arrow and it is returned
and
arrow_function = () => {
// This is a regular logic function
text = "i will be" + " returned";
return text;
}
EDIT 1: Add conditionals between JSX
There are two ways i know to do it
const App = () => {
return (
<div>
<h2>First form</h2>
<FirstForm true={true} />
<hr />
<h2>Second form</h2>
<SecondForm true={false} />
</div>
)
}
const FirstForm = props => {
// This way is just a ternary conditional
return (
<div>
{props.true
? <span className="success">True condition matched</span>
: <span className="danger">False condition matched</span>
}
</div>
)
}
const SecondForm = props => {
// This way uses a anonymous function executed in runtime
return (
<div>
{(() => {
let message = "Hello";
message += " World, from an auto executed anonymous function";
return (
<span className={props.true?"success":"danger"}>{message}</span>
)
})()}
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
.success {
color: darkgreen;
}
.danger {
color: #5e181b;
}
<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>
<div id="react"></div>
Ok psiro, so I looked at your examples, which all work in your situation. However, as soon as I apply one to my scenario, I still get a syntax error. Like here:
<TableBody>
{(() => {
console.log('data = ' + JSON.Stringify(data, null, 2) + '.'));
return ((data.length > 0) ? (
data.map((x, i) => row(
x,
i,
formColumns,
handleRemove,
handleSelect,
editIdx
))) : (<TableRow><TableCell colSpan={`${cols}`}>No Data</TableCell></TableRow>) )
})()}
</TableBody>
And this is the problem I have w/anonymous functions. They create unmaintainable code. I am not doing anything different than your example. Just TWO js statements inside the code you presented. Your code works, mine returns a syntax error.
And why do I even NEED a double-anonymous function to have 2 simple js statements inside an if statement? Why does your code work, and mine not?
Addendum:
Ok, so I made some inferences based on your code and realized I had added yet ANOTHER anonymous function (the map statement) into the code. I reworked it into this, which compiled:
{(() => {
console.log('data = ' + JSON.Stringify(data, null, 2) + '.');
if (data.length > 0) {
return (data.map((x, i) => row(x, i, formColumns, handleRemove, handleSelect, editIdx)))
}
return(<TableRow><TableCell colSpan={`${cols}`}>No Data</TableCell></TableRow>)
})()}
The fact that it looks completely unmaintainable is irrelevant I guess. But it doesn't matter because it STILL doesn't work! Now I get a 'JSON.stringify is not a function' at runtime, which is ridiculous of course. Why can't I get a simple console.log to work in reactjs?
ADDENDUM:
Ok, I fixed the issue thanks to all the help. For anyone else that has an issue w/multiple statements inside an anonymous function, if you want to do it, you need to add a return statement so the function knows what result to return.
<TableBody>
{(() => {
console.log('data = ' + data + '.');
if (data.length > 0) {
return (data.map((x, i) => row(x, i, formColumns, handleRemove, handleSelect, editIdx)))
}
return(<TableRow><TableCell colSpan={`${cols}`}>No Data</TableCell></TableRow>)
})()}
</TableBody>
That includes when you have an anonymous function inside another anonymous function. Hope this helps anyone else having this problem.

In my React application, how can I add a newline after each item in this map function?

I have this redux selector I've been working on in my React application. I've made good progress but I've hit a wall with this last issue i'm working on and I feel like it shouldn't be that difficult to solve but I'm struggling.
What I'm trying to achieve is after each item has been mapped, the next item must go to a new line.
export const vLineRejectionSelector = createSelector(
selectedVIdSelector,
linesSelector,
(id, lines) =>
lines
.filter(line => line.id === id)
.map(
(rejectString, index) =>
`Line: ${index + 1} ${rejectString.rejectReason}`
)
);
The only relevant code to look at in this is the map function. I want each item to go to a new line as its being mapped.
The output should look something like:
Line 1: Reject Reason One
Line 2: Reject Reason Two
Instead the output looks like:
Line1: Reject ReasonOneLine2: Reject Reason Two
This is being rendered in JSX as well
The value of this is passed around as a prop and gets rendered in the JSX like:
<Typography variant="body2">
{rejectReason}
{reasons && reasons.join(', ')}
</Typography>
Its value is {rejectReason}.
I would advise against composing JSX in your selector and rather just return the lines as an array as you are doing currently, but then map it to either a list or a simple <br /> joined list in the render() function. This keeps your selector more easily testable and also doesn't mix state selection concerns with presentational concerns.
E.g:
in your container
const mapStateToProps = (state) => {
return {
rejectReason: vLineRejectionSelector(state)
}
}
const SomeComponentContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SomeComponent)
export default SomeComponentContainer
and then in your SomeComponents render function:
<Typography variant="body2">
{this.props.rejectReason.map((rejectReason) => <>Line: {index + 1} {rejectReason}<br /></>)}
</Typography>
Try to use <br /> tag at the end of each line.
Like Line: ${index + 1} ${rejectString.rejectReason}<br />

Outputting n instances of a React component

I want to create n instances of a React component.
What is a good terse way to do this give that JSX can only contain expressions?
I currently am trying the following:
<Wrapper>
{repeat(n)(i => <MyComponent i={i} />}
</Wrapper>
function repeat(n) {
return cb => Array(n).fill(null).forEach((_, i) => cb(i));
}
You can use as much JavaScript als you like :)
render() {
const lst = [1, 2, 3, 4];
return (
<div>
{lst.map(itm => <span key={itm}>{itm}</span>)}
</div>
);
}
If you do not have a key ready, you can use the second argument of the map callback which is the index in the array. More info on MDN.
In your specific case where you do not have an array but just a number:
render() {
var times = [];
for (let i = 0; i < 10; i++) {
times.push(<MyComponent key={i} i={i} />);
}
return <Wrapper>{times}</Wrapper>;
}
Also check this answer on how to use for loops. It's not quite as nice but it also works. I believe the React team has planned to make working with arrays in JSX more straight forward.
If you have just a number, and do not want to use a for loop, you could also "fake" it, for example by using string.repeat. Not sure if this is very readable though :)
render() {
return (
<div>
{'a'.repeat(10).split('').map((_, i) => <MyComponent i={i} />}
</div>
);
}

Categories