Rerender input react component on parent props change - javascript

I have in an input field that should use some delay option on typing and making a search request. Also I need Re-run the this search component whenever the search text change in props. But I got an issue with and Search field is hanging after pasting value that could not be found and trying to remove it.
export class TableToolbar extends Component {
state = {
search: this.props.search,
}
static getDerivedStateFromProps(props, state) {
// Re-run the table whenever the search text change.
// we need to store prevSearch to detect changes.
if (props.search !== state.prevSearch) {
return {
search: props.search,
prevSearch: state.search,
}
}
return null
}
captureInput = e => {
if (this.timer) {
clearTimeout(this.timer)
delete this.timer
}
this.input = e.target.value
this.setState({search: this.input})
this.timer = setTimeout(() => {
this.props.handleSearch(this.input)
delete this.input
}, capturedInputTimeout)
}
render() {
<input onChange={this.captureInput} value={this.state.search} />
}
}
Also I found another solution to debounce this handleSearch request with use-debounce https://github.com/xnimorz/use-debounce
But still not quite understand how to rerender component correctly.
I need pass search props from parent in some case when I want to keep search value when move between pages.
Second variant with use-debounce, but still not quite understand how to rerender component when search props updates
const TableToolbar = ({search, handleSearch}) => {
const [searchValue, setSearchValue] = useState(search)
const [debouncedText] = useDebounce(searchValue, 500)
useEffect(() => {
handleSearch(debouncedText)
},
[debouncedText]
)
render() {
<input onChange={e => setSearchValue(e.target.value)} />
}
}

