updating parent component on child reactquill onChange() in component with componentWillReceiveProps - javascript

Hi am using reactquill in my child component and i want to update my parent state when users type. currently i am doing it using onBlur() but that is not what the users want.
this is my child component.
public componentWillReceiveProps(newProps): void {
//console.log(newProps, "new props");
this.setState({
text: newProps.value
});
}
public setProps() {
//console.log("set props", this.state.text);
if(this.state.text === "<p><br></p>"){
this.props.onChange("");
} else {
this.props.onChange(this.state.text);
}
}
public handleChange(value) {
this.setState({ text: value });
//console.log("update props of parent", value);
//this.props.onChange(value);
}
public render() {
return (
<div>
<div className="text-editor" onBlur= {this.setProps}>
<ReactQuill value={this.state.text}
onChange={this.handleChange}
//onKeyPress={this.handleKeyDown}
//onKeyDown={this.handleKeyDown}
onBlur= {this.setProps}
modules={this.modules}
formats={this.formats}/>
</div>
</div>
);
}
and this i from my Parent Component calling the child;
public renderEditableAnswer = (cellInfo) => {
return (
<div>
<QnAAnswerInput
value={cellInfo.original.Answer}
onChange={data => this.updateQnAAnswer(data, cellInfo)}
/>
</div>
);
}
public updateQnAAnswer = (data, cellInfo) => {
let qnaItems = [...this.state.qnaItems];
let index;
if(cellInfo.original.Id != null){
index = _.findIndex(qnaItems,d => d.Id == cellInfo.original.Id);
} else {
index = _.findIndex(qnaItems,d => d.identifier == cellInfo.original.identifier);
}
if(this.getText(data) !== this.getText(cellInfo.original.Answer)){
let item = {
...qnaItems[index],
Answer: data,
};
qnaItems[index] = item;
this.setState({ qnaItems });
this.updateActionHistory(item,index);
}
}
this component is inside a ReactTable cell, hence the cellInfo. Note that i do have one functionality in the parent component that would add a new row to the table which needs to have an empty values for the child component. i noticed that without the WillReceiveProps method, my "Add New Empty Row" is not working.
In my current code, if i comment out the this.props.onChange(this.state.text); inside the handleChange method, typing inside the editor fires the componentWillReceiveProps (iterating through all my reacttable values, which is a lot) which renders a delay in typing a text. and this is not good.
is there anyway for me to update my parent state with onChange without having typing delays?

Use only componentDidMount() and componentDidUpdate() the other life cycle methods are bad practice.
You have a typing delay because of componentWillReceiveProps, never use it. I do not understand your code, there are no names and you have unnecessary code.

Instead of onBlur= {this.setProps} in div ,
call it in componentDidUpdate
componentDidUpdate = ( prevProps , prevState) =>{
if(prevState.editorHtml !== this.state.editorHtml )
this.setProps()
}
Do you have any better solution?

Related

Rerender input react component on parent props change

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.

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />

React setState re-render

