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.
Related
In my React application, I would like a button that normally says Save, changes its text to Saving... when clicked and changes it back to Save once saving is complete.
This is my first attempt:
import React from 'react';
import { Button } from 'react-bootstrap';
class SaveButton extends React.Component {
constructor(props) {
super(props);
this.state = { isSaving : false };
this.onClick = this.onClick.bind(this);
}
onClick() {
// DOES NOT WORK
this.setState({ isSaving : true });
this.props.onClick();
this.setState({ isSaving : false });
}
render() {
const { isWorking } = this.state;
return (
<Button bsStyle="primary"
onClick={isWorking ? null : this.onClick}>
{isWorking ? 'Saving...' : 'Save'}
</Button>
);
}
}
export default SaveButton;
This doesn't work because both setState and this.props.onClick() (passed in by the parent component) are executed asynchronously, so the calls return immediately and the change to state is probably lost in React optimization (and would probably be visible only for a few milliseconds anuway...).
I read up on component state and decided to lift up state to where it belongs, the parent component, the form whose changes the button saves (which in my app rather is a distant ancestor than a parent). So I removed isSaving from SaveButton, replaced
const { isWorking } = this.state;
by
const { isWorking } = this.props.isWorking;
and tried to set the state in the form parent component. However, this might be architecturally cleaner, but essentially only moved my problem elsewhere:
The actual saving functionality is done at a totally different location in the application. In order to trigger it, I pass onClick event handlers down the component tree as props; the call chain back up that tree upon a click on the button works. But how do I notify about completion of the action in the opposite direction, i.e. down the tree ?
My question: How do I notify the form component that maintains state that saving is complete ?
The form's parent component (which knows about save being complete) could use a ref, but there isn't only one of those forms, but a whole array.
Do I have to set up a list of refs, one for each form ?
Or would this be a case where forwarding refs is appropriate ?
Thank you very much for your consideration! :-)
This doesn't work because both setState and this.props.onClick()
(passed in by the parent component) are executed asynchronously, so
the calls return immediately and the change to state is probably lost
in React optimization
setState can take a callback to let you know once state has been updated, so you can have:
onClick() {
this.setState({ isSaving : true }, () => {
this.props.onClick();
this.setState({ isSaving : false });
});
}
If this.props.onClick() is also async, turn it into a promise:
onClick() {
this.setState({ isSaving : true }, () => {
this.props.onClick().then(() => {
this.setState({ isSaving : false });
});
});
}
can you see ?
https://developer.mozilla.org/tr/docs/Web/JavaScript/Reference/Global_Objects/Promise
parent component's onClick function
onClick(){
return new Promise(function(resolve,reject){
//example code
for(let i=0;i<100000;i++){
//or api call
}
resolve();
})
}
SaveButton component's onClick funtion
onClick(){
this.setState({ isSaving : true });
this.props.onClick().then(function(){
this.setState({ isSaving : false });
});
}
Here is your code that works :
import React from 'react';
import { Button } from 'react-bootstrap';
class SaveButton extends React.Component {
constructor() {
super();
this.state = {
isSaving: false
};
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
this.setState(prevState => {
return {
isSaving: !prevState.isSaving
}
});
console.log("Clicked!",this.state.isSaving);
this.handleSave();
}
handleSave() {
// Do what ever and if true => set isSaving = false
setTimeout(()=>{
this.setState(prevState=>{
return {
isSaving: !prevState.isSaving
}
})
},5000)
}
render() {
const isWorking = this.state.isSaving;
return (
<Button
bsStyle="primary"
onClick={this.handleOnClick}
>
{isWorking ? 'Saving...' : 'Save'}
</Button>
);
}
}
export default SaveButton;
You need to change const isWorking instead of const {isWorking} and you need to handle save function in some manner.
In this example I used timeout of 5 second to simulate saving.
I'm using a high order component that is not rendering the child on a render change. The code below has been chopped down for simplicity sake.
The HOC that looks like this:
const withMyComponent = (WrapperComponent) => {
class HOC extends Component {
state = {
value: ''
};
changedHandler = (event) => {
this.setState({ value: event.target.value });
};
render() {
return (<WrapperComponent {...this.props}
changed={this.changedHandler}
value={this.state.value} />);
}
};
return HOC;
};
export default withMyComponent;
Then I have a child component that uses this HOC:
class myChildComponent extends Component {
render() {
return (
<input type="text"
onChange={(event) => this.props.changed(event)}
value={this.props.value || ''} />
);
};
};
export default withMyComponent(myChildComponent);
The problem I am experiencing is that the input is not updating with the new value that is passed back from the HOC. In fact, the child render is not even firing after the initial mount and doesn't seem to fire on any changed event.
I have placed debugging and console.logs in the HOC render and the changed event and the render are firing with the proper data.
So, if I type in the textbox, it fires the change event in the HOC and updates the state in the HOC and fires the render event in the HOC. But it is not firing the wrapped components render event, and thus the textbox never updates with any values.
Anyone able to solve this issue or lead me in the right direction?
Thanks again.
You need to pass an object to this.setState
const withMyComponent = WrapperComponent => {
class HOC extends Component {
state = {
value: ""
};
changedHandler = event => {
// Instead of
// this.setState( value: event.target.value );
// Do This.
this.setState({ value: event.target.value });
};
render() { ... }
}
return HOC;
};
Working Demo
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>
);
}
}
The situation is this: i have a database, where the data is. The data is structured with an array of objects. The objects have three properties (which are relevant now). These are: id, parentId, and status. The components build up with these properties, and the component clones itself recursively, so they are nested within each other. It looks like this:
class Task extends React.Component{
constructor(props){
super(props);
this.state = {
status: this.props.status
}
}
//gets fired right after the state changed by removeTask
static getDerivedStateFromProps(props){
console.log("state from props")
return{status: props.status}
}
componentDidUpdate(){
console.log("update");
this.checkDeleted()
}
checkDeleted = () => {
if (this.state.status === 'deleted'){
deleteTask(this.props.id) //a function which deletes from database
}
}
tasks = () => {
console.log("childs rendered")
return this.props.tasks
.filter(task => task.pId === this.props.id)
.map(task => {
return <Task
id={task.id}
pId={task.pId}
status={this.state.status}
/>
})
}
removeTask = () => {
console.log("state changed")
this.setState({status: 'deleted'})
}
render(){
console.log("render")
return(
<div
<button onClick={this.removeTask} />
{this.tasks()}
</div>
)
}
}
What happens: the order of the logs are the next:
state changed (removeTask())
state from props (gDSFP())
render
childs rendered (tasks() fired inside render())
update (componentDidUpdate())
This isn't good, because when the state changed from removeTask(), it gets right back from the props with gDSFP, before the component can pass the changed state to it's childs. But i want to set the state from the props, because the childs need to get it. What could happen here is: the removeTask() fired, sets the new state, rerender, the childs get the new status as a prop, and when the update happens, deletes all the component, and it's child from the database. So what will be good:
click happened, set new state
render
render childs, set they status prop to the state
check if the status is "deleted"
set state from props if it's changed, and rerender
How to earn this?
I have problem with your order to begin with. Your data depends on whats in the DB. You might delete from the state and the DB task failed. So why bother updating the state manually. Just listen and load your state from props that come from DB. when you delete from the DB, your props will be updated and re-render will occur. So basically if i were you , i would stick with
class Task extends React.Component{
constructor(props){
super(props);
}
//gets fired right after the state changed by removeTask
static getDerivedStateFromProps(props){
console.log("state from props")
return{status: props.status}
}
componentDidUpdate(){
console.log("update");
}
tasks = () => {
console.log("childs rendered")
return this.props.tasks
.filter(task => task.pId === this.props.id)
.map(task => {
return <Task
id={task.id}
pId={task.pId}
status={this.props.status}
/>
})
}
removeTask = () => {
console.log("state changed");
deleteTask(this.props.id) //a function which deletes from database
}
render(){
console.log("render")
return(
<div
<button onClick={this.removeTask} />
{this.tasks()}
</div>
)
}
}
from the above, you can notice i removed checkDeleted because i don't need to update my state. I can just rely on props. Remove set state status because i can just rely on props sttaus which btw tally or is in sync with DB.
Here is my editing component:
class EditField extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
edit(e) {
this.setState({ value: e.target.value });
if (e.keyCode === 13) {
this.props.onEdited(this.state.value);
}
}
render() {
return (
<div>
<input
type="text"
value={this.state.value}
onChange={this.edit.bind(this)}
/>
</div>
)
}
}
I need to populate state from props like this:
function Container({ entity, onEdited }) {
return (
<div>
<EditField onEdited={onEdited} value={entity.firstName} />
<EditField onEdited={onEdited} value={entity.lastName} />
</div>
);
}
The Container component get onEdited and entity props from redux store.
Container's parent will handle data fetching and onEdited (which will
only be triggered if user hit Enter) will dispatch request to the server.
My problem is how to initialize value props properly? Because if I use:
componentDidMount() {
this.setState({
value: this.props.value
});
}
I got empty state because fetching data is not finished when componentDidMount
called. And if I use:
componentWillReceiveProps(nextProps) {
this.setState({
value: nextProps.value
});
}
I got this warning:
Warning: EditField is changing a controlled input of type text to be
unncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
So, how to do this correctly?
This is what I recommend:
You could use getInitialState from EditField to populate the value state from the value prop. But this won't work, because getInitialState will only be called once, so subsequent renders will not update the state. Besides, this is an anti-pattern.
You should make the EditField component controlled. Always pass the current value as prop and stop dealing with state at all. If you want a library to help you link the input state with Redux, please take a look at Redux-Form.
The onEdited event you created, at least the way you did it, doesn't play well with controlled inputs, so, what you want to do is to have an onChange event that is always fired with the new value, so the Redux state will always change. You may have another event triggered when the user hits enter (e.g onEnterPressed), so you can call the server and update the entity values. Again. Redux-Form can help here.
Apparently entity.firstName and entity.lastName can only contain the values that the user has confirmed (hit enter), not temporary values. If this is the case, try to separate the state of the form from the state of the entity. The state of the form can be controlled by Redux-Form. When the user hits enter, you can trigger an action that actually calls the server and updates the state of the entity. You can even have a "loading" state so your form is disabled while you're calling the server.
Since Container subscribes to Redux store, I suggest make the EditField stateless functional component. Here's my approach:
const EditField = ({
onEdited,
value
}) => (
<div>
<input
type="text"
value={value}
onChange={onEdited}
/>
</div>
);
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
edit = (e) => {
this.setState({value: e.target.value});
e.keyCode === 13 ? this.props.onEdited(this.state.value) : null;
};
sendValue = (val) => val ? val : this.state.value;
render() {
this.props = {
firstName: "Ilan",
lastName: null
}
let { firstName, lastName, onEdited } = this.props;
return (
<div>
<EditField onEdited={this.edit} value={this.sendValue(firstName)} />
<EditField onEdited={this.edit} value={this.sendValue(lastName)} />
</div>
)
}
}
ReactDOM.render(<Container />, document.getElementById('app'));
A live demo: https://codepen.io/ilanus/pen/yJQNNk
Container will send either firstName, lastName or the default state...