For the issue with hanging I think your captureInput function runs on every re-render. You should can call it like this to avoid that onChange={() => this.captureInput
For rerendering on change you could look into shouldComponentUpdate where you've got acccess to nextProps which you can compare with the current props and return true if different.

Related

React Warning: Cannot update a component from inside the function body of a different component

I am using Redux with Class Components in React. Having the below two states in Redux store.
{ spinner: false, refresh: false }
In Parent Components, I have a dispatch function to change this states.
class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
this.props.onShowSpinner();
this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
In Child Component, I am trying to reload the parent component like below.
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
if (somecondition) {
// doing some redux store update
props.reloadApp();
}
}
render() {
return <button />;
}
}
I am getting error as below.
Warning: Cannot update a component from inside the function body of a
different component.
How to remove this warning? What I am doing wrong here?
For me I was dispatching to my redux store in a React Hook. I had to dispatch in a useEffect to properly sync with the React render cycle:
export const useOrderbookSubscription = marketId => {
const { data, error, loading } = useSubscription(ORDERBOOK_SUBSCRIPTION, {
variables: {
marketId,
},
})
const formattedData = useMemo(() => {
// DISPATCHING HERE CAUSED THE WARNING
}, [data])
// DISPATCHING HERE CAUSED THE WARNING TOO
// Note: Dispatching to the store has to be done in a useEffect so that React
// can sync the update with the render cycle otherwise it causes the message:
// `Warning: Cannot update a component from inside the function body of a different component.`
useEffect(() => {
orderbookStore.dispatch(setOrderbookData(formattedData))
}, [formattedData])
return { data: formattedData, error, loading }
}
If your code calls a function in a parent component upon a condition being met like this:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
Try wrapping the condition in a useEffect:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
useEffect(() => {
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
}, [data, handleNoUsersLoaded]);
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
It seems that you have latest build of React#16.13.x. You can find more details about it here. It is specified that you should not setState of another component from other component.
from the docs:
It is supported to call setState during render, but only for the same component. If you call setState during a render on a different component, you will now see a warning:
Warning: Cannot update a component from inside the function body of a different component.
This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the setState call into useEffect.
Coming to the actual question.
I think there is no need of getDerivedStateFromProps in the child component body. If you want to trigger the bound event. Then you can call it via the onClick of the Child component as i can see it is a <button/>.
class Child extends React.Component {
constructor(props){
super(props);
this.updateState = this.updateState.bind(this);
}
updateState() { // call this onClick to trigger the update
if (somecondition) {
// doing some redux store update
this.props.reloadApp();
}
}
render() {
return <button onClick={this.updateState} />;
}
}
Same error but different scenario
tl;dr wrapping state update in setTimeout fixes it.
This scenarios was causing the issue which IMO is a valid use case.
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setSomeState(someNewValue);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
fix
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setTimeout(() => {
setSomeState(someNewValue);
}, 0);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
In my case I had missed the arrow function ()=>{}
Instead of onDismiss={()=>{/*do something*/}}
I had it as onDismiss={/*do something*/}
I had same issue after upgrading react and react native, i just solved that issue by putting my props.navigation.setOptions to in useEffect. If someone is facing same problen that i had i just want to suggest him put your state changing or whatever inside useEffect
Commented some lines of code, but this issue is solvable :) This warnings occur because you are synchronously calling reloadApp inside other class, defer the call to componentDidMount().
import React from "react";
export default class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
// this.props.onShowSpinner();
// this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
// if (somecondition) {
// doing some redux store update
props.reloadApp();
// }
}
componentDidMount(props) {
if (props) {
props.reloadApp();
}
}
render() {
return <h1>This is a child.</h1>;
}
}
I got this error using redux to hold swiperIndex with react-native-swiper
Fixed it by putting changeSwiperIndex into a timeout
I got the following for a react native project while calling navigation between screens.
Warning: Cannot update a component from inside the function body of a different component.
I thought it was because I was using TouchableOpacity. This is not an issue of using Pressable, Button, or TouchableOpacity. When I got the error message my code for calling the ChatRoom screen from the home screen was the following:
const HomeScreen = ({navigation}) => {
return (<View> <Button title = {'Chats'} onPress = { navigation.navigate('ChatRoom')} <View>) }
The resulting behavior was that the code gave out that warning and I couldn't go back to the previous HomeScreen and reuse the button to navigate to the ChatRoom. The solution to that was doing the onPress in an inline anonymous function.
onPress{ () => navigation.navigate('ChatRoom')}
instead of the previous
onPress{ navigation.navigate('ChatRoom')}
so now as expected behavior, I can go from Home to ChatRoom and back again with a reusable button.
PS: 1st answer ever in StackOverflow. Still learning community etiquette. Let me know what I can improve in answering better. Thanx
If you want to invoke some function passed as props automatically from child component then best place is componentDidMount lifecycle methods in case of class components or useEffect hooks in case of functional components as at this point component is fully created and also mounted.
I was running into this problem writing a filter component with a few text boxes that allows the user to limit the items in a list within another component. I was tracking my filtered items in Redux state. This solution is essentially that of #Rajnikant; with some sample code.
I received the warning because of following. Note the props.setFilteredItems in the render function.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- Danger! Updates Redux during a render!
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I ran this code with React 16.12.0, I received the warning listed in the topic of this thread in my browser console. Based on the stack trace, the offending line was my props.setFilteredItems invocation within the render function. So I simply enclosed the filter invocations and state change in a useEffect as below.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
useEffect(() => {
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- OK now; effect runs outside of render.
}, [nameFilter, cityFilter]);
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I first added the useEffect I blew the top off the stack since every invocation of useEffect caused state change. I had to add an array of skipping effects so that the effect only ran when the filter fields themselves changed.
I suggest looking at video below. As the warning in the OP's question suggests, there's a change detection issue with the parent (Parent) attempting to update one child's (Child 2) attribute prematurely as the result of another sibling child's (Child 1) callback to the parent. For me, Child 2 was prematurely/incorrectly calling the passed in Parent callback thus throwing the warning.
Note, this commuincation workflow is only an option. I personally prefer exchange and update of data between components via a shared Redux store. However, sometimes it's overkill. The video suggests a clean alternative where the children are 'dumb' and only converse via props mand callbacks.
Also note, If the callback is invoked on an Child 1 'event' like a button click it'll work since, by then, the children have been updated. No need for timeouts, useEffects, etc. UseState will suffice for this narrow scenario.
Here's the link (thanks Masoud):
https://www.youtube.com/watch?v=Qf68sssXPtM
In react native, if you change the state yourself in the code using a hot-reload I found out I get this error, but using a button to change the state made the error go away.
However wrapping my useEffect content in a :
setTimeout(() => {
//....
}, 0);
Worked even for hot-reloading but I don't want a stupid setTimeout for no reason so I removed it and found out changing it via code works just fine!
I was updating state in multiple child components simultaneously which was causing unexpected behavior. replacing useState with useRef hook worked for me.
Try to use setTimeout,when I call props.showNotification without setTimeout, this error appear, maybe everything run inTime in life circle, UI cannot update.
const showNotifyTimeout = setTimeout(() => {
this.props.showNotification();
clearTimeout(showNotifyTimeout);
}, 100);

How to prevent container component from re-rendering when child component updates

I've been trying to optimise an app by preventing the container component in the example below from re-rendering every time the child component updates. The app example only has one counter which is updated via a key. I've stripped the example down to just one counter although I'm using multiple counters in the actual app.
It works by dispatching an increment action every second, which updates the redux state. The container maps through each counter (in this case just one) held in state and renders a child component which just displays the count.
I've looked at using PureComponent and shouldComponentUpdate but couldn't get it to stop re-rendering and I'm also not sure if the latter is the best approach. From similiar questions I've gleaned that the problem might be that both the container and child component are referring to the same object in state and therefore are both updated. Sandbox example here: https://codesandbox.io/s/nostalgic-rubin-k1gxy
Container
class CounterContainer extends React.PureComponent {
renderWatches = () => {
//was some map logic here originally hence separate method
const { counters } = this.props;
const counterKeys = Object.keys(counters);
return counterKeys.map(key => {
return <SingleCounter key={key} mapKey={key} />;
});
};
render() {
console.log("CounterContainer rendered");
return <div>{this.renderWatches()}</div>;
}
}
const mapStateToProps = state => {
return {
counters: state.counters
};
};
export default connect(mapStateToProps)(CounterContainer);
Child
export const SingleCounter = props => {
useEffect(() => {
const interval = setInterval(() => {
props.dispatch(actionCreators.increment(props.counter.key));
}, 1000);
return () => clearInterval(interval);
});
console.log("Child rendered");
return <div>{props.counter.counter}</div>;
};
export const mapStateToProps = (state, ownProps) => {
return {
counter: state.counters[ownProps.mapKey]
};
};
export default connect(mapStateToProps)(SingleCounter);
Looking at the console logs, I'd ideally like to see the child component re-render every time the counter is incremented however the container should render just once (or in the actual app, whenever another counter is added)

