Extracting and reuse React components - javascript

In a React app I have a couple of working components like this one:
<div className="form-group col-md-6">
<label className='inp-lbl'>{utl.nameWord()}</label>
<div className='inp-name'>
<input
type="text"
ref="name"
defaultValue={this.name}
onChange={e => {
this.setState((state) => {
return {name: e.target.value};
});
}}
/>
</div>
</div>
Instead of repeating similar code for each or them, I want to extract a generic component that I will be able to reuse; but it does not work as I expect. Here is the new component I made:
class InputField extends React.Component {
constructor(props) {
super(props);
this.state = {
labelStr: props.lbStr,
nameStr: props.nmStr,
defltVal: props.dfVl,
onChgFnc: props.onChFn
};
// this.onChgFnc = props.onChFn
}
render() {
return (
<React.Fragment>
<div className="form-group col-md-6">
<label className='inp-lbl'>{this.state.labelStr}</label>
<div className='inp-name'>
<input
type="text"
ref={this.state.nameStr}
defaultValue={this.state.defltVal}
onChange={this.state.onChgFnc}
/>
</div>
</div>
</React.Fragment>
)
}
}
And this is how I call the new component:
<InputField lbStr={utl.nameWord()} nmStr='name'
dfVl={this.name}
onChFn={handleChange} />
The function handleChange is defined as:
function handleChange(event) {
this.setState((state) => {
return {name: event.target.value};
})
}
Though I thought this should work, it does not. So it would be great if somebody could spot the mistake I am making and let me know.

You could be suffering from an incorrectly bound this inside your handleChange function. Perhaps try just closing over the setState call instead of referencing it via this. For example
function handleChange(event) {
setState({ name: event.target.value })
}
(I've also changed the particular usage of the state-setter to accept a value as you weren't using the previous state value)
I would also go one further and change to a functional component and don't store your props in state as this will disconnected the component from the flow of prop values if/when they change.
function InputField({ name, label, onChange, defaultValue }){
return (
<div className="form-group col-md-6">
<label className='inp-lbl'>{label}</label>
<div className='inp-name'>
<input
type="text"
ref={name}
defaultValue={defaultValue}
onChange={onChange}
/>
</div>
</div>
)
}

I don’t understand why you need to assign the onChangehandler from props to state object in Child Component. You can directly bind the change handler from props’ change callback function.
<input type="text"
ref={this.state.nameStr}
defaultValue={this.state.defltVal}
onChange={this.props.onChFn} />

Storing props in state is an anti-pattern in React as it often leads to problem of multiple sources of truth(same info) in a component.
A better way to write your component would be to initialize the value of field from props and derive rest of the values directly from props.
You can directly bind the change handler from props’ change callback function.
class InputField extends React.Component {
constructor(props) {
super(props);
this.state = {
nameStr: props.nmStr,
};
}
render() {
return (
<React.Fragment>
<div className="form-group col-md-6">
<label className='inp-lbl'>{props.lbStr}</label>
<div className='inp-name'>
<input
type="text"
value={this.state.nameStr}
defaultValue={this.props.defltVal}
onChange={this.props.onChgFnc}
/>
</div>
</div>
</React.Fragment>
)
}
}

Related

Manage many inputs between sibling components in React - best approach?

