React - How to share Child component states with Parent? - javascript

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)

Related

Why does custom input component cause "Function components cannot be given refs" warning?

While trying to customize the input component via MUI's InputUnstyled component (or any other unstyled component, e.g. SwitchUnstyled, SelectUnstyled etc.), I get the warning
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `ForwardRef`.
InputElement#http://localhost:3000/main.4c2d885b9953394bb5ec.hot-update.js:59:45
div
...
I use the components prop to define a custom Input element in my own MyStyledInput component which wraps MUIs InputUnstyled:
import InputUnstyled, {
InputUnstyledProps
} from '#mui/base/InputUnstyled';
const MyStyledInput: React.FC<InputUnstyledProps> = props => {
const { components, ...otherProps } = props;
return (
<InputUnstyled
components={{
Input: InputElement,
...components,
}}
{...otherProps}
/>
);
};
My custom input component InputElement which is causing the Function components cannot be given refs warning:
import {
InputUnstyledInputSlotProps,
} from '#mui/base/InputUnstyled';
import { Box, BoxProps } from '#mui/material';
const InputElement: React.FC<BoxProps & InputUnstyledInputSlotProps> = (
{ ownerState, ...props }
) => {
return (
<Box
component="input"
// any style customizations
{...props}
ref={ref}
/>
);
});
Note: I'm using component="input to make MUI's Box component not render an HTML div but an HTML input component in the DOM.
Why am I getting this warning?
Other related questions here, here and here address similar issues but don't work with MUI Unstyled components. These threads also don't explain why
The warning wants you to have a look at the InputElement component. To be honest, the stack-trace is a bit misleading here. It says:
Check the render method of ForwardRef.
InputElement#http://localhost:3000/main.4c2d885b9953394bb5ec.hot-update.js:59:45
div
You can ignore the ForwardRef here. Internally InputElement is wrapped by
The crucial part for understanding this warning is:
Function components cannot be given refs. Attempts to access this ref will fail.
That is, if someone tries to access the actual HTML input element in the DOM via a ref (which Material UI actually tries to do), it will not succeed because the functional component InputElement is not able to pass that ref on to the input element (here created via a MUI Box component).
Hence, the warning continues with:
Did you mean to use React.forwardRef()?
This proposes the solution to wrap your function component with React.forwardRef. forwardRef gives you the possibility to get hold of the ref and pass it on to the actual input component (which in this case is the Box component with the prop component="input"). It should look as such:
import {
InputUnstyledInputSlotProps,
} from '#mui/base/InputUnstyled';
import { Box, BoxProps } from '#mui/material';
const InputElement = React.forwardRef<
HTMLInputElement,
BoxProps & InputUnstyledInputSlotProps
>(({ ownerState, ...props }, ref) => {
const theme = useTheme();
return (
<Box
component="input"
// any style customizations
{...props}
ref={ref}
/>
);
});
Why do I have to deal with the ref in the first place?
In case of an HTML input element, there as a high probability you want to access it's DOM node via a ref. This is the case if you use your React input component as an uncontrolled component. An uncontrolled input component holds its state (i.e. whatever the user enters in that input field) in the actual DOM node and not inside of the state value of a React.useState hook. If you control the input value via a React.useState hook, you're using the input as a controlled component.
Note: An input with type="file" is always an uncontrolled component. This is explained in the React docs section about the file input tag.

React best practice for changing state of parent component from child without rerendering all children?

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;
})
}, []);

React - unnecessary rendering

