Rerender only specific Child Components in JSX map function - javascript

I am mapping through an array, which returns JSX Components for each of the items in the array. During runtime I want to pass down values. If they match the value of the individual items, their individual component gets modified.
I am trying to find a way to achieve this without rerendering all components, which currently happens because the props change
I have tried using shouldComponentUpdate in a class component, but it seems this way I can only compare prevState and prevProps with the corresponding changes. I have further considered useMemo in the Map function, which didnt work, because it was nested inside the map function.
const toParent=[1,2,4,5]
Parent Component:
function parent({ toParent }) {
const [myNumbers] = useState([1,2,3,4, ..., 1000]);
return (
<div>
{myNumbers.map((number, index) => (
<Child toChild = { toParent } number = { number }
index= { index } key = { number }/>
))}
</div>
)
}
Child Component:
function Child({toChild, number, index}){
const [result, setResult] = useState(() => { return number*index }
useEffect(()=> {
if (toChild.includes(number)) {
let offset = 10
setResult((prev)=> { return { prev+offset }})
}
}, [toChild])
return (
<div style={{width: result}}> Generic Div </div> )
}

The solution to my problem was using the React.memo HOC and comparing the properties to one another and exporting it as React.memo(Child, propsAreEqual).
Performance
This way other methods like findElementbyId (not recommended in any case) and shouldComponentUpdate to target specific items in a map function can be avoided.
Performance is quite good, too. Using this method cut down the rendering time from 40ms every 250ms to about 2 ms.
Implementation
In Child Component:
function Child(){...}
function propsAreEqual(prev, next) {
//returning false will update component, note here that nextKey.number never changes.
//It is only constantly passed by props
return !next.toChild.includes(next.number)
}
export default React.memo(Child, propsAreEqual);
or alternatively, if other statements should be checked as well:
function Child(){...}
function propsAreEqual(prev, next) {
if (next.toChild.includes(next.number)) { return false }
else if ( next.anotherProperty === next.someStaticProperty ) { return false }
else { return true }
}
export default React.memo(Key, propsAreEqual);

Related

React - useCallback throwing error on renderProps function

I'm passing renderProps function in the props. i want to wrap it with useCallback, so the optimised child component will no re-render on the function creation.
when wrapping the function with useCallback i get this error:
Invalid hook call. Hooks can only be called inside of the body of a
function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
none of the above applies to my situation.
renderCell = React.useCallback((
{
events,
popperPlacement,
popperStyle,
time
}
) => {
const { localeToggle } = this.state;
const { weekStarter, isTimeShown } = this.props;
const eventsListPopperStyle = utils.isWeekFirst(time, weekStarter) ||
utils.isWeekSecond(time, weekStarter) ? { left: '-17% !important' } : { left: '17% !important' };
return (
<MonthlyCell
events={events}
isTimeShown={isTimeShown}
popperPlacement={popperPlacement}
popperStyle={popperStyle}
time={time}
eventsListPopperStyle={eventsListPopperStyle}
/>
)
}, [])
Because hooks doesn't work inside class components, the error was thrown.
I managed to find a work around by providing the second parameter for the React.memo. in the function i provided, i compare the prevProps and nextProps, and when the prop is a function i disregard it and return true.
it might not work for everyone because sometime the function do change, but for situations when it's not, it's ok.
const equalizers = {
object: (prevProp, nextProp) => JSON.stringify(prevProp) === JSON.stringify(nextProp),
function: () => true, // disregarding function type props
string: (prevProp, nextProp) => prevProp === nextProp,
boolean: (prevProp, nextProp) => prevProp === nextProp,
number: (prevProp, nextProp) => prevProp === nextProp,
}
export const areEqualProps = (prevProps, nextProps) => {
for (const prop in prevProps) {
const prevValue = prevProps[prop];
const nextValue = nextProps[prop];
if (!equalizers[typeof prevValue](prevValue, nextValue)) { return false; }
}
return true
}
export default React.memo(MyComponent, areEqualProps)

How to pass ref from parent element to children

I am facing a problem which causes a lot of headaches and I just can't solve it.
I have a component which is used through the whole application (about 100 times in total) and the main goal of it to render some list based on children (children is a function which returns the child component) and payload (payload is an array of objects, which holds information about future children).
<GenericList
children={this.renderElement}
payload={this.state.treeData}
// other props...
/>
Here is the renderElement
renderElement = (role) => {
return (
<BaseListItem>
<div className="fb jCenter">
<Text
title={role.Name}
>
{role.Name}
</Text>
</div>
</BaseListItem>
);
};
renderElement can both return RFC or RCC.
Inside GenericList I should somehow manage it to pass ref to RFC or RCC.
The Part of code, which implements that logic is this
cloneChildrenElement({ payload, children, selected, payloadSortBy }) {
if (payload && payload.length) {
const { selectBy, scrollToIndex, idToScrollIndex } = this.props;
const data = sortPayload(payload, payloadSortBy);
return data.map((item, index) => {
return cloneElement(children(item), {
item,
isActive,
key: index,
onDoubleClick: this.handleDblClick,
ref: (node) => {
console.log(node, 'node');
if (Number.isInteger(scrollToIndex) || isActive) {
this.genericContentSelected = node;
}
},
});
});
}
return false;
}
I have also tried to implement ref forwarding, but in that case, I should manually do it for all components that use that logic.
I want to know whether it's possible to achieve this by only making changes inside GenericList component.

Passing data up through nested Components in React

Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering

Use dynamically created react components and fill with state values

Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));

