I am trying to create a comment section for a site im working on. Once the comment form (inside AfterCommentButtonClick) is submitted, the state formSubmitted changes from false to true which triggers a conditional statement inside the render method. This calls a child component which receives the users comment and does some styling with it. The issue im having is that, i want my app to allow more than one comment. Is there a way to save the previously rendered comment, and then create a new instance of <UserComment> as currently, following form submittal the old one is simply overwritten. I also need to reset the textInput state following the submittal of the form, to reset the form for the next comment. However, again im not sure how to do this without entering setState inside render, which will cause an infinite loop
import React from 'react'
import UserComment from './UserComment'
class CommentSection extends React.Component {
constructor(props){
super(props)
this.state = {selectedFile: this.props.selectedFile, textinput : '', formSubmitted:false}
}
onFormSubmit (event){
event.preventDefault()
this.setState({formSubmitted:true})
}
render(){
//conditional render depending on if comment button has been clicked or not. props.selectedFile only
//passed here from parent if user clicks comment button
const file = this.props.selectedFile
let messageToUser
if (file !=null){
messageToUser = <AfterCommentButtonClick
selectedFile = {file}
onTextChange = {(e)=> this.setState({textinput: e.target.value})}
onFormSubmit = {(e)=>this.onFormSubmit(e)}
/>
}else {
messageToUser = <BeforeCommentButtonClick />
}
return (
<div>
<div> {messageToUser}</div>
<div className="ui container comments">
{this.state.formSubmitted &&
<UserComment commentText = {this.state.textinput}/>
/*conditionally send text typed into comment bar if user submits form*/
}
</div>
</div>
)
}
}
Create a functional component to render all of your submitted comments. To do this, you would keep an array of 'submitted comments' in state and, on submission of a new comment, just add the new user comment to the array of submitted comments. Pass that submitted comments array from state to your new functional component. Use the array.map() function to render the array of submitted components by rendering a <UserComment/> for each item in the array.
So, on submission of a User Comment, it would just add to the submitted comments component, the UI re-renders and updates with the new UserComment in your submitted comments. This should be entirely separate logic.
i.e. Render method of your <CommentsSection/> component would look something like this:
render() {
return (<div>
{this.props.submittedComments.map((comment) => (
<UserComment author={comment.author} content={comment.content}></UserComment>))}
</div>);
}
You can add another state field to store the comments in an array. So, when you get a new comment, you do this:
this.setState({
comments: [...this.state.comments, newComment]
});
And in your render method, you map over that array and display a single comment component for every comment in this.state.comments
this.state.comments.map(comment => <UserComment commentText = {comment}}/>);
Related
Background
So I have a simple example, a Form component (Parent) and multiple Input components (Children). Each individual Input component will have an useState hook to initialize and manage its value's state.
Issue
As with most forms, I would like to submit all of the data to a backend for processing. However, the issue is that I cannot retrieve the state of value from each Child Input component.
// app.jsx
import Form from "./Form";
export default function App() {
return <Form />;
}
// Form.jsx
import React from "react";
import Input from "./Input";
const handleSubmit = (e) => {
e.preventDefault();
console.log("Wait, how do I retreive values from Children Inputs?");
};
const Form = () => {
console.log("Form render");
return (
<form onSubmit={handleSubmit}>
Sample Form
<Input initial="username" name="user" />
<Input initial="email" name="email" />
<button type="submit">Submit</button>
</form>
);
};
export default Form;
// Input.jsx
import React from "react";
import useInputValue from "./useInputValue";
const Input = ({ name, initial }) => {
const inputState = useInputValue(initial);
console.log(`${name}'s value: ${inputState.value}`);
return <input {...inputState} />;
};
export default Input;
Plausible Solution
Of course, I can lift the Input states up to the Form component, perhaps in an obj name values. However, if I do that, every time I change the Inputs, the Form will re-render, along with all of the Inputs.
To me, that is an undesirable side-effect. As my Form component gets bigger, this will be more costly to re-render all inputs (inside the form) every time one input changes.
Because of that, I would like to stick with my decision of having each individual input manage its own state, that way if one input changes, not all other input will re-render along with the Parent component.
Question
If each of the Child components manages its own state, could the Parent component access the value of that state and do actions (like form submission)?
Update
Many answers and comments mentioned that this is premature optimization and the root of all known evil; which I agree with, but to clarify, I am asking this question with a simple example because I wanted to find a viable solution to my current and more complex project. My web app has a huge form (where all states are lifted to the form level), which is getting re-rendered at every change in its inputs. Which seemed unnecessary, since only one input is changed at a time.
Update #2
Here is a codesandbox example of the issue I am having. There are two forms, one with all states managed by individual Child input components, and the other has all states lifted up in the form level. Try entering some values and check the console for log messages. One can see that with all states lifted to the form level, every change will cause both inputs to be re-rendered.
I think yes, you can share state. Also there are 3 options:
I recommend you to use such library as Formik. It will help you in your case.
You can share state using useState() hook as props.
Use such tools as Redux Toolkit (if we are speaking about memoisation), useContext() and etc.
If the thing you want is getting final values from input, assign ref to each input and access using emailRef.current.value in the submit function.
import { useState, useRef, forwardRef } from 'React';
const Input = forwardRef((props, ref) => {
const [value, setValue] = useState('');
return <input ref={ref} value={value} onChange={(e) => {setValue(e.target.value)}} {...props} />;
});
const App = () => {
const emailRef = useRef(null);
const submit = () => {
const emailString = emailRef.current.value
};
return (
<>
<Input ref={emailRef} />
<button onClick={submit}>Submit</button>
</>
);
};
If the parent needs to know about the childs state, you can
move the state up. This is resolved by passing down a setState function that the child calls (the states data is available in the parent)
use a context https://reactjs.org/docs/context.html
use a state management library e.g. redux
In your simple case I'd go with 1)
I have a project where I'm displaying cards that contain attributes of a person in a textfield, and the user can edit the textfield to directly change that person's attribute values. However every time they're editing the textfield it causes a rerender of all cards which slows down the app. Here is an example:
export default Parent() {
const [personList, setPersonList] = useState(/* list of person objects*/);
const modifyPerson(index, property, value) {
const newPersonList = _.cloneDeep(personList);
newPersonList[index][property] = value;
setPersonList(newPersonList);
}
const children = personList.map((person, index) => {
<Person
modifyPerson={modifyPerson}
index=index
/*properties on the person */
/>
});
return <div> {children} </div>
}
export default Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
}
So essentially when the child's text field is updated, it triggers a state change in the parent that stores the new value, then refreshes what the Child looks like. There's no button that the user clicks to "save" the values-- as soon as they edit the textfield it's a permanent change. And also the parent needs to know the new values of every Person because there's some functions that require knowledge of the current state of the person list. The Person component contains images that slow down the rendering if done inefficiently.
Is there a better way to make this design more performant and reduce rerenders? I attempted to use useCallback to preserve the functions but I don't it works in this specific design because the property and values are different-- do I have to create a new "modifyPerson" for each exact attribute?
Use React.memo()
React.Memo will check the props passed to the component and only if the props changes , it will re-render that particular component.
So, if you have multiple Person component which are getting props which are explicit to those Person, and when you change a particular Person component which leads to Parent state getting updated, only that Person component which you had modified will re-render because its props will change(assuming that you pass the changed value to it).
The other components will have the same props and wont change.
export default React.memo(Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
})
As others have already said React.memo() is the way to go here, but this will still re-render because you recreate modifyPerson every time. You can use useCallback to always get the same identity of the function, but with your current implementation you would have to add personList as dependency so that doesn't work either.
There is a trick with setPersonList that it also accepts a function that takes the current state and returns the next state. That way modifyPerson doesn't depend on any outer scope (expect for setPersonList which is guaranteed to always have the same identity by react) and needs to be created only once.
const modifyPerson = useCallback((index, property, value) {
setPersonList(currentPersonList => {
const newPersonList = _.cloneDeep(currentPersonList);
newPersonList[index][property] = value;
return newPersonList;
})
}, []);
I am practising React props by building an input form using hooks. The idea is when I enter the inputs in NewList component and click the submit button, its values will render on the MyJobList component in a form of HTML elements. The issue is when I submitted the form, nothing is displaying on the web page. It could be that I am passing the data incorrectly.
Here are my codes. I have minimized it to highlight possible problems and included a link to full codes down below:
newlist.js
const NewList = () => {
const [inputState, setInputState] = useState({});
const onSubmitList = () => {
setInputState({
positionTitle: `${inputs.positionTitle}`,
companyName: `${inputs.companyName}`,
jobLink: `${inputs.jobLink}`
});
};
const { inputs, handleInputChange, handleSubmit } = CustomForm(onSubmitList);
myjoblist.js
const MyJobList = inputs => (
<div>
<h3>{inputs.positionTitle}</h3>
<p>{inputs.companyName}</p>
<p>{inputs.jobLink}</p>
</div>
);
navbar.js
const Navbar = inputState => (
<Router>
<Switch>
<Route path="/my-list">
<MyJobList inputs={inputState} />
</Route>
</Switch>
</Router>
);
Any help and guidance are much appreciated.
Here's a link to complete code: Code Sandbox
So, you need the persisting state to live in a shared parent component, in the existing components, your choices would be either App.js or navbar.js (side note, you should PascalCase your component file names, so NavBar.js). Ideally, you should make a shared container that will hold the state. When you navigate away from the newlist component to view myjoblist, the newlist component unmounts and you lose the state. With a shared parent component, the parent won't unmount when it renders its children (newlist & myjoblist).
Another problem is that you you are passing a callback to your custom hook, but the callback doesn't have any arguments. In your handle click, you need to pass it the inputs. You also cannot set your state to an empty string before you pass the inputs to your callback, do it after.
const handleSubmit = event => {
e.preventDefault()
callback(inputs)
setInputs({}) // set it back to the default state, not a string
}
Lastly, the inputState in navbar is an undefined variable. You are rendering Navbar inside your app and not passing it any props. The first argument to a functional component in react is props. So, even if you were passing it state, you'd need to extract via props.inputState or with destructuring.
Here's a working example that could still use some clean up:
https://codesandbox.io/s/inputform-with-hooks-wwx2i
In Redux-Form, it seems like you pass onSubmit as a prop to your form component and then the form component will have a onSubmit={handleSubmit} property on its form tag. This makes sense as Redux-Form will then gather up all of your form data and pass it in as one single object parameter to your onSubmit function. But what happens when you have multiple form component children of the same parent component? You can only have one onSubmit function to pass to both your form children even though they need to do different things with their submitted information. Is the only solution to have multiple parents for multiple forms just so you can pass in a different onSubmit? To me this seems not very DRY.
You can use the composition concept of reactjs here.
What you need to is :-
Keep a common form, lets call it CommonForm.js. This form is going to attached to your redux form. So all your form common functions(maybe opening form,closing form) would be implemented here.
P.S : No Field components should be present here.
Now, say you have 5 different forms which you want to render. Lets take the example of first form. I will name the file FirstForm.js.
Here in FirstForm.js you render your fields and pass your rendered fields as props to your CommonForm(I am using composition here).
Check the following code:-
Form 1
class Form1 extends Component {
render(){
const renderedFields = this.renderFormFields()
return(
<CommonForm {...this.props} renderedFields = {renderedFields } />
)
}
}
CommonForm
class CommonForm extends Component {
componentDidMount(){
const { data,initialize} = this.props
if(data){
initialize(data)
}
}
render(){
const { renderedFormFieldss } = this.props
return(
<div className='form-container'>
<form onSubmit={this.instrumentSplitCreateOrUpdate()}>
{renderedFormFields}
</form>
</div>
)
}
}
CommonForm = reduxForm({fields: ["text"], validate : validateInputField})(CommonForm)
export default CommonForm
This way you are using same base component to connect to redux form.
DYNAMIC FORMS:-
Ideally above should solve your query but there is something more interesting. If you went through the code proper in the last line of common form you can see I am passing following as parameter:-
{fields: ["text"]}
So, in this you can create forms with variable names.Say you have same forms used by multiple components and you want to render same form but with different names. For this you can try following:-
<CommonForm {...this.props} renderedFields = {renderedFields } />
While calling CommonForm from say Form1 pass a prop called form with the variable name with which you want to create form. You can something like following:-
<CommonForm form='//some key' {...this.props} renderedFields = {renderedFields } />
I have dynamic JSON data and have a custom method to go through the JSON to dynamically render a form on the page. The reason for the JSON schema is to build various forms that is not predefined.
I have hooked up Redux so that the schema and the formValues below gets assigned as the props of this class. So, the form is rendering correctly with the correct label, correct input field types etc. When an onChange event happens on the fields, the app state(under formData) is being updated correctly. But I am noticing that when the formData changes in the app state, the entire form gets re-rendered, instead of just the "specific fields". Is this because I am storing the form values as an object under formData like this? How do I avoid this issue?
formData = {
userName: 'username',
firstName: 'firstName
}
Example schema
const form = {
"fields":[
{
"label":"Username",
"field_type":"text",
"name":"username"
},
{
"label":"First Name",
"field_type":"text",
"name":"firstName"
}
]
}
Redux state
const reducer = combineReducers({
formSchema: FormSchema,
formData: Form
});
//render method
render() {
const { fields } = this.props.form,
forms = fields.map(({label, name, field_type, required }) => {
const value = value; //assume this finds the correct value from the "formData" state.
return (
<div key={name}>
<label>{label}</label>
<input type={field_type}
onChange={this.onChange}
value={value}
name={name} />
</div>
);
})
}
//onchange method (for controlled form inputs, updates the fields in the formData app state)
onChange(event) {
this.props.dispatch(updateFormData({
field: event.target.name,
value: event.target.value
}));
}
From your example I'm not sure, but if you're rendering the whole thing in a single render() method, yes, the component will be rendered again. And that is the problem, THE component. If you are trying to have multiple components, then they should be split up as much as possible. Otherwise if the state changes, it triggers a re-render of the only component there is.
Try breaking it as much as you can.
Hints: (dont know if they apply but maybe)
use ref={}s
implement shouldComponentUpdate()
EDIT: Just thought about this, but are you storing the fields' values in your state? This doesnt feel correct. Be sure to read carefully the React guide about controlled components. (Eg try to render using plain <span>s instead of inputs, and listen to onKeyPress. Would it still work? If not you might be misusing the value attribute)