First of all, I'm really new into React, so forgive my lack of knowledge about the subject.
As far as I know, when you setState a new value, it renders again the view (or parts of it that needs re-render).
I've got something like this, and I would like to know if it's a good practice or not, how could I solve this kind of issues to improve, etc.
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
key: value
}
this.functionRender = this.functionRender.bind(this)
this.changeValue = this.changeValue.bind(this)
}
functionRender = () => {
if(someParams !== null) {
return <AnotherComponent param={this.state.key} />
}
else {
return "<span>Loading</span>"
}
}
changeValue = (newValue) => {
this.setState({
key: newValue
})
}
render() {
return (<div>... {this.functionRender()} ... <span onClick={() => this.changeValue(otherValue)}>Click me</span></div>)
}
}
Another component
class AnotherComponent extends Component {
constructor (props) {
super(props)
}
render () {
return (
if (this.props.param === someOptions) {
return <div>Options 1</div>
} else {
return <div>Options 2</div>
}
)
}
}
The intention of the code is that when I click on the span it will change the key of the state, and then the component <AnotherComponent /> should change because of its parameter.
I assured that when I make the setState, on the callback I throw a console log with the new value, and it's setted correctly, but the AnotherComponent doesn't updates, because depending on the param given it shows one thing or another.
Maybe I need to use some lifecycle of the MyComponent?
Edit
I found that the param that AnotherComponent is receiving it does not changes, it's always the same one.
I would suggest that you'll first test it in the parent using a simple console.log on your changeValue function:
changeValue = (newValue) => {
console.log('newValue before', newValue);
this.setState({
key: newValue
}, ()=> console.log('newValue after', this.state.key))
}
setState can accept a callback that will be invoked after the state actually changed (remember that setState is async).
Since we can't see the entire component it's hard to understand what actually goes on there.
I suspect that the newValue parameter is always the same but i can't be sure.
It seems like you're missing the props in AnotherComponent's constructor. it should be:
constructor (props) {
super(props) // here
}
Try replacing the if statement with:
{this.props.param === someOptions? <div>Options 1</div>: <div>Options 2</div>}
also add this function to see if the new props actually get to the component:
componentWillReceiveProps(newProps){
console.log(newProps);
}
and check for the type of param and someOptions since you're (rightfully) using the === comparison.
First, fat arrow ( => ) autobind methods so you do not need to bind it in the constructor, second re-renders occur if you change the key of the component.
Ref: https://reactjs.org/docs/lists-and-keys.html#keys

(react) Listening to dropdown/select value changes triggered by changed data list propagated as props

I am using a simple HTML select dropdown and controlling it with react (==> controlled component). Everything fine so far. The Problem is - the select options are updated via an async ajax call every few seconds and empty at the beginning. The selects data list is propagated via props.
So, the select data list changes, the selected options list changes - but no change is fired (afaik by design of react).
I have found a working way to listen for these changes by listening to "componentDidUpdate" and firing a onChange "by hand" by reading out the value of the select as reference - but this seems very "un react-ish" (code below). Does anyone know the "react" way to do this?
Full code:
class Dropdown extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.dropDown = React.createRef();
}
handleChange(event) {
if (this.props.onChange) this.props.onChange(event.target.value);
}
componentDidUpdate(prevProps) {
if (this.props.options.length != prevProps.options.length) {
if (this.props.onChange) this.props.onChange(this.dropDown.current.value);
} else {
for (let i = 0; i < this.props.options.length; i++) {
if (this.props.options.value != prevProps.options.value) {
if (this.props.onChange) this.props.onChange(this.dropDown.current.value);
return;
}
}
}
}
render() {
const optionList = this.props.options.map(option => <option value={option.value} key={option.value}>{option.name}</option>);
return <select value={this.props.value} onChange={this.handleChange} ref={this.dropDown}>{optionList}</select>;
}
}
props.options start as empty list. Some parent node holds this list as a state and updates it every few seconds with a ajax request.
Sandbox code: https://codesandbox.io/s/6l927kpx13
You should pass props to state.
state = {
options: this.props.options,
}
render method:
render() {
const optionList = this.state.options.map((option, index) => (
<option key={index} value={option.price}>{option.price}</option>
));
return (
<select>{optionList}</select>
);
}
listener for props changes:
componentDidUpdate(prevProps) {
if (this.props.options[0].price !== prevProps.options[0].price) {
this.setState({
options: this.props.options,
});
}
}
Try this codesandbox https://codesandbox.io/s/pjky3r4z60
React handles it's updates by looking at a component's props and state. The way you've implemented it now is mostly correct, whenever you call setState(), a re-render is triggered.
However, the onChange event you're looking for is not whenever your options are dynamically updated, but this event gets triggered whenever your user selects a different option. This has nothing to do with React.
See the answer provided by Rizal Ibnu if you want to check for updates in a more efficient manner.
However, I would add some updates to your code, it could be shorter:
class Dropdown extends React.Component {
constructor(props) {
super(props);
}
// You can 'bind' this also with an arrow function
handleChange = event => {
if (this.props.onChange) this.props.onChange(event.target.value);
};
componentDidUpdate(prevProps) {
if (this.props.options.length != prevProps.options.length) {
if (this.props.onChange) this.props.onChange(this.dropDown.current.value);
} else {
this.props.options.forEach(() => {
if (this.props.options.value != prevProps.options.value) {
if (this.props.onChange)
this.props.onChange(this.dropDown.current.value);
return;
}
})
}
}
}
render() {
return (
<select
value={this.props.value}
onChange={this.handleChange}
// Consider using callback refs
ref={dropdown => (this.dropDown = dropDown)}
>
// Pure preference, I like mapping a list inline
{this.props.options.map(option => (
<option value={option.value} key={option.value}>
{option.name}
</option>
))}
</select>
);
}
}
I would look again at the this.props.onChange method from your parent, I don't think it should be undefined.

