Child component prop causes "Maximum update depth exceeded" - reactjs

I have the following two components.
Brands.js is a parent component that contains the SearchField component and passes a few props to it. I'll explain each prop:
brands is a list of Brand entities each containing an Id and a Name,
searchableItems is a list of Brand entities converted to an array of searchable items
setFilteredItems is a useState returned function that sets the filteredItems variable used by a table component
// I'm cutting away most of the code done in this component,
// but if necessary will expand this section.
...
<SearchField
wrapperClassName="ms-auto me-2 h-100"
inputClassName={"form-control-sm"}
items={brands}
searchableItems={searchableItems}
setFilteredItems={setFilteredItems}
itemIdResolver={(item) => item.id}
placeholder="Search for brand..."
/>
...
SearchField.js is a child component that renders a search input above the table in Brands.js component.
import React, {useEffect, useState} from 'react';
import {
Input
} from 'reactstrap';
export default function SearchField({ inputClassName, wrapperClassName, items, itemIdResolver, searchableItems, setFilteredItems, style, placeholder }) {
if (!inputClassName) inputClassName = '';
if (!wrapperClassName) wrapperClassName = '';
if (!itemIdResolver) itemIdResolver = (item) => item.id;
const [filterText, setFilterText] = useState("");
useEffect(() => {
let filteredSearchableItems = searchableItems?.filter(
searchableItem =>
searchableItem.searchString.indexOf(filterText.toLowerCase()) !== -1
);
setFilteredItems(
items?.filter(
item => filteredSearchableItems?.some(fsi => fsi.id === itemIdResolver(item))
));
}, [filterText, items, searchableItems, setFilteredItems, itemIdResolver]);
return (
<div className={"search-field-wrapper " + wrapperClassName}>
<i className="ri-search-line"></i>
<Input
type="text"
className={inputClassName}
placeholder={placeholder}
value={filterText}
onChange={e => setFilterText(e.target.value)}
style={style}
/>
</div>
)
}
The problem:
The SearchField components itemIdResolver causes the Maximum update depth exceeded error since it constantly re-renders the whole page/component.
I don't understand why fitting the itemIdResolver inside the dependency array of SearchField useEffect causes this.
Any help is appreciated.
P.S.
I tried removing the resolver and that got me rid of the maximum update error, so it's definitely due to that.

Related

How to setState with select element in react - WITHOUT LAG

There seems to be a lag of one render cycle when I change a select element and when it's state actually changes. I know that there are several similar questions on this but none of them see to work for me. useEffect is still showing the old state from one render cycle before.
Anyone know how to address this?
Parent component code:
import React, {useCallback, useEffect, useState} from 'react'
import Dropdown from '../components/Dropdown.js'
const ValueCalculation = () => {
const industryData = require('../../../backend/standard_data/industry_benchmarks.json')
var industryList = []
industryData.map((record)=>{
var x = record.Industry;
industryList.push(x)
})
const [industry, setIndustry] = useState(industryList[8]);
const updateIndustry = (updatedValue)=>{
console.log(updatedValue); //<--This is updated with the right value!
setIndustry(updatedValue) //<--This is NOT updating the current value
console.log(industry); //<-- This does NOT show the updated value
}
useEffect(()=>{
console.log(industry); //<--Still showing value from previous render cycle
},[])
return (
<div>
<Dropdown
label="Industry"
value={industry}
onChange={(e)=>updateIndustry(e.target.value)}
list={industryList}
/>
</div>
)
}
export default ValueCalculation
Code for Child Dropdown component..
import React from 'react'
const Dropdown = (props) => {
return (
<div className="form-group mb-3">
<label>{props.label}</label>
<select
className="form-control"
value={props.value}
onChange={props.onChange}
>
{
props.list.map(item=>(
<option key={props.list.indexOf(item)} value={item}>{item}</option>
))
}
</select>
</div>
)
}
export default Dropdown
SetState is async so your console.log is going to run before the state has been set. The code you have works correctly as you can see in the sandbox link provided.
const updateIndustry = (updatedValue) => {
//This is updated with the right value!
console.log(updatedValue);
//This is updating correctly and will show on re render
setIndustry(updatedValue);
//This will not work since setState is async
//Console.log() is firing before the state has been set
console.log(industry);
};
As for the useEffect. You will need to add industry as a dependency so that the console.log is called as soon as the state changes :
useEffect(() => {
console.log(industry);
}, [industry]);
Sandbox : https://codesandbox.io/s/hidden-voice-8jvt2f?file=/src/App.js
So, it's a little bit complicated but your state is changing on every rerender cycle, so ur state it's updated after the updateIndustry it's finished (popped out from js callstack). I tested your code, and it is working perfectly and i refactored it a little bit
import React, { useEffect, useState } from "react";
import Dropdown from "./Dropdown.js";
const App = () => {
var industryList = ["a", "b", "c", "d"];
const [industry, setIndustry] = useState(industryList[0]);
useEffect(() => {
console.log(industry);
}, [industry]);
return (
<div>
<Dropdown
label="Industry"
value={industry}
onChange={(e) => setIndustry(e.target.value)}
list={industryList}
/>
</div>
);
};
export default App;
Also, useEffect hook is reexecuted when its dependency changes value, in your case your dependency array is empty so I added [industry] to it.