I am new to this topic.
In the parent component App I have two siblings : SideMenu and Document
The idea is that the user inputs values (SideMenu) which will be renedered on the Document. There will be more than 20 inputs. Since this is the first time I do this sort of state management, what are the best or maybe easiest approaches for this attempt of project.
function App() {
const [fullName, setFullName] = useState("")
const [address, setAddress] = useState("")
return (
<div className='app'>
<SideMenu />
<Document />
</div>
)
}
export default App
const SideBar = () => {
return (
<div>
<div className='input-group'>
<label>Full Name:</label>
<input type='text' />
</div>
<div className='input-group'>
<label>Address:</label>
<input type='text' />
</div>
</div>
)
}
const Document = () => {
return (
<div>
<h1>{fullName}</h1>
<p>{address}</p>
</div>
)
}
You can create an object for your form and store the form inputs in this object. Shared state can be stored in the most closest and common component (in your situation this is your parent component) according to your child components. [1]
When you make an update from a child component other child component that is sharing state will be syncronized and your state will be updated. You shouldn't use redux like state management tools unless you are need to set a global state.
I have made a revision for your example, this scenario allows you to pass the state in the parent component to the child components and update the state in the parent component from the child components.
I used a common event handler in the parent component, this functions captures the html event and we parse this event and update the state via this function. [2][3]
import "./styles.css";
import { useState } from "react";
import SideBar from "./SideBar";
import Document from "./Document";
export default function App() {
const [values, setValues] = useState({
fullName: "",
address: "",
postalCode: ""
});
function handleChange(event) {
setValues({ ...values, [event.target.name]: event.target.value });
}
return (
<div className="app">
<SideBar values={values} setValues={handleChange} />
<Document values={values} setValues={handleChange} />
</div>
);
}
export default function Document({ values }) {
return (
<div>
<h1>Document</h1>
<p>Full Name: {values.fullName}</p>
<p>Address: {values.address}</p>
<p>Postal Code: {values.postalCode}</p>
</div>
);
}
export default function Sidebar({ setValues }) {
return (
<div>
<div className="input-group">
<label>Full Name:</label>
<input type="text" name="fullName" onChange={setValues} />
</div>
<div className="input-group">
<label>Address:</label>
<input type="text" name="address" onChange={setValues} />
</div>
<div className="input-group">
<label>Address:</label>
<input type="text" name="postalCode" onChange={setValues} />
</div>
</div>
);
}
Code Sandbox Link: https://codesandbox.io/s/stackoverflow-74961591-wpmcnd
[1]: Passing Props to a component: https://beta.reactjs.org/learn/passing-props-to-a-component
[2]: Updating Objects in State: https://beta.reactjs.org/learn/updating-objects-in-state
[3]: HTML Change Event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event
The default, go-to solution would be to use a container component that controls your form inputs state (this would be App in your case). Just pass the values and setters down one level by props and everything should be ok, simple and predictable.
If things start to get complicated then libraries such as Formik or react-hook-form help a lot. When it comes to managing multiple or complex forms that may also need validation they are your best bet. I suggest you take this approach.
Using Redux for this kind of situation is a huge anti-pattern. Redux global store should be only used for global state, not local form state.
Context API is well suited when you need to pass data to multiple deeply nested children. This way you do not need to pass props dozens of levels down the tree. However, it is usually used by 3rd party libraries such as the ones mentioned above (all of them).
You can use Formik library for handling many inputs. Wrap both components inside Formik and use formik's methods.
import { Formik } from 'formik';
<Formik
initialValues={{ fullName: '', address: '' }}
onSubmit={(values) => {
alert(JSON.stringify(values, null, 2));
}}
>
{({handleChange, values, handleSubmit}) => (
<form onSubmit={handleSubmit}>
<div className='app'>
<SideMenu
handleChange={handleChange}
/>
<Document values={values} />
<button type="submit">Submit</button>
</form>
)}
</Formik>
You dont need to create multiple states for each input. handlChange will handle itself. You just need add name or id attribute to input. Also you can access values of each input using the values parameter like values.fullName.
const SideBar = ({handleChange}) => {
return (
<div>
<div className='input-group'>
<label>Full Name:</label>
<input
type='text'
onChange={handleChange}
name="fullName"
/>
</div>
<div className='input-group'>
<label>Address:</label>
<input
type='text'
onChange={handleChange}
name="address"
/>
</div>
</div>
)
}
const Document = ({values}) => {
return (
<div>
<h1>{values.fullName}</h1>
<p>{values.address}</p>
</div>
)
}

I am updating my state using this.setState but still i am recieving the old values in render()

