How to turn React RichTextEditor class into a function? - reactjs

I'm using react-rte in my project and try to get the text editor working, but with no success. I copied the class from the above website (code below) and tried to turn it into a function, since I have more experience with them. What am I missing?
Class from react-rte-link
class MyStatefulEditor extends Component {
static propTypes = {
onChange: PropTypes.func
};
state = {
value: RichTextEditor.createEmptyValue()
}
onChange = (value) => {
this.setState({value});
if (this.props.onChange) {
// Send the changes up to the parent component as an HTML string.
// This is here to demonstrate using `.toString()` but in a real app it
// would be better to avoid generating a string on each change.
this.props.onChange(
value.toString('html')
);
}
};
render () {
return (
<RichTextEditor
value={this.state.value}
onChange={this.onChange}
/>
);
}
}
And here's my function:
function MyStatefulEditor ({ values, setValues }) {
const value = RichTextEditor.createEmptyValue();
console.log(values, setValues);
const handleChange = name => event => {
setValues({ ...values, [name]: event.target.value });
value.toString("html");
};
return (
<RichTextEditor
value={values.bodyText}
onChange={handleChange("bodyText")}
required
id="body-text"
name="bodyText"
className={classes.textField}
type="string"
multiline
rows="20"
variant="filled"
style={{ minHeight: 410 }}
/>
);
}

I got it working, in case anyone is looking for the answer!
// values.bodyText has the database information,
// if I'm not adding a new thing, but editing an old one
<BodyTextEditor
value={values.bodyText}
setValue={bodyText => setValues({...values, bodyText })}
/>
Function:
// some things inside RTE are just for styling, delete the id's etc.
function BodyTextEditor({ value, setValue }) {
const [editorValue, setEditorValue] =
React.useState(RichTextEditor.createValueFromString(value, 'markdown'));
const handleChange = value => {
setEditorValue(value);
setValue(value.toString("markdown"));
};
return (
<RichTextEditor
value={editorValue}
onChange={handleChange}
required
id="body-text"
name="bodyText"
type="string"
multiline
variant="filled"
style={{ minHeight: 410 }}
/>
);
}

import React, { useState } from 'react';
import RichTextEditor from 'react-rte';
const BodyTextEditor = (props) => {
const [value, setValue] = useState(RichTextEditor.createEmptyValue());
const onChange = (value) => {
setValue(value);
if (props.onChange) {
props.onChange(value.toString('html'));
}
};
return <RichTextEditor value={value} onChange={onChange} />;
};
export default BodyTextEditor;
You can use this as your child component.

Related

Can't access forwareded Ref value in parent component React typescript

I have a situation where i have created a component for input. This is a custom component and i want to access the value entered by user in this input in the parent component(where i am using it).
I am forwarding ref from this Input Component but the parent component is receiving the complete input not the value. How can i use the value.
Below is my code.
Input.tsx
interface AuxProps {
id :''
}
const Input = React.forwardRef<HTMLInputElement,AuxProps>((props, ref) => {
return (
<input
id={props.id}
ref = {ref}
defaultValue = '1'
type='number'
></input>
);
});
export default Input;
HeaderComponent.tsx
const HeaderComponent= () => {
const inputAmount = useRef<HTMLInputElement>(null);
const addProductHandle = (event: any) => {
event.preventDefault();
console.log(inputAmount.current.value); //<----Error:- object is possibly null
};
return (
<form className={classes["form"]}>
<Input id="1s" ref={inputAmount}></Input>
<button onClick={addProductHandle}> + ADD </button>
</form>
);
};
export default HeaderComponent;
Not sure how can i use this ref value.
You were close.
Let's take a look on useRef return type:
interface RefObject<T> {
readonly current: T | null;
}
According to this type signature, current property might be T (in our case HTMLInputElement) or null.
This is why you are using typescript - to avoid errors on PROD.
Since current might be null, TS asks you to double check if current exists.
you can add ? or if condition:
import React, { useRef, MouseEventHandler } from 'react'
interface AuxProps {
id: string
}
const Input = React.forwardRef<HTMLInputElement, AuxProps>((props, ref) => {
return (
<input
id={props.id}
ref={ref}
defaultValue='1'
type='number'
></input>
);
});
const HeaderComponent = () => {
const inputAmount = useRef<HTMLInputElement>(null);
const addProductHandle: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
console.log(inputAmount.current?.value); // ok
if (inputAmount.current) {
console.log(inputAmount.current.value); //ok
}
};
return (
<form >
<Input id="1s" ref={inputAmount}></Input>
<button onClick={addProductHandle}> + ADD </button>
</form>
);
};
export default HeaderComponent;
Btw, you can use MouseEventHandler<HTMLButtonElement> for click handlers. See my example