.map() working outside of render function, but fails when used inside render

I'm not a very experienced developer, so I may be missing something simple. I have an array of objects that I am getting from my redux state (this.props.assignments.assignments). I am getting a .map() is not a function when trying to call displayAssignments() in my render return. However, I have another function that is attached to an onClick that is doing the same map and logging "name" into the console, and that one works as expected (when i comment out displayAssignments()). I have no idea why the .map() would work on my onClick but not on displayAssignments.
I included the showAssignments function simply to test if my data could even be mapped...which confused me even more because that one works...
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getAssignments } from '../../Actions/assignmentActions.js';
class Assignments extends Component {
componentDidMount() {
this.props.getAssignments();
};
showAssignments = () => {
console.log(this.props.assignments.assignments);
this.props.assignments.assignments.map(assignment => (
console.log(assignment.name)
));
};
displayAssignments = () => {
this.props.assignments.assignments.map(assignment => {
return (
<div>
<p>{assignment.name}</p>
<p>{assignment.description}</p>
</div>
)
})
};
render() {
return (
<div>
<h1>Assignments</h1>
<button onClick={this.showAssignments}>Click Me</button>
{this.displayAssignments()}
</div>
);
};
};
Assignments.propTypes = {
getAssignments: PropTypes.func.isRequired,
assignments: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
assignments: state.assignments
});
export default connect(mapStateToProps, { getAssignments })(Assignments);
Expected: to render name and description of assignments by way of mapping
Actual: getting this.props.assignments.assignments.map is not a function
You should return your map() as a value of displayAssigments:
displayAssignments = () => {
return this.props.assignments.assignments.map(assignment => {
return (
<div>
<p>{assignment.name}</p>
<p>{assignment.description}</p>
</div>
)
})
};
Or just remove the curly bracket which wrap .map():
displayAssignments = () => this.props.assignments.assignments.map(assignment =>
<div>
<p>{assignment.name}</p>
<p>{assignment.description}</p>
</div>
)
For your error is not function, make sure that this.props.assignments.assignments is an Array.
Your problem is that, given the fact that you call getAssignments() in the componentDidMount method, it means there is a section of time between when the component is mounted and the assignment field is populated in the store in which there are no assignments, hence why you get the .map() is not a function error.
The click handler is only called after the asynchronous operation finishes so assignments is defined by then.
To fix this, you can change your default state for the assignments field to be:
{
"assignments":[]
}
Another problem with your code, as #radonirina-maminiaina has mentioned, is that your displayAssignments function does not return the result of the maps so the HTML will be blank
The Array.map function creates a new array from the given array with the function provided applied to each element.
Another thing to consider is why you need assignments.assignments to get to the list of assignments. You could probably change your actions or reducer to remove the nesting.

Categories

Resources