How to solve Cannot update during an existing state issue - react? - javascript

I have a function I calling it in render() method
and it's setState a Flag from the state.
So I got this error
Cannot update during an existing state transition (such as within
render).
I read about this error, and what I understand it's because I setState in render method and this is the wrong way.
So I'm forced to do it if u have any idea to handle this tell me.
The main idea about this function
I have an array of an object "Name as Tools" so in every object I have "id, name, count, price"
so that will render a three input in UI like this
and I have a boolean flag in-state "isEmpty" that checks every input in array before sending this data to the database.
Code
State = {
toolsUsed: [
{
id: 0,
name: '',
price: '',
count: '',
},
],
// Checker
isEmpty: false,
}
renderToolsUsed = () => {
const {toolsUsed} = this.state;
const tools = toolsUsed.map((item, i) => {
const {count, price, name, id} = item;
this.setState({
isEmpty: ['name', 'price', 'count'].every(key => item[key].length > 0),
});
return (
<View key={i} style={styles.tools}>
.... Inputs here ...
</View>
);
});
return tools;
};
JSX
render() {
return (
<View>
{this.renderToolsUsed()}
</View>
);
}

You can't update the state like this. It is like infinite loop. setState will trigger render, then render will trigger another setState, then you keep repeat the circle.
I don't know why you need isEmpty when you already have toolsUsed which you can use it to check if all input are empty.
Lets say if you insist to have isEmpty, then you can set it inside input change event.
The code is not tesed. I wrote the code directly from browser. But you can get the idea before the code.
renderToolsUsed = () => {
const { toolsUsed } = this.state;
const tools = toolsUsed.map((item, i) => {
return (
<View key={i} style={styles.tools}>
<TextInput value={item.name} onChangeText={(text) => {
this.setState({
toolsUsed: [
...toolsUsed.slice(0, i - 1),
{...item, name: text },
...toolsUSed.slice(i)
]
}, this.updateEmptyState)
}>
// other input here
</View>
);
});
// ...
};
updateEmptyState = () => {
this.setState({
isEmpty: this.state.toolsUsed.every(x => x.name === '' && x.price === '' && x.count === '')
})
}

The state is not designed to store all the data you have in the app
For what you need isEmpty inside the state?
To do this, use a global variable
Or check it out when you want it out of the render

Related

React Native, State Changes, but JSX Conditional Rendering Does not Updated UI

Hello and thank you for your time in advance!
I am struggling with a small issue that I have not encountered before, with React not rendering an UI element based on a check function. Basically, what I am trying to make, is a multiple selection filter menu, where when an option is clicked, the dot next to it changes to red.
For this purpose I append the value of each option to an array, and using array.sort, verify when it is to be added (value is pushed to FilterList) and removed (value is popped from FilterList)
The checkfilter function operates normally, and when logging the state, indeed it works as intended with the array being updated.
However, the JSX code running the checkfilter to render the extra red dot inside the bigger circle, unfortunately does not.
Additionally, when the screen is refreshed, the UI is updated normally, with every option clicked, now showing the aforementioned red dot.
Why is this happening? I have tried several hooks, JSX approaches, using imported components and more that I can't even remember, yet the UI will not update oddly.
Below you can find a snippet of the code. Please bear in mind this is a render function for a flatlist component
const checkFilter = useCallback((element) => {
return filterList?.some((el: any) => (el == element))
}, [filterList])
const removeFilter = useCallback((cat) => {
let temparr = filterList
var index = temparr?.indexOf(cat);
if (index > -1) {
temparr?.splice(index, 1);
}
setFilterList(temparr)
}, [filterList])
const addFilter = useCallback((cat) => {
let temparr = filterList;
temparr.push(cat);
setFilterList(temparr)
}, [filterList])
const renderFilter = useCallback((item) => {
return (
<Pressable
onPress={() => {
checkFilter(item?.item) ? removeFilter(item?.item) : addFilter(item?.item);
console.log(filterList)
}}
style={styles.modalOptionWrapper}
>
<Text style={styles.modalOptionTitle(checkFilter)}>{item?.item}</Text>
<View style={styles.modalOptionRowRight}>
<View style={styles.radioBtn}>
{checkFilter(item?.item) ?
<View style={styles.radioBtnBullet} />
:
null
}
</View>
</View>
</Pressable>
)
}, [filterList])
This may not be correct answer but try this. When I say simple basic codes, like this.
const ListItems = () => {
const [filterList, setFilterList] = React.useState([]); // must be array
const checkFilter = filterList?.some((el) => el === element);
const removeFilter = useCallback(
(cat) => {
// not updating new state, just useing previous state
setFilterList((prev) => [...prev].filter((el) => el !== cat));
// used spread iterator
},
[] // no need dependency
);
const addFilter = useCallback((cat) => {
// used spread indicator
setFilterList((prev) => [...prev, cat]);
}, []);
const renderFilter = useCallback((item) => {
// just checking our codes is right with basic elements
// it may be 'undefined'
return <Text>{JSON.stringify(item)}</Text>;
}, []);
return filterList.map(renderFilter);
};

Optimizing performance for arbitrarily large number of inputs in react

Specifically with Material UI, I'm trying to render a large number of inputs (> 100) and manage a global state effectively. I run into serious performance issues like input lag at around 50 inputs but the lag is still noticeable at around 10 inputs. I've narrowed it down to the Material UI <TextField /> component because if I change that to regular input element, there is virtually no lag which is to be expected since there's a lot more happening in a MUI component.
What I don't know however, is if changing how I manage state might help performance. I've tried 2 state management approaches: traditional react with useState hook and Recoil.
In addition to the 2 approaches above, I also tried a combination of recoil and traditional react by using useState and useSetRecoilState in filter components so that all filters wouldn't re-render on change to recoil state since they are only writing not reading but still no luck.
EDIT
Turns out I forgot to test putting values in more than 1 input... When you enter a value in another input, the previously entered input will get wiped. I'm thinking it's because in my handleChange function, I replace an item in filterList but filterList is now an old outdated reference so it does not have the previously set value. Back at square one now, because if I read state in the filter component, then the component will re-render which eliminates the memo performance boost.
EDIT 2
Posted solution below
Looks like I was trying to use recoil when I really didn't need to. Using React.memo along with React.useState and moving the global state edit up to the parent ended up being all I needed.
Filters.js
const filterMaker = (length) => {
let filters = [];
for (let i = 0; i < length; i++) {
if (i % 2 === 0) {
filters.push({ name: `filter-${i}`, type: 'a', active: { value1: '' } });
} else {
filters.push({
name: `filter-${i}`,
type: 'b',
active: { value1: '', value2: '' },
});
}
}
return filters;
};
function replaceItemAtIndex(arr, index, newValue) {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}
export default function Filters() {
const [filterList, setFilterList] = useRecoilState(filterListState);
useEffect(() => {
setFilterList(filterMaker(200));
}, []);
const handleChange = (filter, name, value, index) => {
setFilterList((prevState) => {
return replaceItemAtIndex(prevState, index, {
...filter,
active: {
...filter.active,
[name]: value,
},
});
});
};
return (
<div>
{filterList.map((filter) =>
filter.type === 'a' ? (
<FilterA
key={filter.name}
filter={filter}
filterList={filterList}
handleChange={handleChange}
/>
) : (
<FilterB
key={filter.name}
filter={filter}
filterList={filterList}
handleChange={handleChange}
/>
)
)}
</div>
);
}
FilterA.js
export const FilterA = memo(
({ filter, filterList, handleChange }) => {
const [values, setValues] = useState(filter.active || {});
const index = filterList.findIndex((item) => item.name === filter.name);
const handleLocalChange = ({ target: { name, value } }) => {
setValues((prevState) => {
return { ...prevState, [name]: value };
});
handleChange(filter, name, value, index);
};
return (
<Box mb={1}>
<Typography>{filter.name}</Typography>
<TextField
variant="standard"
name="value1"
label="value1"
value={values.value1 || ''}
onChange={handleLocalChange}
/>
</Box>
);
},
(prevProps, nextProps) => {
if (prevProps.filter.active.value1 === nextProps.filter.active.value1) {
return true;
}
return false;
}
);

react-widgets DropDownList dynamic load on demand

I would like to use the awesome react-widgets DropDownList to load records on demand from the server.
My data load all seems to be working. But when the data prop changes, the DropDownList component is not displaying items, I get a message
The filter returned no results
Even though I see the data is populated in my component in the useEffect hook logging the data.length below.
I think this may be due to the "filter" prop doing some kind of client side filtering, but enabling this is how I get an input control to enter the search term and it does fire "onSearch"
Also, if I use my own component for display with props valueComponent or listComponent it bombs I believe when the list is initially empty.
What am I doing wrong? Can I use react-widgets DropDownList to load data on demand in this manner?
//const ItemComponent = ({item}) => <span>{item.id}: {item.name}</span>;
const DropDownUi = ({data, searching, fetchData}) => {
const onSearch = (search) => {
fetchData(search);
}
// I can see the data coming back here!
useEffect(() => {
console.log(data.length);
}, [data]);
<DropDownList
data={data}
filter
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
Got it! This issue is with the filter prop that you are passing to the component. The filter cannot take a true as value otherwise that would lead to abrupt behavior like the one you are experiencing.
This usage shall fix your problem:
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix πŸ˜…
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
Working demo
The entire code that I had tried at codesandbox:
Warning: You might have to handle the clearing of the values when the input is empty.
I thought that the logic for this was irrelevant to the problem statement. If you want, I can update that as well.
Also, I added a fakeAPI when searchTerm changes that resolves a mocked data in 2 seconds(fake timeout to see loading state).
import * as React from "react";
import "./styles.css";
import { DropdownList } from "react-widgets";
import "react-widgets/dist/css/react-widgets.css";
// Coutesy: https://usehooks.com/useDebounce
import useDebounce from "./useDebounce";
interface IData {
id: string;
name: string;
}
const fakeAPI = () =>
new Promise<IData[]>((resolve) => {
window.setTimeout(() => {
resolve([
{
name: "NA",
id: "user210757"
},
{
name: "Yash",
id: "id-1"
}
]);
}, 2000);
});
export default function App() {
const [state, ss] = React.useState<{
searching: boolean;
data: IData[];
searchTerm: string;
}>({
data: [],
searching: false,
searchTerm: ""
});
const debounceSearchTerm = useDebounce(state.searchTerm, 1200);
const setState = (obj: Record<string, any>) =>
ss((prevState) => ({ ...prevState, ...obj }));
const getData = () => {
console.log("getting data...");
setState({ searching: true });
fakeAPI().then((response) => {
console.log("response: ", response);
setState({ searching: false, data: response });
});
};
React.useEffect(() => {
if (debounceSearchTerm) {
getData();
}
}, [debounceSearchTerm]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix πŸ˜…
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
</div>
);
}
Let me know if you have more queries on this πŸ˜‡
So it i think that list should be loaded a then you can filtering your loaded data.In your example on the beginning you don't have value so list is empty, you tape in some text and then value of list re render but it look like is not filtered.....
However I look through code base, and it's look like is not ready until you don't set manually open prop drop down list component. In getDerivedStateFromprops, next data list is read only if in next props is open set. to true
From DropDwonList
static getDerivedStateFromProps(nextProps, prevState) {
let {
open,
value,
data,
messages,
searchTerm,
filter,
minLength,
caseSensitive,
} = nextProps
const { focusedItem } = prevState
const accessors = getAccessors(nextProps)
const valueChanged = value !== prevState.lastValue
let initialIdx = valueChanged && accessors.indexOf(data, value)
//-->> --- -- --- -- -- -- -- - - - - - - - - - --- - - --------
//-->>
if (open)
data = Filter.filter(data, {
filter,
searchTerm,
minLength,
caseSensitive,
textField: accessors.text,
})
const list = reduceToListState(data, prevState.list, { nextProps })
const selectedItem = data[initialIdx]
const nextFocusedItem = ~data.indexOf(focusedItem) ? focusedItem : data[0]
return {
data,
list,
accessors,
lastValue: value,
messages: getMessages(messages),
selectedItem: valueChanged
? list.nextEnabled(selectedItem)
: prevState.selectedItem,
focusedItem:
(valueChanged || focusedItem === undefined)
? list.nextEnabled(selectedItem !== undefined ? selectedItem : nextFocusedItem)
: nextFocusedItem,
}
}
I would try:
<DropDownList
data={data}
filter
open
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
if it will be works, then you just have to
manage your open state by yourself.

Antd Modal how to update the value in it

I'm using Antd Modal to display order information by passing props to TableDetail Component. The thing is when I try to remove the items in the order I use setState and the modal won't update its data.
https://codesandbox.io/s/antd-reproduction-template-forked-vu3ez
A simple version of reproduction is provided above.
enter image description here
After I click on the remove button the items remain unchanged but when I close and open the modal it will update.
enter image description here
What should I do to make it change? I tried forceUpdate() but it won't work.
Here is the TableDetail component.
export default class TableDetail extends Component {
constructor(props) {
super(props);
this.state = {
tableOrder: props.tableOrder,
column: []
};
}
tableInitializer = () => {
const column = [
{
title: "name",
render: (item) => {
return <span>{item.name}</span>;
}
},
{
title: "price",
render: (item) => {
return item.price;
}
},
{
title: "amount",
render: (item) => {
return <span>{item.amount}</span>;
}
},
{
title: "removeButton",
render: (item) => {
return (
<Button
type="link"
style={{ color: "red" }}
onClick={() => this.props.removeItem(item)}
>
Remove
</Button>
);
}
}
];
this.setState({
column
});
};
UNSAFE_componentWillMount() {
this.tableInitializer();
}
render() {
return (
<Modal
visible={true}
title={
<span style={{ fontSize: "1.5rem", fontWeight: "bold" }}>
Table: {this.state.tableName}
</span>
}
cancelText="Cancel"
okText="OK"
onCancel={this.props.hideModal}
>
<Table
columns={this.state.column}
dataSource={this.state.tableOrder}
rowKey="name"
/>
</Modal>
);
}
}
TableDetail.propTypes = {
tableOrder: PropTypes.array.isRequired,
tableName: PropTypes.string.isRequired,
hideModal: PropTypes.func.isRequired,
removeItem: PropTypes.func.isRequired
};
You can use the filter method in your removeItem function
removeItem = (item) => {
const filtered = this.state.tableOrder.filter(
(order) => item.name !== order.name
);
console.log(item, filtered);
this.setState({
tableOrder: filtered
});
};
and one more thing you should avoid assigning your props to state like
this.state = {
tableOrder: props.tableOrder,
};
instead of assigning to state directly use that as props like
<Table
dataSource={this.props.tableOrder}
/>
In most cases, this is an antipattern. Don’t β€œcopy props into state.”
It creates a second source of truth for your data, which usually leads
to bugs. One source of truth is best.
Improved code Live demo
The issue in your code is in removeItem. You are trying to mutate tableOrder. According to react docs:
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
So that is why it is kind of asynchronous and you can't see your updated changes. You need to do this:
removeItem = (item) => {
const { tableOrder } = this.state;
const newTable = [...tableOrder]; <- Create new variable and update variable
for (let i = 0; i < newTable.length; i++) {
if (item.name === newTable[i].name) {
console.log("Got it! " + newTable[i].name);
newTable.splice(i, 1);
console.log("new table: ", tableOrder);
break;
}
}
this.setState({
tableOrder: newTable <- update here
});
};
You dont need to actually modify array you can simply do this also:
this.setState(
{
tableOrder: this.state.tableOrder.filter(data => !(data.name===item.name))
}
)
//filter creates new array
Here is demo: https://codesandbox.io/s/antd-reproduction-template-forked-j4bfl?file=/tables.js

setState long execution when managing large amount of records

I am trying to solve a problem that happens in react app. In one of the views (components) i have a management tools that operate on big data. Basically when view loads i have componentDidMount that triggers ajax fetch that downloads array populated by around 50.000 records. Each array row is an object that has 8-10 key-value pairs.
import React, { Component } from "react";
import { List } from "react-virtualized";
import Select from "react-select";
class Market extends Component {
state = {
sports: [], // ~ 100 items
settlements: [], // ~ 50k items
selected: {
sport: null,
settlement: null
}
};
componentDidMount() {
this.getSports();
this.getSettlements();
}
getSports = async () => {
let response = await Ajax.get(API.sports);
if (response === undefined) {
return false;
}
this.setState({ sports: response.data });
};
getSettlements = async () => {
let response = await Ajax.get(API.settlements);
if (response === undefined) {
return false;
}
this.setState({ settlements: response.data });
};
save = (key, option) => {
let selected = { ...this.state.selected };
selected[key] = option;
this.setState({ selected });
};
virtualizedMenu = props => {
const rows = props.children;
const rowRenderer = ({ key, index, isScrolling, isVisible, style }) => (
<div key={key} style={style}>
{rows[index]}
</div>
);
return (
<List
style={{ width: "100%" }}
width={300}
height={300}
rowHeight={30}
rowCount={rows.length || 1}
rowRenderer={rowRenderer}
/>
);
};
render() {
const MenuList = this.virtualizedMenu;
return (
<div>
<Select
value={this.state.selected.sport}
options={this.state.sports.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("sport", option)}
/>
<Select
components={{ MenuList }}
value={this.state.selected.settlement}
options={this.state.settlements.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("settlement", option)}
/>
</div>
);
}
}
The problem i am experiencing is that after that big data is downloaded and saved to view state, even if i want to update value using select that has ~100 records it takes few seconds to do so. For example imagine that smallData is array of 100 items just { id: n, name: 'xyz' } and selectedFromSmallData is just single item from data array, selected with html select.
making a selection before big data loads takes few ms, but after data is loaded and saved to state it suddenly takes 2-4 seconds.
What would possibly help to solve that problem (unfortunately i cannot paginate that data, its not anything i have access to).
.map() creates a new array on every render. To avoid that you have three options:
store state.sports and state.settlements already prepared for Select
every time you change state.sports or state.settlements also change state.sportsOptions or state.settlementsOptions
use componentDidUpdate to update state.*Options:
The third option might be easier to implement. But it will trigger an additional rerender:
componentDidUpdate(prevProps, prevState) {
if (prevState.sports !== this.state.sports) {
this.setState(oldState => ({sportsOptions: oldState.sports.map(...)}));
}
...
}
Your onChange handlers are recreated every render and may trigger unnecessary rerendering of Select. Create two separate methods to avoid that:
saveSports = option => this.save("sport", option)
...
render() {
...
<Select onChange={this.saveSports}/>
...
}
You have similar problem with components={{ MenuList }}. Move this to the state or to the constructor so {MenuList} object is created only once. You should end up with something like this:
<Select
components={this.MenuList}
value={this.state.selected.settlement}
options={this.state.settlementsOptions}
onChange={this.saveSettlements}
/>
If this doesn't help consider using the default select and use a PureComponent to render its options. Or try to use custom PureComponents to render parts of the Select.
Also check React-select is slow when you have more than 1000 items
The size of the array shouldn't be a problem, because only the reference is stored in the state object, and react doesn't do any deep equality on state.
Maybe your render or componentDidUpdate iterates over this big array and that causes the problem.
Try to profile your app if this doesn't help.

Categories

Resources