Props "Object is possibly 'undefined'" in React Typescript

I am new to Typescript and I have an error I don't understand in React Typescript. I suspect that it comes from the way I write my interface but I am not sure.
First I call my CellEditable component
<CellEditable value={'Hello'} onChange={() => {}} />
CEllEditable has an isEditable state that toggles InputText on click
CellEditable.tsx
import React, { useState } from 'react'
import Cell from './Cell.comp'
import InputText from './InputText.comp'
interface CellEditableProps {
value: string
onChange?: () => void
}
const renderCellInput = (type: string, opts: any) => {
switch (type) {
case 'text':
return <InputText {...opts} />
default:
return <div>Missing!</div>
}
}
const CellEditable = (props: CellEditableProps) => {
const { value, onChange } = props
const [isEditing, setEditing] = useState<boolean>(false)
const handleClick = () => setEditing(!isEditing)
const handleBlur = () => setEditing(!isEditing)
const opts = {
value,
helpers: {
onBlur: handleBlur,
onChange
}
}
return (
<div onClick={handleClick}>
{
isEditing
? renderCellInput('text', opts)
: <Cell value={value} />
}
</div>
)
}
export default CellEditable
InputText.tsx
import React from 'react'
interface InputTextProps {
value?: string
helpers?: HelpersProps
}
interface HelpersProps {
onChange?: () => void
onBlur?: () => void
}
const InputText = (props: InputTextProps) => {
const { value, helpers } = props
console.log('propsInputText:', props) // Empty object in the console
return (
<input type={'text'} value={value} onChange={helpers.onChange} onBlur={helpers.onBlur} />
)
}
export default InputText
The issue is:
helpers.onChange gets this error "Object is possibly 'undefined'. TS2532"
console.log('propsInputText:', props) in InputText.tsx output an empty object.
Is it an issue with typescript and the way I write my interface?
the helpers property in InputTextProps and the onChange property in your HelpersProps are optional. either make them required by removing the question mark or assign to them a default values when destructuing.
const { value, helpers = {} } = props;
const { onChange = () => {} } = helpers;
return (
<input type={'text'} value={value} onChange={onChange} onBlur={helpers.onBlur} />
)
inside your interface:
interface CellEditableProps {
value: string
onChange?: () => void
}
you placed a ? after onChange, that tells the compiler that it can be not passed and hence you get "Object is possibly 'undefined'
To remedy this solution either you can use onChange with ! like onChange!. This thells compiler that you are sure that onChange will not be null. But this is a bad approach.
What you should do is check if it is not null or undefined and then proceed:
if(onChange) {
...do your stuff here
}
your interface declaration clearly states that these can indeed be undefined (? marks these properties as optional). You'll need to check for their existance or fill them.
const value = props.value || '';
const helpers = {
onChange: () => {},
onBlur: () => {},
...(props.helpers || {}),
};
return (
<input type={'text'} value={value} onChange={helpers.onChange} onBlur={helpers.onBlur} />
)
or similar.

Unable to update parent prop in react

