Let's say I have a React element <Parent> and I need to render an array of <FooBar>
My code currently looks something like
Parent = React.createClass({
getChildren() {
var children = this.props.messages; // this could be 100's
return children.map((child) => {
return <FooBar c={child} />;
});
},
render() {
return (
<div>
{this.getChildren()}
</div>
);
}
});
This is really slow when there are 100's of children because Parent waits for all the children to render. Is there a workaround to render the children incrementally so that Parent does not have to wait for all its children to render?
You can take a subset of messages at a time for render, and then queue up further updates with more children via a count in state.
Using requestIdleCallback or setTimeout to queue will allow you to escape React's state batching from intercepting the current browser paint, which would be a problem if you did setState directly from componentDidUpdate
Heres something to get you going
const Parent = React.createClass({
numMessagesPerRender = 10
constructor(props) {
super(props)
this.state = {
renderedCount: 0
}
}
componentWillReceiveProps(props) {
// must check that if the messages change, and reset count if you do
// you may also need to do a deep equality of values if you mutate the message elsewhere
if (props.messages !== this.props.messages) {
this.setState({renderedCount: 0})
}
}
getChildren() {
// take only the current
const children = this.props.messages.slice(0, this.state.renderedCount);
return children.map(child => <FooBar c={child} />);
},
render() {
return (
<div>
{this.getChildren()}
</div>
);
}
renderMoreMessagesPlease() {
// you MUST include an escape condition as we are calling from `componentDidXYZ`
// if you dont your component will get stuck in a render loop and crash
if (this.state.renderedCount < this.props.messages.length) {
// queue up state change until the frame has been painted
// otherwise setState can halt rendering to do batching of state changes into a single
// if your browser doesnt support requestIdleCallback, setTimeout should do same trick
this.idleCallbackId = requestIdleCallback(() => this.setState(prevState => ({
renderedCount: prevState.renderedCount + this.numMessagesPerRender
})))
}
}
componentDidMount() {
this.renderMoreMessagesPlease()
}
componentDidUpdate() {
this.renderMoreMessagesPlease()
}
componentDidUnmount() {
// clean up so cant call setState on an unmounted component
if (this.idleCallbackId) {
window.cancelIdleCallback(this.idleCallbackId)
}
}
});
Related
class NewComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
obj: [],
externalObj: [],
};
}
getFunc = (external) => {
...
arr = arr.filter(a => a.toLowerCase().includes('eg')
this.setState({ obj: arr });
return arr.map((Id) => {
return <Legend key={Id} id={Id} title={Id}/>;
});
}
render() {
return(
this.getFunc(false);
)
}
}
This is the structure of my React component. I want to set obj in state to that arr to use obj in some other method called completely differently
I am getting this error - Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
I kind of have an idea what is causing this error but I dont know what other thing I can do to achieve what I want - to be able to use this arr in some other method called seperately
You are setting state inside your render function, so when it try to render it, it will set new state and render again, it might cause an infinite loop.
I'm not sure what this line do this.setState({ obj: arr });, but it should be moved somewhere into componentDidMount or componentDidUpdate or wherever you need it.
You can make your filter in one of those lifecycle methods and then render your array from state
UPDATE
In your class add componentDidMount
componentDidMount(){
arr = arr.filter(a => a.toLowerCase().includes('eg')
this.setState({ obj: arr });
}
Then in your getFunc
return this.state.obj && this.state.obj.map((Id) => {
return <Legend key={Id} id={Id} title={Id}/>;
});
I have a react-autosuggest component which takes a loading state prop, as well as a data prop from an API request. The loading state changes when the data prop is hydrated.
In the react-autosuggest component, I am trying to use componentDidUpdate to automatically render autosuggest results based on the current input (if there is any at the time the props update). In other words, if the user has input a value before the loading prop changes (along with the data prop), I want the autosuggest results to render automatically when the props change. Currently, the results don't render until the next keystroke is made, based on the onChange method built in to react-autosuggest.
My parent component:
const Home = (props) => {
const { data, loading, error } = useQuery(GET_DATA);
if (error) return `Error! ${error.message}`;
return (
<HomeContainer>
<Logo />
<Search
data={data ? data : null}
home={true}
loading={loading ? true : false}
/>
</HomeContainer>
);
};
Some relevant parts of Search.js:
class Search extends React.Component {
constructor() {
super();
this.state = {
value: "",
suggestions: [],
};
this.inputRef = React.createRef();
}
// Only try to render suggestions if loading has finished.
getSuggestions = (value) => {
if (!this.props.loading) {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
const trie = createTrie(this.props.data, "name");
return inputLength === 0 ? [] : trie.getMatches(inputValue);
} else {
return [];
}
};
// I assume this is the method that is currently successfully triggering re-render
// on input change.
onChange = (event, { newValue }) => {
this.setState({
value: newValue,
});
};
// If the data prop has changed and data has hydrated, set the current input value
// to the this.value state. I need the re-render to happen after this completes,
// and the value has been set to state. The check against the previous props prevents
// an infinite updating loop.
componentDidUpdate(prevProps) {
if (
this.props.data !== prevProps.data &&
Object.keys(this.props.data).length
) {
this.setState({
value: this.inputRef.current.input.value,
});
}
};
render() {
const { value, suggestions } = this.state;
const inputProps = {
value,
onChange: this.onChange,
};
return (
<Autosuggest
ref={this.inputRef}
/>
);
}
}
So everything works fine, other than the fact that I need the component to re-render after the componentDidUpdate life cycle completes. It's setting the current input value to state when the loading finishes, but it still requires the keystroke from onChange to cause the component to re-render.
How would I go about doing this?
edit: Why isn't the setState in the componentDidUpdate triggering a re-render with the new value? That's all onChange is doing isn't it? Unless the event argument is doing something that I'm missing.
I have a complete running code, but it have a flaw. It is calling setState() from inside a render().
So, react throws the anti-pattern warning.
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount
My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
checkData: true,
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost !== nextProps.cost) {
this.setState({
checkData: true
});
}
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
if (this.props.cost.length && this.state.checkData) {
const tmp = this.props.cost;
//some calculations
....
....
this.setState({
theData: tmp,
checkData: false
});
}
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
<PieGraph theData={this.state.theData} />
</div>
);
}
}
The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.
handleFormSubmit = (e) => {
this.props.onGpChange(this.state.value);
e.preventdefaults();
}
The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?
Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.
checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.
Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost != nextProps.cost) {
this.setState({
theData: this.props.cost
});
}
}
shouldComponentUpdate(nextProps, nextState){
if(nextProps.cost !== this.props.cost){
return true;
}
return false;
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
{this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
</div>
);
}
}
PS:- The above solution is for version React v15.
You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.
There are other ways!
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render
method, both on the initial mount and on subsequent updates. It should
return an object to update the state, or null to update nothing.
So in your case
...component code
static getDerivedStateFromProps(props,state) {
if (this.props.cost == nextProps.cost) {
// null means no update to state
return null;
}
// return object to update the state
return { theData: this.props.cost };
}
... rest of code
You can also use memoization but in your case it's up to you to decide.
The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps
For example updating a list (searching) after a prop changed
You could go from this:
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
to this:
import memoize from "memoize-one";
class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
I have a react component that maps over an array of objects whose content is then displayed on screen. This works perfectly, however when I check the ul children in the componentDidMount lifecycle method it is an empty array, however, a second later it contains all the items.
Does anyone know how I can wait until everything has been rendered?
I have tried componentDidUpdate but as there is a setInterval method running regularly this is firing too often.
componentDidMount() {
// this is an empty array here
console.log(this.items.children);
setInterval(() => {
this.setState((prevState) => {
return {
left: prevState.left - 1
};
});
}, 20);
}
render() {
let items = this.props.itemss.map(item => {
return (
<Item left={this.state.left} content={item.content} key={item.date} />
);
});
return (
<div>
<ul ref={(el) => { this.items = el; }} >
{ items }
</ul>
</div>
);
}
The reason you're having this issue (besides the spelling errors) is because the ref callback will not assign a value to this.items until after that component has rendered. componentDidMount is called after the first time render is called and not again until the component unmounts and remounts.
Instead, use componentDidUpdate which is called after a fresh render, in that method check to see if this.items has a value. If so, proceed, if not, just return from the function until the next the render where the ref callback succeeds.
I have the following function:
update() {
this.currentItem = [];
//...Populate currentItem
this.setState({
currentItem
});
}
Which renders on the page like this;
render() {
const { currentItem } = this.state;
return {currentItem}
}
I then pass this function into a child component, like this:
<Component
update={this.update.bind(this)}
/>
and then call it like this in my child component:
let { update } = this.props;
if (typeof update === "function")
update();
The problem is that the update function does not re render the content I am updating on the parent page. As far as I understand this, whenever setState is called, render also gets called. render() does seem to be getting called, but it does not seem to display the updated value - why is this, and how can I resolve it?
I guess it could be to do with the fact that it is coming from a child component?
I have tried forceUpdate, but it does not re render the component either - what am I missing?
Try avoiding this.forceUpdate() because this will not fire shouldComponentUpdate() which is a performance hook for your Component in React. As, I saw that you are passing your state to child component and trying to update the parents state object from there, which against the law. You should create a method in parent and pass that method as a prop to the child component. It should look like this
constructor(props) {
super(props);
this.state = { loading: false };
this.update = this.update.bind(this);
}
update(newState) {
this.setState({loading: newState })
}
render() {
return <ChildComponent update={this.update} />
}
I am just guessing here but i think you set the initial value for the child component in the constructor and the value you want it to reflect points to its own state instead of the parents state
I needed to set the state to loading first, then when I set it to loading = false, it would re render that page
this.setState({
loading:true
});
//...Do the work
this.setState({
loading:false
});