Notify react components about value change

Suppose that I have a component class which is responsible to change any number entered into textbox to text:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
// HERE I NEED TO NOTIFY ABOUT CHANGES
}
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
Now the usage would look something like this:
<NumbersToText>
<input onChange={this.saveValue}
</NumbersToText>
Let's say that all works, and the value gets changed to text.
Now the problem is that after I change numbers to text and assign that value to input onChange handlers are not executed again, thus saveValue is not called with updated value.
How should this problem be approached in order to trigger onChange handlers with new value?
I don't know exactly what you mean by numbers to text so I'll just assume you want to modify the value before calling the onChange function in the input, and also reflect that value in the input.
First of all, what you're doing will never work on React, React reflects internal virtual objects into the DOM, meaning you shloud not modify the DOM directly and instead you should modify this internal representantion (via setState, props) to reflect this change into the DOM.
There's also two types of inputs on React, controlled and uncontrolled. I will assume you want to use this on uncontrolled inputs.
The only possible solution I can see, is to transform the input using the React.cloneElement function adding a aditional step before calling the input's onChange callback.
Here's a possible implementation that will make the input uppercase.
class UpperCase extends React.Component {
constructor(props) {
super(props);
}
onChange(e, input, next) {
let value = input.value || '';
value = value.toUpperCase();
input.value = value;
next(value);
}
render() {
let childs = React.Children.map(this.props.children, child => {
let input = null; //Will take advantage of javascript's closures
let onChangeChild = child.props.onChange.bind(child);
return React.cloneElement(child, {
ref: ref => input = ref,
onChange: e => {
this.onChange(e, input, onChangeChild)
}
});
});
return (
<span>
{childs}
</span>
);
}
}
And you can use it like this:
<UpperCase>
<input onChange={(val) => console.log(val)}></input>
<textarea onChange={(val) => console.log(val)}></textarea>
</UpperCase>
Thanks to #tiagohngl I came up with a similar, but maybe a little less cluttered (without cloning elements) way:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
this.childrenOnChange(event);
}
}
childrenOnChange(event) {
const { children } = this.props;
React.Children.forEach(children, child => {
if (child.props.onChange) {
child.props.onChange(event);
}
});
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
export default class NumbersToText extends React.Component {
constructor(props) {
super(props)
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
this.setState({ anyData: [] });
}
onChange(event) {
this.setState({anyData: event.target.value},
()=>{console.log("AnyData: "+this.state.anyData)});
// callback to console.log after setState is done
}
render() {
return (
<input type="text"
value={this.state.anyData}
onChange={this.onChange} />
);
}
}
As you mention that,
onChange is not called after changed value.
There are multiple possibilities.
onChange is not binded.
There are no state change in render method, so it will not re-render
make use of console.log() to trace the problem
I slightly ammend the code for illustration.
Hope it helps.
How react handle State Change (answer I posted before)

Categories

Resources