I can insert the input value say "1,2,3" and when backspace it removes all but in the console "1" is still shown i.e., House{props{house{property{rent:1}}}}
I am providing the code here which has 3 files.
(1) house.js
import ValInput from "main/components/val-input";
class House extends Component {
state = {
rent:"",
};
componentDidMount() {
if (this.props.house.rent) {
const { rent} = this.props.house;
this.setState({ rent });
}
}
onChange = (e) => {
const rent = parseInt(e.target.value.replace(string);
this.setState({
rent,
});
};
render(){
const {house} = this.props;
const {rent} = this.state;
...
<ValInput
type="text"
value={ rent }
onChange={e => {
this.onChange(e);
}}
/>
}
(2) val-input\index.js
import React from "react";
import Input from "main/components/input";
const ValInput = props => (
<Input
{...props}
type={props.type ? props.type : "text"}
/>
);
export default valInput;
(3) components/input/index.js
import React from "react";
const noOp = () => {};
const Input = ({
onBlur = xP,
...otherProps
}) => (
<input
onBlur={e => {
e.target.placeholder = placeholder;
onBlur(e);
}}
{...otherProps}
/>
);
export default Input;
The expected result should be, after emptying the value say with backspace, and visit the page next time, the input field should be empty and should not show old value.
Check this CodeSandbox out I replicated your code and if I understood the problem right then fixed it
https://reactjs.org/docs/cross-origin-errors.html
For updating #NaderZouaoui, has given me an example how to do Call back :
1. Child file :
onChange={e => {
this.onChange(e);
}}
onChange = e => {
this.setState({
rent
});
this.props.callback(rent);
};
2. Parent file :
state = {
rent: ""
};
handleChangeRent = newRent => {
this.setState({ rent: newRent });
};
render(){
return(
<House house={{ rent }} callback={this.handleChangeRent} />
);
}

React controlled input cursor jumps

I am using React and have formatted a controlled input field, which works fine when I write some numbers and click outside the input field. However, when I want to edit the input, the cursor jumps to the front of the value in the input field. This only occur in IE, and not in e.g. Chrome. I've seen that for some programmers the cursor jumps to the back of the value. So I think the reason that my cursor is jumping to the front is because the value is aligned to the right instead of to the left in the input field. Here is a senario:
My first input is 1000
Then I want to edit it to 10003, but the result is
31000
Is there a way to controll that the cursor should not jump?
Here's a drop-in replacement for the <input/> tag. It's a simple functional component that uses hooks to preserve and restore the cursor position:
import React, { useEffect, useRef, useState } from 'react';
const ControlledInput = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState(null);
const ref = useRef(null);
useEffect(() => {
const input = ref.current;
if (input) input.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;
Taking a guess by your question, your code most likely looks similar to this:
<input
autoFocus="autofocus"
type="text"
value={this.state.value}
onChange={(e) => this.setState({value: e.target.value})}
/>
This may vary in behaviour if your event is handled with onBlur but essentially its the same issue. The behaviour here, which many have stated as a React "bug", is actually expected behaviour.
Your input control's value is not an initial value of the control when its loaded, but rather an underlying value bound to this.state. And when the state changes the control is re-rendered by React.
Essentially this means that the control is recreated by React and populated by the state's value. The problem is that it has no way of knowing what the cursor position was before it was recreated.
One way of solving this which I found to work is remembering the cursor position before it was re-rendered as follows:
<input
autoFocus="autofocus"
type="text"
value={ this.state.value }
onChange={(e) => {
this.cursor = e.target.selectionStart;
this.setState({value: e.target.value});
}
}
onFocus={(e) => {
e.target.selectionStart = this.cursor;
}
}
/>
This is my solution:
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
//get reference for input
this.nameRef = React.createRef();
//Setup cursor position for input
this.cursor;
}
componentDidUpdate() {
this._setCursorPositions();
}
_setCursorPositions = () => {
//reset the cursor position for input
this.nameRef.current.selectionStart = this.cursor;
this.nameRef.current.selectionEnd = this.cursor;
};
handleInputChange = (key, val) => {
this.setState({
[key]: val
});
};
render() {
return (
<div className="content">
<div className="form-group col-md-3">
<label htmlFor="name">Name</label>
<input
ref={this.nameRef}
type="text"
autoComplete="off"
className="form-control"
id="name"
value={this.state.name}
onChange={event => {
this.cursor = event.target.selectionStart;
this.handleInputChange("name", event.currentTarget.value);
}}
/>
</div>
</div>
);
}
}
export default App;
This is an easy solution. Worked for me.
<Input
ref={input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)}
value={this.state.inputtext}
onChange={(e)=>{
this.cursor = e.target.selectionStart;
this.setState({inputtext: e.target.value})
/>
Explanation:
What we are doing here is we save the cursor position in onChange(), now when the tag re-renders due to a change in the state value, the ref code is executed, and inside the ref code we restore out cursor position.
If you're using textarea, then here's the hook based on Daniel Loiterton's code using TypeScript:
interface IControlledTextArea {
value: string
onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
[x: string]: any
}
const ControlledTextArea = ({ value, onChange, ...rest }: IControlledTextArea) => {
const [cursor, setCursor] = useState(0)
const ref = useRef(null)
useEffect(() => {
const input: any = ref.current
if (input) {
input.setSelectionRange(cursor, cursor)
}
}, [ref, cursor, value])
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setCursor(e.target.selectionStart)
onChange && onChange(e)
}
return <textarea ref={ref} value={value} onChange={handleChange} {...rest} />
}
As (I think) others have mentioned, React will keep track of this if you make your changes synchronously. Unfortunately, that's not always feasible. The other solutions suggest tracking the cursor position independently, but this will not work for input types like 'email' which will not allow you to use cursor properties/methods like selectionStart, setSelectionRange or whatever.
Instead, I did something like this:
const Input = (props) => {
const { onChange: _onChange, value } = props;
const [localValue, setLocalValue] = useState(value);
const onChange = useCallback(
e => {
setLocalValue(e.target.value);
_onChange(e.target.value);
},
[_onChange]
);
useEffect(() => {
setLocalValue(value);
}, [value]);
// Use JSX here if you prefer
return react.createElement('input', {
...props,
value: localValue,
onChange
});
};
This allows you to delegate the cursor positioning back to React, but make your async changes.
My cursor jumped always to the end of the line. This solution seems to fix the problem (from github):
import * as React from "react";
import * as ReactDOM from "react-dom";
class App extends React.Component<{}, { text: string }> {
private textarea: React.RefObject<HTMLTextAreaElement>;
constructor(props) {
super(props);
this.state = { text: "" };
this.textarea = React.createRef();
}
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const cursor = e.target.selectionStart;
this.setState({ text: e.target.value }, () => {
if (this.textarea.current != null)
this.textarea.current.selectionEnd = cursor;
});
}
render() {
return (
<textarea
ref={this.textarea}
value={this.state.text}
onChange={this.handleChange.bind(this)}
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Here is my solution
const Input = () => {
const [val, setVal] = useState('');
const inputEl = useRef(null);
const handleInputChange = e => {
const { value, selectionEnd } = e.target;
const rightCharsCount = value.length - selectionEnd;
const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
const newPosition = formattedValue.length - rightCharsCount;
setVal(formattedValue);
setTimeout(() => {
inputEl.current.setSelectionRange(newPosition, newPosition);
}, 0);
};
return <input ref={inputEl} value={val} onChange={handleInputChange} />;
};
// Here is a custom hook to overcome this problem:
import { useRef, useCallback, useLayoutEffect } from 'react'
/**
* This hook overcomes this issue {#link https://github.com/reduxjs/react-redux/issues/525}
* This is not an ideal solution. We need to figure out why the places where this hook is being used
* the controlled InputText fields are losing their cursor position when being remounted to the DOM
* #param {Function} callback - the onChangeCallback for the inputRef
* #returns {Function} - the newCallback that fixes the cursor position from being reset
*/
const useControlledInputOnChangeCursorFix = callback => {
const inputCursor = useRef(0)
const inputRef = useRef(null)
const newCallback = useCallback(
e => {
inputCursor.current = e.target.selectionStart
if (e.target.type === 'text') {
inputRef.current = e.target
}
callback(e)
},
[callback],
)
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
}
})
return newCallback
}
export default useControlledInputOnChangeCursorFix
// Usage:
import React, { useReducer, useCallback } from 'react'
import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'
// Mimics this.setState for a class Component
const setStateReducer = (state, action) => ({ ...state, ...action })
const initialState = { street: '', address: '' }
const SomeComponent = props => {
const [state, setState] = useReducer(setStateReducer, initialState)
const handleOnChange = useControlledInputOnChangeCursorFix(
useCallback(({ target: { name, value } }) => {
setState({ [name]: value })
}, []),
)
const { street, address } = state
return (
<form>
<input name='street' value={street} onChange={handleOnChange} />
<input name='address' value={address} onChange={handleOnChange} />
</form>
)
}
For anybody having this issue in react-native-web here is solution written in TypeScript
const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => {
if(typeof refInput === "function") {
console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
return <TextInput key={"invalid-ref"} {...props} />;
}
if(!("HTMLInputElement" in self)) {
return <TextInput key={"no-web"} {...props} />;
}
const { value, onChange, ...restProps } = props;
const defaultRefObject = useRef<TextInput>(null);
const refObject: RefObject<TextInput> = refInput || defaultRefObject;
const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);
useEffect(() => {
if(refObject.current instanceof HTMLInputElement) {
refObject.current.setSelectionRange(selection.start, selection.end);
}
}, [ refObject, selection, value ]);
return (
<TextInput
ref={refObject}
value={value}
onChange={event => {
const eventTarget = event.target as any;
if(eventTarget instanceof HTMLInputElement) {
setSelection({
start: eventTarget.selectionStart,
end: eventTarget.selectionEnd
});
}
if(onChange) {
onChange(event);
}
}}
{...restProps}
/>
)
});
The simplest and safest way of doing this is probably to save the cursor position before React renders the input and then setting it again after React finishes rendering.
import React, {ReactElement, useEffect, useRef} from "react";
/**
* Text input that preserves cursor position during rendering.
*
* This will not preserve a selection.
*/
function TextInputWithStableCursor(
props: React.InputHTMLAttributes<HTMLInputElement> & {type?: "text"}
): ReactElement {
const inputRef = useRef<HTMLInputElement>(null);
// Save the cursor position before rendering
const cursorPosition = inputRef.current?.selectionStart;
// Set it to the same value after rendering
useEffect(function () {
if (
typeof cursorPosition === "number" &&
document.activeElement === inputRef.current
) {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
}
});
return <input ref={inputRef} {...props} />;
}
If you faced an issue with the cursor jumping to the end after updating the input state and updating the cursor using refs -> I found a workaround for it by setting the cursor in Promise.resolve's microtask.
<input
value={value}
onChange={handleValueUpdate}
ref={inputRef}
/>
const handleValueUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
// ...
// some value handling logic
setValue(newValue)
const cursorPosition = getCursorPositionLogic();
/**
* HACK: set the cursor on the next tick to make sure that the value is updated
* useTimeout with 0ms provides issues when two keys are pressed same time
*/
Promise.resolve().then(() => {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
});
}
I know the OP is 5 years old but some are still facing the same kind of issue and this page has an high visibility on Google search.
Try by replacing :
<input value={...}
with
<input defaultValue={...}
This will solve most of the cases i've seen around there.
I tried all of the above solutions and none of them worked for me. Instead I updated both the e.currentTarget.selectionStart & e.currentTarget.selectionEnd on the onKeyUp React synthetic event type. For example:
const [cursorState, updateCursorState] = useState({});
const [formState, updateFormState] = useState({ "email": "" });
const handleOnChange = (e) => {
// Update your state & cursor state in your onChange handler
updateCursorState(e.target.selectionStart);
updateFormState(e.target.value);
}
<input
name="email"
value={formState.email}
onChange={(e) => handleOnChange(e)}
onKeyUp={(e) => {
// You only need to update your select position in the onKeyUp handler:
e.currentTarget.selectionStart = cursorState.cursorPosition;
e.currentTarget.selectionEnd = cursorState.cursorPosition;
}}
/>
Also, be aware that selectionStart & selectionEnd getters are not available on input fields of type email.