class EditLocation extends Component {
constructor(props) {
super();
this.state = {
LocationId: '',
locationOptions: [],
}
this.baseState = this.state;
this.findLocationById = this.findLocationById.bind(this);
}
findLocationById = (locationId) => {
let locationOptions = [];
if (locationId <= 0) {
setTimeout(()=>{
this.setState(this.baseState);
locationOptions.push(
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={false}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>
)
this.setState({locationOptions:locationOptions})
},200)
else {
setTimeout(() => {
let location = this.props.store.selectedLocation;
this.props.store.LocationTypes.forEach((value)=>{
if(location.LocationTypes ?
location.LocationTypes.includes(value.LocationTypeId): false)
{
locationOptions.push(
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={true}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>
)
}
else
{
locationOptions.push(
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={false}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>
)
}
})
this.setState({
LocationId: location.LocationId,
locationOptions: locationOptions,
})
render(){
return (
<div>
<Modal>
<Form>
<FormGroup>
<input
value={this.state.LocationId}
type="text"
name="Location"
id="Location"
/>
</FormGroup>
<FormGroup>
{console.log(this.state.locationOptions)} // showing updated state value
{this.state.locationOptions} // showing previous state.locationOptions value
</FormGroup>
</Form>
</Modal>
</div>
)
}
}
console.log() inside the render is updating the value by my checks on customInput are not updating. I need to either reopen the modal or reload the whole program to see updates.
Any solution and resources would be helpful as I am stuck at it for hours and can't seem to figure the issue. and store is mobx store if that helps
You using setState in a wrong way.
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
React docs
So it asynchronous and you can't guarantee when update happens.
Using setTimeout is a bad manners in React.
Storing whole components in state, e.g. locationOptions isn't good idea either.
Better to move input to separate component, as i see only defaultChecked different.
Better to use Hooks, easier to think about this in React way, it's require some time and effort to figure out how to write declarative instead of imperative code.
Can refactor a litle
// sync local state and props is a pain, better to avoid it
//constructor(props) {
//super(props);
//const location = this.props.store.selectedLocation
// this.state = {
//LocationId: location.LocationId,
//}
//}
const location = this.props.store.selectedLocation;
render() {
return (
<div>
<Modal>
<Form>
<FormGroup>
<input
value={this.props.store.selectedLocation}
type="text"
name="Location"
id="Location"
/>
</FormGroup>
<FormGroup>
{this.props.store.cationTypes.map((value) => (
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={location.LocationTypes.includes(value.LocationTypeId)}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>}
))
</FormGroup>
</Form>
</Modal>
</div>
);
}

unable to type in react input field

Currently, the default value of my input field is 1. If I try to type something in the input field, nothing changes.
interface Orders {
order_graph_1: number;
order_graph_2: number;
}
interface MyProps extends Orders {
setOrders: (...args: any) => void; // function which takes args...??
}
interface MyState extends Orders {
//otherProperty: string;
}
class Setup extends React.Component<MyProps, MyState>{
state = {
order_graph_1: this.props.order_graph_1,
order_graph_2: this.props.order_graph_2
};
// needs to be an arrow function to access `this` properly
// could use ( event: React.ChangeEvent<HTMLInputElement>)
// could avoid the assertion by passing the name as an argument
setOrders = (event: any) => {
this.setState((prevState) => ({
...prevState,
[event.target.name]: parseInt(event.target.value)
}));
};
render(){
return(
<div className="row">
<div className="col-6">
<p className="text-center">Order of first model: </p>
<div className="w-100 d-flex justify-content-center">
<input className="text-center" name="order_graph_1" type="number" value={this.props.order_graph_1} onChange={this.setOrders.bind(this)} min="1" max="10"/>
</div>
</div>
</div>
);
}
}
export default Setup;
To test, I canged the onChange function
onChange={()=>console.log("hello")}
everytime I tried to type in the input field, I saw hello being printed in the console but the value of the input field still does not change.
edit:
This was a JS code (https://github.com/MenesesGHZ/polynomial-regression-js):
class RegressionSetup extends React.Component{
constructor(props){
super(props);
this.orders = {
"order_graph_1":this.props.order_graph_1,
"order_graph_2":this.props.order_graph_2
};
}
setOrders(event){
this.orders[event.target.name] = parseInt(event.target.value);
this.props.setOrders(Object.values(this.orders));
}
render(){
return(
<div className="row">
<div className="col-6">
<p className="text-center">Order of first model: </p>
<div className="w-100 d-flex justify-content-center">
<input className="text-center" name="order_graph_1" type="number" value={this.props.order_graph_1} onChange={this.setOrders.bind(this)} min="1" max="10"/>
</div>
</div>
<div className="col-6">
<p className="text-center">Order of second model: </p>
<div className="w-100 d-flex justify-content-center">
<input className="text-center"name="order_graph_2" type="number" value={this.props.order_graph_2} onChange={this.setOrders.bind(this)} min="1" max="10"/>
</div>
</div>
</div>
);
}
}
export default RegressionSetup;
Upon changing the value of input, a line on a graph changed according to the value. I had to change this code to Typescript. This is what I have now.
interface Orders {
order_graph_1: number;
order_graph_2: number;
}
interface MyProps extends Orders {
setOrders: (...args: any) => void; // function which takes args...??
}
interface MyState extends Orders {
//otherProperty: string;
}
class Setup extends React.Component<MyProps, MyState>{
state = {
// it is best not to derive state from props
order_graph_1: this.props.order_graph_1,
order_graph_2: this.props.order_graph_2
};
// needs to be an arrow function to access `this` properly
// could use ( event: React.ChangeEvent<HTMLInputElement>)
// could avoid the assertion by passing the name as an argument
setOrders = (event: any) => {
// I don't love this solution, but we can avoid the TS errors by copying the previous state
this.setState((prevState) => ({
...prevState,
[event.target.name]: parseInt(event.target.value)
}));
};
render(){
return(
<div className="row">
<div className="col-6">
<p className="text-center">Order of first model: </p>
<div className="w-100 d-flex justify-content-center">
<input className="text-center" name="order_graph_1" type="number" value={this.state.order_graph_1} onChange={this.setOrders.bind(this)} min="1" max="10"/>
</div>
</div>
{/* <div className="col-6">
<p className="text-center">Order of second model: </p>
<div className="w-100 d-flex justify-content-center">
<input className="text-center"name="order_graph_2" type="number" value={this.props.order_graph_2} onChange={this.setOrders.bind(this)} min="1" max="10"/>
</div>
</div> */}
</div>
);
}
}
export default Setup;
although it compiles without an error, the input value thing is not working. It does not change the line on the graph so I am assuming the state is not saved. How can I fix this?
The problem is that you are using the value of order_graph_1 and order_graph_2 that you get from props but updating the ones that you have in state and not updating the ones in the props.
The code that you are converting updates both the state and the props (by calling this.props.setOrders). But it is totally unnecessary to have these variables in a component state of RegressionSetup at all since it can already access and update them through props.
The setOrders function which is passed down from the parent Main component is also a bit silly because both components have a state with properties order_graph_1 and order_graph_2 -- so why are we passing them as a tuple to setOrders?
In Main you can delete the setOrders function and instead pass down an arrow function that calls setState.
<RegressionSetup
order_graph_1={this.state.order_graph_1}
order_graph_2={this.state.order_graph_2}
setOrders={(orders) => this.setState(orders)}
/>
setState in a class component takes a partial new state and merges it with the current state. That means the we don't even have to pass both orders to setOrders. We can call it with just one order and that's fine.
So we can define the props for our RegressionComponent like this:
export interface OrderGraphs {
order_graph_1: number;
order_graph_2: number;
}
interface Props extends OrderGraphs {
setOrders(graphs: Partial<OrderGraphs>): void;
}
There is no reason for it to be a class component, but it also doesn't matter. RegressionSetup does not need to use state, hooks, or lifecycle methods. It just takes props and returns JSX.
I still prefer to render the two inputs using a function since it's less repetition but obviously you don't have to do that. We use the variable property to get the correct property from props value={this.props[property]} and to set the correct key on the object that we pass to setOrders: onChange={(e) => this.props.setOrders({[property]: parseInt(e.target.value, 10)})}
class RegressionSetup extends React.Component<Props> {
renderInput(property: keyof OrderGraphs, label: string) {
return (
<div className="col-6">
<p className="text-center">{label}</p>
<div className="w-100 d-flex justify-content-center">
<input
className="text-center"
name={property}
type="number"
value={this.props[property]}
onChange={(e) =>
this.props.setOrders({ [property]: parseInt(e.target.value, 10) })
}
min="1"
max="10"
step="1"
/>
</div>
</div>
);
}
render() {
return (
<div className="row">
{this.renderInput("order_graph_1", "Order of first model: ")}
{this.renderInput("order_graph_2", "Order of second model: ")}
</div>
);
}
}
Code Sandbox Link
I messed with the other components as well. I added Props and State types for everything. I also used arrow functions to get rid of all the bind(this) calls. There are still a few Typescript errors related to the external packages (chart.js and mathjs).
A React component only rerenders upon state change. You should therefore change the state accordingly to the value of the input field every time the value changes, and then simply load this state value in the input field.
This is causing issue value={this.props.order_graph_1} The value here is derived from props instead of state. Ideally it should be populate using some state and onChange should update that state.
Since after every rerender, the props won't change and the input value will still remain the same.
this.state.order_graph_1 should be the value for input field.

React doesn't re-render on props change

I am kinda new to react and to the webitself.
this is my render function
render() {
const {repositories} = this.props
return (
<div className='mt4 bt b--black-20 boardingbox scrollarea-content' style={{overflow: 'scroll', height: '100vh'}}>
{
repositories.map((repo, index) => {
console.log(repo.name)
return <Note name={repo.name} desc={repo.name} key={index} onClick={ this.handleClick.bind(this) }/>
})
}
</div>
)
}
The repositories is changing the way I want, but for some reason the its not get re-rendered. I passing the repositiores property from the parent.
The first time I render it (click to the search button, get a response from the server, and set the repo array), its working fine. But at the 2nd search, when there is something in the array, its not working properly, and not re-render.
UPDATE:
The parent's render / onClick
render() {
const {repositories} = this.state
return (
<div className='w-third navpanel br b--black-20'>
<SearchBar onClick={this.onClick} onChange={this.onChange}/>
<RepoList repositories={repositories}/>
</div>
//<NewNote />
//<Tags />
//<NoteList />
);
}
onClick = (event) => {
const {searchTerm} = this.state
let endpoint = 'https://api.github.com/search/repositories?sort=stars&order=desc&q=' + searchTerm;
fetch(endpoint)
.then(blob => blob.json())
.then(response => {
if(response.items)
this.setState({ repositories: response.items });
})
}
UP-UPDATE:
Search Comp:
constructor({onClick, onChange}) {
super()
this.onClick = onClick
this.onChange = onChange
this.state = {
imageHover: false
}}
render() {
return (
<div className='flex items-center justify-between bb b--black-20'>
<div className='ma2 inputContainer w-100'>
<input className='pa1 pl4 boardingbox w-100 input-reset ba b--black-20 br4 black-50 f6' placeholder='repos' type="text" onChange={this.onChange}/>
</div>
<div className='mr2'>
<div className='boardingbox pointer contain grow'>
<img src={(this.state.imageHover) ? NoteImageOnHover : NoteImage} alt=''
onMouseOver={()=>this.setState({imageHover: true})}
onMouseOut={()=>this.setState({imageHover: false})}
onClick={this.onClick}/>
</div>
</div>
</div>
)}
first responde
second responde
and I am really ashamed that I could screw up like this.
So basicly the problem was:
return <Note name={repo.name} desc={repo.name} key={index} onClick={ this.handleClick.bind(this) }/>
So I was as stupid to use INDEX as a KEY so I could not add again the same key to the array.
Thanks anyway guys! :)
The root cause most probably is due to error in function binding.
In your SearchComponent you are using the "props" to create function bindings in the contructor. This can cause your SearchComponent to refer to wrong instance of the functions for onClick and onChange. Would suggest referring to the official documentation for more details.
you do not need to rebind the functions in your SearchComponent, you can just use the functions received in props.
<input className='pa1 pl4 boardingbox w-100 input-reset ba b--black-20 br4 black-50 f6' placeholder='repos' type="text" onChange={this.props.onChange}/>
<!-- snipped other irrelevant code -->
<img src={(this.state.imageHover) ? NoteImageOnHover : NoteImage} alt=''
onMouseOver={()=>this.setState({imageHover: true})}
onMouseOut={()=>this.setState({imageHover: false})}
onClick={this.props.onClick}/>
Why could be happening to cause this behavior
Remember, constructor is only called once the component instance is being constructed, once it has been created and remains alive, React lifecycles take over.
So, when you first render your screen, the component is created and since there is only 1 of everything, it kind of works.
When you run your first search: onChange/onClick callbacks modify the state of the parent component. Which then calls render on the parent component.
At this point, it is possible that your SearchComponent maybe holding on to the wrong instance of the call back methods, which would thus not set state on the parent and thus not force re-render.
Additional Notes on your constructor
Normally you shouldn't refer to props in your constructor, but if you need to, then you need to have it in the format below. Here are the relevant docs:
constructor(props) {
super(props);
// other logic
}

Keypress is failing to record all keystrokes

I'm trying to capture keystrokes from a textarea and have tried using attributes onKeyUp, onKeyPress, onKeyPressCapture, onKeyDown, onKeyDownCapture. All of them seem to miss some key entries:
When I enter a new key, one of the ones that was not displaying before then shows, in order.
Because of that queued delay, I'm thinking I might need to put a delay on the console log. But that doesn't actually solve the underlying issue. Does anyone know why this behavior is happening?
Here is the parent (App) and child component (TypeArea)
Parent
class App extends React.Component {
constructor (props) {
super(props)
// sets up this.props to function
this.state = {
textbox_in_parent_state: 'string passed from state of Parent(App)',
someVar: 'parent_constructor_state',
text_from_textarea: ''
}
this.handler = this.handler.bind(this)
this.text_capture_from_parent = this.text_capture_from_parent.bind(this)
}
text_capture_from_parent(eventObject) {
this.setState({
text_from_textarea: eventObject.target.value
})
console.log(this.state.text_from_textarea)
}
render() {
return (
<div>
<div className="container">
<Header />
<div className="row">
<div className="col-sm-6">
<TypeArea textcapture={this.text_capture_from_parent}
/>
</div>
<div className="col-sm-6">
<MarkdownPreview />
</div>
</div>
</div>
{/* <div style={{textAlign: 'center'}}>*/}
{/* <h1>App component written in client/components/App.jsx</h1>*/}
{/* </div>*/}
</div>
)
}
}
export default App
Child
import React from 'react';
class TypeArea extends React.Component {
constructor(props) {
super(props)
this.state = {
textbox_text: 'string from state of child "TypeArea"'
}
console.log(this.state.textbox_text)
}
render() {
return (
<div>
<h1>Typing Area</h1>
<div className="form-group">
<textarea className="form-control" id="textbox" rows="25" placeholder="Type Here" onKeyPressCapture={this.props.textcapture}>
</textarea>
<button onClick={this.props.passdown}>Click me</button>
</div>
</div>);
}
}
export default TypeArea
text_capture_from_parent(eventObject) {
this.setState({
text_from_textarea: eventObject.target.value
})
console.log(this.state.text_from_textarea)
}
this.setState() is async, and you are calling directly console.log(..) after setting the state, at this moment the state maybe didn't successfully changed already. but luckily this.setState(..)is providing a callback when it finished setting the new state. so you can call it like this:
this.setState({
text_from_textarea: eventObject.target.value
}), () => {
console.log(this.state.text_from_textarea);
});
and you should see the actual value.

Categories

Resources