React Hooks: State Is Not Updating in Function

I have spent several hours trying to fix this issue with no luck. I have a reusable component that starts with a state of any empty object, the object is given a series of properties based off of props, and then based on user input on each set of radio buttons will show a child component.
Unfortunately, the function that is supposed to update the state that will then trigger whether the user sees the child component is not working.
I believe the issue is in my editDisplay function. Currently when I click one of the radio buttons the new object that should become the new state is logged to the console correctly, but when I use React Devtools to inspect the page, I see that state is not updating. That being said, I could see a case where I am misunderstanding useEffect, and perhaps useEffect is running each time editDisplay is running. Any help would be appreciated.
import React, {useState, useEffect} from 'react'
import {Form} from 'react-bootstrap'
import RatingInput from './RatingInput'
export default function MembershipForm({inputs, name, record, setRecord}) {
const [display, setDisplay] = useState({})
useEffect(()=>{
inputs.forEach(input => {
var newDisplay = display
newDisplay[name+input]=false
setDisplay(newDisplay)})
},[])
const editDisplay = (input, visible) => {
var newDisplay =display
newDisplay[name+input]=visible
console.log(newDisplay)
setDisplay(newDisplay)
}
const questions=inputs.map(input => {
return (
<div key={name+input}>
<Form.Group>
<Form.Label className='mx-2'>Do you have a {name} {input} Rating?</Form.Label>
<Form.Check inline label='Yes' value={true} name={name+input} type='radio' className = 'mx-2' onClick ={() =>{editDisplay(input, true)}} />
<Form.Check inline label='No' value ={false} name={name+input} type='radio' className = 'mx-2' defaultChecked onClick = {() =>{editDisplay(input, false)}} />
</Form.Group>
{display[name+input] && <RatingInput input={input}/>}
</div >
)
})
return (
<div className='ml-3'>
{questions}
</div>
)
}
const editDisplay = (input, visible) => {
var newDisplay = display
newDisplay[name+input]=visible
console.log(newDisplay)
setDisplay(newDisplay)
}
You should perform react state update in an immutable way.
What you are doing above is you are mutating existing state variable (that assignment on first line doesn't add much: newDisplay refers to same object as display). In such case react might not detect change. Do this instead:
var newDisplay = { ... display };

Is it possible to partially apply a React component?

Say I have a <Button> component which takes two properties: text and id e.g.,
<Button text="delete" id="123"/>
Now say I have a list of user ids: [101, 102, 103, …]
Would it be possible to partially apply <Button>? e.g.,
ids.map(<Button text="delete" id={__}>)
Where __ is just a placeholder waiting to be replaced with the current id.
If it was possible, would partially applying a React component have any adverse effect on the React Reconciliation Algorithm?
You could use two ways
one, which is not really a partial
ids.map((id)=><Button text="delete" id={id} />)
and the partial one which is really extracting the function above and using it
const PartialDeleteButton = (id) => <Button text="delete" id={id} />
ids.map(PartialDeleteButton)
which you could also use as
<PartialDeleteButton id={5} />
i cannot see how these would affect the reconciliation algorithm
There is no partial render of a component in React.
A component watches on state and props. Whenever you change either one, it will refresh the component. So if you change id dynamically, it will re-render the component.
However that would be 1 extra re-render.
You can however choose to write functions to prevent that like
React.memo: For latest react
shouldComponentUpdate: For older version.
Following is a demo for React.memo:
What to look in fiddle, notice I have added a setTimeout that updates data and it will call the render of ToDoApp but since components are memoised, it will not be called
function Button({id, value}) {
const onClick = () => {
console.log(`${id} - ${value}`)
}
console.log(`Rendering Btn ${value}`)
return (<button id={id || '__'} onClick={onClick}>{ value }</button>);
}
const MyButton = React.memo(
Button,
(prevProps, nextProps) => {
return prevProps.value === nextProps.value
}
)
Note: Since you will stop rendering of a component, you will not get updated props in it.
You could use useCallback to get a similar effect to partial application:
const HelloGreeter = useCallback(({name}: {name: string}) =>
(<Greeter name={name} greet="hello" />), []);
So, in context:
interface GreeterProps {
greet: string
name: string
}
const Greeter = ({greet, name}: GreeterProps) => (
<div>{greet}, {name}</div>
);
const MyComponent = () => {
const [name1, setName1] = useState<string>("world")
const HelloGreeter = useCallback(({name}: {name: string}) =>
(<Greeter name={name} greet="hello" />), []);
const setNameCallback = useCallback((e: ChangeEvent<HTMLInputElement>) =>
setName1(e.target.value), []);
return(
<>
<HelloGreeter name={name1} >
<input value={name1} onChange={setNameCallback} />
</>
);
}
This would not confuse the React renderer, because useCallback defines the function once only.

Component rendered 8 times

I don't understand clearly why Row component rendered 8 times. Should I use custom comparing function in React Memo for this component? I'm using the react-window package. Please explain how it works for me. Thanks a lot.
Parent component of ListView: CovidMap Component
My entire project: Github
This is my code:
import React, { useRef, memo, useEffect } from 'react';
import { FixedSizeList as FixedList, areEqual } from 'react-window';
import './ListView.scss';
const ListView = (props) => {
const listRef = useRef();
const Row = memo((props) => {
console.log('Row rendering...');
const { data, index, style } = props;
const className =
data.itemIndex === index
? 'PatienIndicator Highlight'
: 'PatientIndicator';
return (
<button
key={index}
className={className}
onClick={() => data.onClickPatient(data.patients[index])}
style={style}
>
{data.patients[index].name}
</button>
);
}, areEqual);
const patientsLength = props.patients
? Object.keys(props.patients).length
: 0;
const data = Object.assign(
{},
{ patients: props.patients },
{ onClickPatient: props.onClickPatient },
{ itemIndex: props.itemIndex }
);
console.log('List View rendering...');
useEffect(() => {
if (props.itemIndex) {
listRef.current.scrollToItem(props.itemIndex, 'smarter');
}
});
return (
<FixedList
className="List"
height={300}
itemCount={patientsLength}
itemSize={50}
width={'100%'}
ref={listRef}
itemData={data}
>
{Row}
</FixedList>
);
};
export default memo(ListView);
The itemCount prop tells FixedSizeList how many rows to render, so your list will render however many items are in your props.patients list, which is ultimately coming from the result of filtering the array returned by the fetch in the parent component. The index prop that you're using in the Row component is passed to the Row from FixedSizeList and refers to its position in the list, so it tells the first instance of the Row component that its index is 0, and Row uses that information to render the name of the first item in the data.patients prop. FixedSizeList passes its own itemData prop to each Row component as Row's data prop.
From the react-window documentation for the FixedSizeList component:
itemCount: number
Total number of items in the list. Note that only a few items will be rendered and displayed at a time.
and
children: component
React component responsible for rendering the individual item specified by an index prop
You're passing the Row component to FixedSizeList as a child, so it renders the Row component itemCount number of times.
React memo won't affect the number of instances of the component. All memo does is potentially improves performance of rendering each instance by reusing its last rendered version if the props haven't changed.

How to generate unique IDs for form labels in React?

I have form elements with labels and I want to have unique IDs to link labels to elements with htmlFor attribute. Something like this:
React.createClass({
render() {
const id = ???;
return (
<label htmlFor={id}>My label</label>
<input id={id} type="text"/>
);
}
});
I used to generate IDs based on this._rootNodeID but it’s unavailable since React 0.13. What is the best and/or simplest way to do it now?
The id should be placed inside of componentWillMount (update for 2018) constructor, not render. Putting it in render will re-generate new ids unnecessarily.
If you're using underscore or lodash, there is a uniqueId function, so your resulting code should be something like:
constructor(props) {
super(props);
this.id = _.uniqueId("prefix-");
}
render() {
const id = this.id;
return (
<div>
<input id={id} type="checkbox" />
<label htmlFor={id}>label</label>
</div>
);
}
2019 Hooks update:
import React, { useState } from 'react';
import _uniqueId from 'lodash/uniqueId';
const MyComponent = (props) => {
// id will be set once when the component initially renders, but never again
// (unless you assigned and called the second argument of the tuple)
const [id] = useState(_uniqueId('prefix-'));
return (
<div>
<input id={id} type="checkbox" />
<label htmlFor={id}>label</label>
</div>
);
}
This solutions works fine for me.
utils/newid.js:
let lastId = 0;
export default function(prefix='id') {
lastId++;
return `${prefix}${lastId}`;
}
And I can use it like this:
import newId from '../utils/newid';
React.createClass({
componentWillMount() {
this.id = newId();
},
render() {
return (
<label htmlFor={this.id}>My label</label>
<input id={this.id} type="text"/>
);
}
});
But it won’t work in isomorphic apps.
Added 17.08.2015. Instead of custom newId function you can use uniqueId from lodash.
Updated 28.01.2016. It’s better to generate ID in componentWillMount.
Update React 18
React 18 has introduced a new hook which generates a unique ID:
const id = useId();
Hook API docs: https://reactjs.org/docs/hooks-reference.html#useid
From your example, you could call the hook inside a component:
import React, { useId } from 'react'
function TextField = (props) => {
// generate unique ID
const id = useId();
return (
<>
<label htmlFor={id}>My label</label>
<input id={id} type="text"/>
</>
);
}
Following up as of 2019-04-04, this seems to be able to be accomplished with the React Hooks' useState:
import React, { useState } from 'react'
import uniqueId from 'lodash/utility/uniqueId'
const Field = props => {
const [ id ] = useState(uniqueId('myprefix-'))
return (
<div>
<label htmlFor={id}>{props.label}</label>
<input id={id} type="text"/>
</div>
)
}
export default Field
As I understand it, you ignore the second array item in the array destructuring that would allow you to update id, and now you've got a value that won't be updated again for the life of the component.
The value of id will be myprefix-<n> where <n> is an incremental integer value returned from uniqueId. If that's not unique enough for you, consider making your own like
const uniqueId = (prefix = 'id-') =>
prefix + Math.random().toString(16).slice(-4)
There are also hundreds or thousands of other unique ID things out there, but lodash's uniqueId with a prefix should be enough to get the job done.
Update 2019-07-10
Thanks to #Huong Hk for pointing me to hooks lazy initial state, the sum of which is that you can pass a function to useState that will only be run on the initial mount.
// before
const [ id ] = useState(uniqueId('myprefix-'))
// after
const [ id ] = useState(() => uniqueId('myprefix-'))
You could use a library such as node-uuid for this to make sure you get unique ids.
Install using:
npm install node-uuid --save
Then in your react component add the following:
import {default as UUID} from "node-uuid";
import {default as React} from "react";
export default class MyComponent extends React.Component {
componentWillMount() {
this.id = UUID.v4();
},
render() {
return (
<div>
<label htmlFor={this.id}>My label</label>
<input id={this.id} type="text"/>
</div>
);
}
}
Extending #forivall's comment
If the whole goal is to link up a <label> and <input> elements and they don't depend on props, then instead of using auto generated unique id's, the most optimal and performant approach would be to use useRef.
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
Meaning, you can use useRef to mimic instance variables which is not recomputed on props changes. useRef is not only used to reference a DOM element.
Example using an external random ID generator (e.g. loadash)
import React, { useRef } from 'react'
import uniqueId from 'lodash/utility/uniqueId'
function InputField = (props) => {
const {current: fieldId} = useRef(uniqueId('prefix-'))
return (
<div>
<input id={fieldId} type="checkbox" />
<label htmlFor={fieldId}>label</label>
</div>
);
}
Example using a simple custom random ID generator
import React, { useRef } from 'react'
function InputField = (props) => {
const {current: fieldId} = useRef("prefix-" + (Math.random().toString(36)+'00000000000000000').slice(2, 7))
return (
<div>
<input id={fieldId} type="checkbox" />
<label htmlFor={fieldId}>label</label>
</div>
);
}
Explanation:
The above random ID (Math.random().toString(36)+'00000000000000000').slice(2, 7) comes from this stackoverflow answer and will always guarantee 5 characters, compared to Math.random().toString(16).slice(-4) which may return empty strings.
Also, it's important to use a prefix where the prefix must start with a letter ([A-Za-z]) in order for it to be a valid HTML4 id attribute value.
A version without Lodash using hooks:
function useUniqueId() {
const [id] = useState(() => `component-${Math.random().toString(16).slice(2)}`)
return id
}
Hopefully this is helpful to anyone coming looking for a universal/isomorphic solution, since the checksum issue is what led me here in the first place.
As said above, I've created a simple utility to sequentially create a new id. Since the IDs keep incrementing on the server, and start over from 0 in the client, I decided to reset the increment each the SSR starts.
// utility to generate ids
let current = 0
export default function generateId (prefix) {
return `${prefix || 'id'}-${current++}`
}
export function resetIdCounter () { current = 0 }
And then in the root component's constructor or componentWillMount, call the reset. This essentially resets the JS scope for the server in each server render. In the client it doesn't (and shouldn't) have any effect.
I create a uniqueId generator module (Typescript):
const uniqueId = ((): ((prefix: string) => string) => {
let counter = 0;
return (prefix: string): string => `${prefix}${++counter}`;
})();
export default uniqueId;
And use top module to generate unique ids:
import React, { FC, ReactElement } from 'react'
import uniqueId from '../../modules/uniqueId';
const Component: FC = (): ReactElement => {
const [inputId] = useState(uniqueId('input-'));
return (
<label htmlFor={inputId}>
<span>text</span>
<input id={inputId} type="text" />
</label>
);
};
The useId hook will replace the unstable useOpaqueIdentifier in an upcoming stable version of React (already available in the latest React alphas). It will generate stable ids during server rendering and hydration to avoid mismatches.
For the usual usages of label and input, it's just easier to wrap input into a label like this:
import React from 'react'
const Field = props => (
<label>
<span>{props.label}</span>
<input type="text"/>
</label>
)
It's also makes it possible in checkboxes/radiobuttons to apply padding to root element and still getting feedback of click on input.
I found an easy solution like this:
class ToggleSwitch extends Component {
static id;
constructor(props) {
super(props);
if (typeof ToggleSwitch.id === 'undefined') {
ToggleSwitch.id = 0;
} else {
ToggleSwitch.id += 1;
}
this.id = ToggleSwitch.id;
}
render() {
return (
<input id={`prefix-${this.id}`} />
);
}
}
Don't use IDs at all if you don't need to, instead wrap the input in a label like this:
<label>
My Label
<input type="text"/>
</label>
Then you won't need to worry about unique IDs.

Resources