Reputation: 25
I'm a total beginner in React, and am trying to build my first form.
I fetch the questions asked in said form using an API, and once the form submitted, I would like to create an answer object through this same API.
The problem is that everything seems to work just fine (questions render ok, answer object is updated in state), but everytime the value of a field change, I have to re-select the field to keep typing. Basically, it's like I'm auto-clicked away from the field when the value changes.
Here's what happens :
And here's a snippet of (probably) the offending code :
class Form extends React.Component {
constructor(props) {
super(props);
{/* Questions are filled on page load, answers is what I'm working with */}
this.state = { questions: [], answers: [] };
this.handleChange = this.handleChange.bind(this);
}
// This is where the magic is supposed to happen
handleChange(event) {
let key = event.target.id,
value = event.target.value;
{/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */}
this.setState(prevState => {
let copy = Object.assign({}, prevState.answers);
copy[key] = value;
return { answers: copy };
})
}
render() {
const { questions } = this.state;
const TextareaInput = (fieldId) => (
<div>
<textarea name={ fieldId.fieldId } value={this.state.answers[fieldId.fieldId]} id={ fieldId.fieldId } onChange={this.handleChange} ></textarea>
</div>
);
const TextInput = (fieldId) =>(
<div>
<input type='text' name={ fieldId.fieldId } value={this.state.answers[fieldId.fieldId]} id={ fieldId.fieldId } onChange={this.handleChange} />
</div>
);
const allQuestions = (
questions.map((q, key) =>
<div key={q.id} className='question'>
<label htmlFor={ q.field_id } className={ q.required ? 'required' : '' } dangerouslySetInnerHTML={{__html: q.question}}></label>
{q.field_type == 'text' ? <TextInput fieldId={q.field_id}/> : <TextareaInput fieldId={q.field_id}/>}
</div>
)
)
return (
<form>
{ allQuestions }
</form>
)
}
}
export default Form;
I think the problem comes from my handleChange function, but I'm not sure what could be causing this. I tried adding some stuffs and moving things around a little without any luck...
Upvotes: 0
Views: 505
Reputation: 11037
You need to call the TextInput
and TextareaInput
like functions instead of using them like separate components since you defined them within the component.
{q.field_type == 'text'
? TextInput(q.field_id)
: TextareaInput(q.field_id)}
React was unable to keep the reference to them straight and seemingly considered them different elements every render.
Also, as I'm sure you are already aware, you should be careful using dangerouslySetInnerHTML
as the name implies, it can be dangerous.
class Form extends React.Component {
constructor(props) {
super(props);
{
/* Questions are filled on page load, answers is what I'm working with */
}
this.state = {
questions: [
{
id: 2,
field_id: 2,
question: 'How are you today?',
field_type: 'text',
},
{
id: 3,
field_id: 3,
question: 'What\'s the answer to life, the universe, and everything??',
field_type: 'textarea',
},
],
answers: [],
};
this.handleChange = this.handleChange.bind(this);
}
// This is where the magic is supposed to happen
handleChange(event) {
let key = event.target.id,
value = event.target.value;
{
/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */
}
this.setState((prevState) => {
let copy = Object.assign({}, prevState.answers);
copy[key] = value;
return { answers: copy };
},()=>{console.log(this.state)});
}
render() {
const { questions } = this.state;
const TextareaInput = (fieldId) => (
<div>
<textarea
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
></textarea>
</div>
);
const TextInput = (fieldId) => (
<div>
<input
type="text"
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
/>
</div>
);
const allQuestions = questions.map((q, key) => (
<div key={q.id} className="question">
<label
htmlFor={q.field_id}
className={q.required ? 'required' : ''}
// As I'm sure you are already aware, this is likely a terrible idea.
dangerouslySetInnerHTML={{ __html: q.question }}
></label>
{q.field_type == 'text'
? TextInput(q.field_id)
: TextareaInput(q.field_id)}
</div>
));
return <form>{allQuestions}</form>;
}
}
ReactDOM.render(<Form/>, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
In order to avoid using dangerouslySetInnerHTML
for the questions, I'd suggest using some sort of markdown renderer. It should be a good enough for most use cases in asking questions.
https://www.npmjs.com/package/react-markdown
https://www.markdownguide.org/
class Form extends React.Component {
constructor(props) {
super(props);
{
/* Questions are filled on page load, answers is what I'm working with */
}
this.state = {
questions: [
{
id: 2,
field_id: 2,
question: 'How are *you* today?',
field_type: 'text',
},
{
id: 3,
field_id: 3,
question: 'What\'s the **answer** to life, the universe, and everything??',
field_type: 'textarea',
},
{id: 4,
field_id: 4,
field_type: 'text',
question:`# This is the big question
#### *ARE YOU READY?*
1. Is this the real life?
1. Or is this just fantasy?
`
}
],
answers: [],
};
this.handleChange = this.handleChange.bind(this);
}
// This is where the magic is supposed to happen
handleChange(event) {
let key = event.target.id,
value = event.target.value;
{
/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */
}
this.setState((prevState) => {
let copy = Object.assign({}, prevState.answers);
copy[key] = value;
return { answers: copy };
},()=>{console.log(this.state)});
}
render() {
const { questions } = this.state;
const TextareaInput = (fieldId) => (
<div>
<textarea
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
></textarea>
</div>
);
const TextInput = (fieldId) => (
<div>
<input
type="text"
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
/>
</div>
);
const allQuestions = questions.map((q, key) => (
<div key={q.id} className="question">
<label
htmlFor={q.field_id}
className={q.required ? 'required' : ''}
><ReactMarkdown source={q.question}/></label>
{q.field_type == 'text'
? TextInput(q.field_id)
: TextareaInput(q.field_id)}
</div>
));
return <form>{allQuestions}</form>;
}
}
ReactDOM.render(<Form/>, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-markdown/4.3.1/react-markdown.js" integrity="sha256-4jDgUokdWbazrdnMjWm+TftvBFnOwSNIpvKhgYsInfw=" crossorigin="anonymous"></script>
<div id="root" />
If you need full capabilities of rendering:
https://pragmaticwebsecurity.com/files/cheatsheets/reactxss.pdf, gives an example to use DOMPurify to sanitize the input coming in to prevent cross site scripting and other dangerous behavior rendering outside html can give.
So, for that, you'd purify the string and then pass it into dangerouslySetInnerHTML
once it's purified.
Upvotes: 3
Reputation: 2211
I think the questions
get lost from your state due to the setState
call. AFAIK if setState
is given an updater function, it must return the whole state. Shallow merging is only supported if an object is given.
this.setState(prevState => ({
...prevState,
answers: {
...prevState.answers,
[key]: value;
}
}))
(I hope object spread is supported in your environment, If not, do yourself a favor ;-))
Upvotes: 0