React Redux - Uncaught TypeError: Cannot read property 'setState' of undefined

New to this.
I have looked for answers here and here.
am using Redux as well. As per good practice I have a container "AddressContainer" and its component "Address".
The AddressContainer is as follows -
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Field, change } from 'redux-form'
import { Col, Panel, Row } from 'react-bootstrap'
import Select from 'react-select'
import Address from './address'
import { ensureStateData, getSuburbs } from './actions'
import { CLIENT_FORM_NAME } from '../clients/client/client'
export class AddressContainer extends Component {
static contextTypes = {
_reduxForm: PropTypes.object.isRequired,
}
constructor(props, context) {
super(props, context)
this.state = {
selectedSuburb: null,
}
}
componentDidMount() {
this.props.ensureStateData()
}
// Manage asyncSelect for new data request - for suburbs.
handleSuburbSearch = (query) => {
const { addressData } = this.props
const companyStateId = addressData.companyStateId
if (!query || query.trim().length < 2) {
return Promise.resolve({ options: [] })
}
const queryString = {
query: query,
companyStateId: companyStateId,
}
return getSuburbs(queryString)
.then(data => {
return { options: data }
})
}
render() {
const {
initialValues,
addressData,
updatePostcodeValue,
} = this.props
//const { value } = this.state
const sectionPrefix = this.context._reduxForm.sectionPrefix
if (addressData.isLoading || !addressData.states.length) {
return (
<p>Loading</p>
)
}
if (addressData.error) {
return (
<p>Error loading data</p>
)
}
const companyStateId = addressData.companyStateId
// initialValues = {
// ...initialValues.Address=null,
// state: addressData.states.find(option => option.stateId === companyStateId),
// }
return (
<Address
initialValues={initialValues}
addressData={addressData}
handleSuburbSearch={this.handleSuburbSearch}
/>
)
}
}
const mapStateToProps = (state) => ({
initialValues: state.address,
companyStateId: state.companyStateId,
addressData: state.addressData,
})
const mapDispatchToProps = (dispatch) => ({
ensureStateData: () => dispatch(ensureStateData()),
getSuburbs: (values) => dispatch(getSuburbs(values)),
updatePostcodeValue: (postcode, sectionPrefix) => dispatch(change(CLIENT_FORM_NAME, `${sectionPrefix ? (sectionPrefix + '.') : ''}postcode`, postcode))
})
export default connect(mapStateToProps, mapDispatchToProps)(AddressContainer)
The Address component is as follows -
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Field, reduxForm, change } from 'redux-form'
import { Col, Panel, Row } from 'react-bootstrap'
import Select from 'react-select'
import FormField from '../formComponents/formField'
import TextField from '../formComponents/textField'
import StaticText from '../formComponents/staticText'
export const ADDRESS_FORM_NAME = "Address"
export const Address = (props) => {
const { addressData, handleSuburbSearch } = props
const { reset } = props
return (
<Panel header={<h3>Client - Address Details</h3>}>
<Row>
<Field component={TextField}
name="address1"
id="address1"
type="text"
label="Address Line 1"
placeholder="Enter street 1st line..."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
<Field component={TextField}
name="address2"
id="address2"
type="text"
label="Address Line 2"
placeholder="Enter street 2nd line..."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
<Row>
<Field
component={props => {
const { input, id, placeholder, type } = props
const { fieldCols, labelCols, controlCols, label, inputClass } = props
// just the props we want the inner Select textbox to have
const { name, onChange } = input
const onStateChange = (state) => {
console.log('onStateChange', state)
onChange(state)
}
return (
<FormField
id={id}
label={label}
fieldCols={fieldCols}
labelCols={labelCols}
controlCols={controlCols}
inputClass={inputClass}
>
<Select
name={name}
onChange={onStateChange}
placeholder="Select state"
valueKey="id"
options={addressData.states}
labelKey="stateLabel"
optionRenderer={option => `${option.stateShortName} (${option.stateName})`}
value={input.value}
selectValue={Array.isArray(input.value) ? input.value : undefined}
/>
</FormField>
)
}}
name="state"
id="state"
label="State."
fieldCols={6}
labelCols={3}
controlCols={6}
/>
</Row>
<Row>
<Field
component={props => {
const { input, id, placeholder, type } = props
const { fieldCols, labelCols, controlCols, label, inputClass } = props
const { name, value, onChange, onBlur, onFocus } = input
const inputProps = {
name,
value,
onChange,
onBlur,
onFocus,
}
const onSuburbChange = (value) => {
console.log('onSuburbChange: ', value)
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
return (
<FormField
id={id}
label={label}
fieldCols={fieldCols}
labelCols={labelCols}
controlCols={controlCols}
inputClass={inputClass}
>
<Select.Async
{...inputProps}
onChange={onSuburbChange}
valueKey="id"
labelKey="suburbName"
loadOptions={handleSuburbSearch}
backspaceRemoves={true}
/>
</FormField>
)
}}
name="suburb"
id="AddressLocation"
label="Suburb."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
<Row>
<Field component={StaticText}
name="postcode"
id="postcode"
label="Postcode."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
</Panel>
)
}
Address.propTypes = {
handleSuburbSearch: PropTypes.func.isRequired,
}
const AddressForm = reduxForm({
form: ADDRESS_FORM_NAME,
})(Address)
export default AddressForm
The problem is with the following function in the address component below and with setState which it says is undefined -
const onSuburbChange = (value) => {
console.log('onSuburbChange: ', value)
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
You will note there is a console.log for "value". This produces the result:
onSuburbChange: Object {id: 6810, suburbName: "Eaglehawk", postcode: "3556", state: "VIC"}
I am using React-Select as the async dropdown. This all works. If I select an option I get dropdown options but select one and it gives me the error.
I am using react state here for selectSuburb options as I dont need to update redux with this - just react state.
It seems all right but I still get the error. Why am I getting this error and how do I fix it?
This specific error is caused by the fact that the <Address /> component is a stateless functional component and cannot have a this.state object or a setState function in it. However, more generally it looks like you are expecting state and functions from the <AddressContainer /> component to be available to the child <Address /> component, but that cannot happen. In this case, you are wanting to modify the state of the parent by calling setState on the child.
A child React component (in this case <Address />) will only have state/functions/properties from its parent that are explicitly passed down as props to that component. If you want to change the local state of a component it must happen on the component with the local state. If you want to have a child component trigger some type of function call on the parent, then that function must be passed down as a prop to the child and the child can call it.
If I understand your code correctly, you want 3 things to happen when the Suburbs FormField is changed, in this order.
The selectedSuburb state on <AddressContainer /> is updated.
The onChange of the Redux-Form <Field /> in <Address /> is triggered.
The updatePostCode action is fired off.
If that is correct, then you will need to move your onSuburbChange to the <AddressContainer /> and pass it to <Address /> as a prop. However, you cannot call the onChange of the Redux-Form <Field /> inside <AddressContainer />. Therefore, you can make that function expect to receive a callback function that will be fired off after the state updates. Then you can define the callback in the child component but the state-changing function in the parent. As long as you pass down needed props on the parent, such as updatePostCode and sectionPrefix, you'll be golden. Here's what it would look like:
AddressContainer
export class AddressContainer extends Component {
/* Everything else in this component */
onSuburbChange = (value, callback) => {
this.setState({ selectedSuburb: value }, callback);
}
render() {
/* Other stuff inside render */
return (
<Address
initialValues={initialValues}
addressData={addressData}
handleSuburbSearch={this.handleSuburbSearch}
onSuburbChange={this.onSuburbChange}
updatePostcodeValue={this.props.updatePostcodeValue}
sectionPrefix={sectionPrefix}
/>
);
}
}
Address
export const Address = (addressProps) => {
return (
/* All other JSX */
<Field
component={props => {
const { input } = props;
const handleSuburbChange = (value) => {
addressProps.onSuburbChange(value, () => {
input.onChange(value);
addressProps.updatePostcodeValue(value ? value.postcode : null, addressProps.sectionPrefix)
});
}
return (
<Select.Async
onChange={handleSuburbChange}
/>
)
}}
);
}
As you can see, there is going to be a naming conflict between the different props variables in the <Address /> component, so I call the main props addressProps to avoid this.

Resources