Exposing the state of a react widget

I have made a react UI widget thats let's the user select a number of different times and dates. The user's current selection is stored in the state of a top level component, DateTimePicker. I then have a widget wrapper like so:
import ...
export default {
new: (args) => {
const store = {
reactElement: <DateTimePicker
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return store.reactElement.getState(); // DOESN'T WORK
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
I want to add a validation to make sure that at least X days/times are selected, but this validation needs to be implemented outside of the widget.
For this, I'll need someway of asking the widget of it 's state. i.e. what has the user selected? Although it seems like the state of the class is not part of the public api of a react component.
How can I acess the state, or is there another way I'm missing?
The solution to doing things imperatively from the parent to the child usually involves getting a ref to the child component. Something along these lines:
export default {
new: (args) => {
let myRef = React.createRef();
const store = {
reactElement: <DateTimePicker
ref={myRef}
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return myRef.current.getState();
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
With ref={myRef} added as a prop, whenever DateTimePicker gets mounted, it will assign a reference to the mounted component to myRef.current. You can then use that reference to interact directly with the most recently mounted component.

React: UI Flickering When State Updated

I have a component that displays search data returned from the Spotify API. However, every time I update the state the UI flickers:
Input:
<DebounceInput
debounceTimeout={300}
onChange={handleChange}
/>
Hook:
const [searchResults, setSearchResults] = useState(null)
API call w/ Apollo:
const searchSpotify = async (query) => {
const result = await props.client.query({
query: SearchTracks,
variables: {
query
}
})
const tracks = result.data.searchedTracks
setSearchResults(tracks)
}
Render:
{searchResults &&
<div className="search-results">
{searchResults.map((song) => (
<SongInfo key={song.id} {...song} />
))}
</div>
}
I noticed it only happens on the first load. For example, if I were to type the query again it shows without flickering. Is there a better way to implement this so the UI doesn't flicker?
Below are the frames that cause the flicker. What I think is happening is it takes some time for the images to load. While they are loading the items have reduced height. You should make sure SongInfo layout does not depend on whether the image has been loaded or not.
Images not loaded - items are collapsed:
Images were loaded:
I think whats happening is that you are executing a search query on every key stroke which is causing the weird behavior.
Use lodash debounce to avoid doing a search on every key stroke.
That should address the flickering. (Also, adding a loading state will help)
Sample debounce component
import React, {Component} from 'react'
import { debounce } from 'lodash'
class TableSearch extends Component {
//********************************************/
constructor(props){
super(props)
this.state = {
value: props.value
}
this.changeSearch = debounce(this.props.changeSearch, 250)
}
//********************************************/
handleChange = (e) => {
const val = e.target.value
this.setState({ value: val }, () => {
this.changeSearch(val)
})
}
//********************************************/
render() {
return (
<input
onChange = {this.handleChange}
value = {this.props.value}
/>
)
}
//********************************************/
}

React setState not immediately updating component view

I have a input field that is filtering the elements on of an array.
The search results are always one keystroke behind, I assume because setState doesn't instantly update the view? What's the best way to work around that?
class App extends Component {
constructor() {
super();
this.state = {
images:[],
searchfield: '',
filteredImages:[],
suggestedKeywords:[],
inputValue: ''
}
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
this.setState({inputValue: event.target.value});
let filteredImages = this.state.images.filter(image => {
return image.labels.includes(this.state.searchfield.toLowerCase());
});
console.log(event.target.value);
this.setState({filteredImages});
}
}
const SearchBox = ({searchfield, searchChange, inputValue}) => {
return (
<div>
<input
type="search"
value={inputValue}
onChange={searchChange}
placeholder="Search images..."
/>
</div>
);
}
The search results are always one keystroke behind, I assume because setState doesn't instantly update the view? What's the best way to work around that?
That isn't the problem.
Your problem is that you are assuming updates to setState occur instantly.
this.setState({searchfield: event.target.value}); //You update searchfield here
return image.labels.includes(this.state.searchfield.toLowerCase());
//but this.state.searchfield doesn't reflect the update yet!
So instead, simply work off of the updated value rather than the one from the store.
return image.labels.includes(event.target.value.toLowerCase());
setState is asynchronous, so you will be filtering by the old searchfield when you do this.state.searchfield.toLowerCase(). With this in mind you could do something like this instead:
onSearchChange = (event) => {
const { value } = event.target;
const newState = {
searchField: value,
inputValue: value
};
newState.filteredImages = this.state.images.filter(image => {
return image.labels.includes(value.toLowerCase());
});
this.setState(newState);
}

Categories

Resources