I am learning ReactJS. I would like to use functional component. In my component, I have a textbox and a clear button.
My concern is - whenever I type a char from my keyboard to enter info for the text box, I see the search account called!!! in my console output. If i type 5 chars, I would see this 5 times - it basically means the whole component is re-rendered & methods re-defined.rt?
Is it not bad practice and affect the performance? Is there any other alternative?
import React, { useState, useContext } from 'react';
import AccountContext from . './accountContext'
const SearchAccounts = () => {
const [text, setText] = useState('');
const onChange = (evt) => setText(evt.target.value);
console.log('search account called!!!');
// some methods are defined here
onSubmit = () => {...}
onClear = () => {...}
return (
<div>
<form onSubmit={onSubmit} className="form">
<input
type="text"
name="text"
value={text}
onChange={onChange}
placeholder="Search Accounts..."
/>
<input type="submit" value="Search" className="...." />
</form>
<button
className="...."
onClick={onClear}
style={getClearStyle()}
>Clear</button>
</div>
);
}
export default SearchAccounts;
Re-renders aren't necessarily expensive, and you have to accept that your components will re-render frequently in order propagate the changes in your data to the UI. Your example is very cheap, since the component is small and does not render any additional components in its return function - this is the ideal way to compose React components that have to re-render often.
You have to remember also that your JSX is not trashing and appending all HTML elements to the DOM every time the component re-renders. Only the difference between the last render and the current one is being applied, which is what allows React and other front end frameworks to create smooth and fast UIs when built at scale.
If and when you do reach a bottleneck in your components, you can use memoisation techniques (React.memo in a functional component context, shouldComponentUpdate in a class context) to prevent regularly rendering components from affecting the performance of their component children. It's usually best to do this towards the end of a project or unit of code, and only as a final measure since memoisation escapes built in React optimisation and can actually cause more problems than it solves if you use it inappropriately. A well structured component tree and flux-based state solution will alleviate most performance issues.
My concern is - whenever I type a char from my keyboard to enter info for the text box, I see the search account called!!! in my console output. If i type 5 chars, I would see this 5 times - it basically means the whole component is re-rendered & methods re-defined.rt?
Yes, this is expected and aligns with the mental model of React.
In React, you'll hear people say "the view is a function of state" — meaning that whatever data you inside of variables in react state should directly determine the rendered output, sometimes refered to as "one-way data flow"
The consequence of doing this however is that, in order to determine what changed, you need to render the react component.
This is fine!
Remember what powers React — the virtual dom. Internally, react creates a light-weight representation of the browser's DOM so that the above one-way data flow is efficient.
I see a separate concern here though regarding the amount of time you see the console.log output.
What you're doing there is a "side-effect" in React, and whenever you have a side-effect, you should wrap in in useEffect. useEffect will ensure your side-effect only runs when the values you feed to its dependency array changes.
const { useEffect, useState } = React;
const { render } = ReactDOM
function MyComponent() {
const [value, setValue] = useState('world');
useEffect(() => {
console.log('this should only render once');
}, []);
useEffect(() => {
console.log(`this will render on every value change: the value: ${value}`);
}, [value])
return (
<label>
hello, <input value={value} onChange={e => setValue(e.target.value)} />
</label>
);
}
render(<MyComponent />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
See here for more on how useEffect works and here if you want a deep dive.

Input values not rendering to another component in React Hooks

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

How to access component form data in a Redux `matchDispatchToProps` function?

Edited. People have suggested passing in values to my action creator but I've tried that every way I can think of and it fails to work.
I'm currently getting my first taste of Redux and trying to get my call to mapDispatchToProps to read information in a form on button click but I'm not clear as how to get it to do so. The form component is rendered by React, and so it's a question of being able to bind when it's available but Redux is a monkey wrench I don't know how to compensate for yet. Essentially I have this for my component:
import React from 'react';
import { connect } from 'react-redux';
import { action } from '../actions/actionFile';
const Add = (props) => (
<div className="add">
<input className="field-one" type="text" placeholder="One" />
<input className="field-two" type="number" placeholder="Two" />
<input className="field-three" type="number" placeholder="Three" />
<button onClick={() => props.addItem('Literally anything')}>+</button>
</div>
)
const mapDispatchToProps = (dispatch) => {
return {
action: () => dispatch(action({
// I have three fields I need to update in the store.
}))
}
}
export default connect(null, mapDispatchToProps)(Add);
And this for my actions file:
import { ADD_ITEM } from '../constants/items';
export const addItem = (val) => {
return {
type: ADD_ITEM,
val
}
}
But if I run this and set a breakpoint inside the action creator the val value is undefined. For some reason Redux isn't letting me feed dynamic data to the action creator and I don't understand why.
Obviously I can't just pull the information with querySelector because the form doesn't exist when the callback is loaded. If I fill the object passed to the action with hard-coded dummy values it works, but I'm not able to pull in data from the form fields. I'm not even clear as to where to start with this. Any direction is greatly appreciated.
You can't access any data from Redux state, or from inside the component, in mapDispatch, because it is used as part of a wrapper component that goes around your actual component (and thus doesn't have access to anything in your component's state).
Your main options are:
Pass any necessary values as arguments into the function, like props.action(a, b, c)
Switch to using the React-Redux hooks API (useSelector and useDispatch), which lets you access data from the Redux store inside of your function component. You can then capture these values while defining a click handler.
Also, as a side note: if you are going to use connect, you should use the "object shorthand" form of mapDispatch rather than defining it as a function.
You just need to add onChange event handler to your three fields and store data of each input into your component state.
Then on button click dispatch action using this.props.action with data in your state.
In this way you can get all of your data into redux.
render() {
return <button onClick={() =>this.props.toggleTodo(this.props.todoId)} />
}
const mapDispatchToProps = dispatch => {
return { toggleTodo: todoId =>dispatch(toggleTodo(todoId)) }
}
For reference -Connect: Dispatching Actions with mapDispatchToProps · React Redux

Categories