I want to implement a form that is made of base fields (e.g.: product name, price and type), then type-specific fields depending on the type selected.
The solution seemed straightfoward at first (see this codesandbox), since I managed to render different components based on the type found in the current product state.
However, because these components are being rendered conditionally while using hooks internally such as useEffect, they are breaking the Rules of Hooks and causing the following error when I change from one product type to another:
Should have a queue. This is likely a bug in React. Please file an issue.
Warning: React has detected a change in the order of Hooks called by TypeSpecificForm. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks (...)
Previous render Next render
------------------------------------------------------
1. useState useEffect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
in TypeSpecificForm (at ProductForm.js:36)
in div (at ProductForm.js:14)
in ProductForm (at App.js:8)
in App (at index.js:15)
in StrictMode (at index.js:14)
What is the correct way to render these partial fields dynamically?
As I suspected, the solution was something rather silly.
Instead of returning the render function for the dynamic field...
function TypeSpecificForm({ product, onChange }) {
const productType = productTypes[product.type];
if (!productType?.renderForm) return null;
return productType.renderForm({ product, onChange });
}
...I called it as a React Node component like so:
function TypeSpecificForm({ product, onChange }) {
const productType = productTypes[product.type];
if (!productType?.renderForm) return null;
const Component = productType.renderForm
return <Component product={product} onChange={onChange} />
}
Now React no longer complains about breaking the Rules of Hooks and everything works as expected.
I remember however seeing something on the React docs that these 2 approaches could be interchangeable, which clearly they are not. I'll update my answer once I find the exact root cause for this issue.
Edit: I still couldn't find anything on the official docs that highlights the differences between rendering components via a render function vs JSX components, but I did come across an interesting discussion about this very subject.
Related
I've seen a few questions related to this topic, but none that tackle the issue head-on in a pure way. useContext is a great tool for minimizing prop-drilling and centralizing key data needed across your app; however, it comes at a cost that I'm trying to minimize.
The closest issue to the one I'm describing here was asked two years ago here. The question didn't gain a lot of traction and the only answer basically says call the context in a parent container and pass values down through props (defeats the purpose of context) or use Redux. Maybe that's the only way, but I wanted to bring the question back to the collective to see if there is a better answer that's not dependent on external libraries.
I've set up a code sandbox here to illustrate the issue. Re-renders are console logged out to make seeing them easier.
In short, when a state changes in context, every app accessing data from that context re-renders, even if it's not utilizing the state data that changed (because it's all ultimately passed through the value object). React.Memo does not work on values accessed from context the same way it does for properties.
For example, in the code sandbox linked above, in App.js if you comment out <AppContainerWithContext /> and load <AppContainer />, there are two states managed in the container, one called titleText and the other called paragraphText. titleText is passed as a prop to component called TitleText and paragraphText is passed to a component called ParagraphText. Both components are wrapped in React.memo(). There are two buttons called in the AppContainer and each has a function that changes the text back and forth based on the value of separate boolean states.
Here is the function that toggles the titleText, the one for paragraph text is the same logic:
const changeTitleHandler = useCallback(() => {
const title = listTitleToggle ? "Title B" : "Title A";
setListTitleToggle((pV) => !pV)
setTitleText(title);
}, [listTitleToggle]);
Since the titleText component and paragraphText components are wrapped with React.useMemo, they only re-render when the corresponding value passed to them changes. Perfect.
Now in App.js if you comment out the <AppContainer /> component and enable the <AppContainerWithContext /> component, the rendered output and result of button clicks is identical; however, the states that change and are rendered on the screen are now managed by AppContext (called contextTitleText and contextParagraphText and passed to the TitleText component and ParagraphText component via useContext.
Now, if you click on the button to toggle the title, the ParagraphText component re-renders too, even though it doesn't use the contextTitleText state. My understanding of why this happens is because the value object changes when the contextTitleText is updated, causing any component accessing that value object through useContext to re-render.
My question is this:
Is there a way to utilize useContext without causing re-renders on all components accessing the context. In the example above, can we utilize useContext to manage the contextTitleText and the contextParagraphText but only re-render the components where the state from context being accessed changes?
I was doing some coding on React, and encountered an issue I would like to properly deal with. Details on the matter are provided below.
The Environment
Suppose you have a component FormDemo made to handle a potentially complex form, parts of which involve the dynamic management of certain input fields. As an example, the provided code sample allows to create any amount of fields for names between 0 and (232 - 1) fields due to JavaScript's limitations on array length.
Press Add New Name button above all name fields to append another name field. Press Remove button to the right of any input to delete it from the list.
Each name input created is handled by a separate component SubForm that takes three properties:
id: a unique generated identifier of the current field.
onChange: a function executing whenever the value of that input was changed.
onRemove: a function executing whenever the Remove button of that form was clicked.
The Sample
Here is a working sample of a code I've made on CodeSandbox provided for demonstration purposes.
The Problem
The approach used in the code sample works, but it has the eslint problem mentioned in Problems tab of CodeSandbox, and I am aware that it's not a CodeSandbox issue, as I've tested the same project in my local environment and got the same problem. Here are that problem's details taken right from the console:
React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback. (react-hooks/exhaustive-deps)
Following the advice from the problem directly (i.e. adding onChange to dependency list of SubForm's useEffect) results in infinite rendering, and thus is not a solution to the problem.
The Research
After some reading of the official React docs on useCallback, as well as the other part of these on useEffect, I've figured out that, when rendering a component, React creates new instances of functions declared in a component's body. Therefore, adding such functions to a dependency list of some useEffect hook that has an effect function attached to it will entail that function being called on each render.
In my approach, I pass update function to SubForm component in onChange property as a reference (proven here by React docs), hence the SubForm component's onChange property has exactly the same instance of the update function as the parent component. So, whenever the instance of the update function changes with it added to the dependencies of a useEffect hook, that executes the effect function attached to it, and, taking the above into account, this happens on each render of a parent component FormDemo.
The update function changes the value of forms, a state variable of FormDemo component, causing it to rerender. That recreates the instance of an update function. The SubForm component gets notified of that change and executes an effect function attached to a useEffect hook, calling the update function once again. In turn, this causes another change of a state variable forms, telling the parent component FormDemo to render again... and this continues indefinitely, creating an infinite loop of renders.
Some of you may ask why does it happen if an input field of the form was not changed between these two renders, thus the value passed to update function is effectively the same as before. After some testing, which you can try yourself here, I came to the conclusion that it's actually wrong: the value set to forms is always different. That's because even though the object's content is exactly the same, its instance is different, and React compares object instances instead of their contents, sending a command to rerender the component if these instances differ.
As useCallback hook memoizes the instance of the function between renders, recreating the function only when the values or instances of its dependencies change, I've assumed that wrapping update function in that hook will solve the original problem, because the instance of the function will always stay the same.
However, wrapping the update function in useCallback will result in another problem: I will need to add forms as a dependency, because I'm using it inside that function. But, taking the above into account, this will bring me the original problem back due to the instance of forms being different after each update, and that will command useCallback to recreate the instance of the function, too.
Potential Solution
With all that being said, I have a solution that I don't quite like, even though it works because it removes the need of adding the state variable forms to the list of dependencies of useCallback:
const update = useCallback((id, value) => {
setForms(prevState => {
const { form_list } = prevState,
new_forms = [...form_list],
mod_id = new_forms.map((e) => e.id).indexOf(id);
new_forms[mod_id] = value;
return { ...prevState, form_list: new_forms };
});
}, []);
So why am I against it, if it works and gives no problems in the console?
In my humble opinion (feel free to prove me wrong), because of these issues:
Direct usage of state setter function instead of a dedicated middleware function. This decentralizes direct state management.
Duplication of an original array, which may be expensive on memory if an array has a lot of values inside, not to mention that each value itself is an object.
The Question
What is the most memory-efficient and readable solution of the stated problem in the provided case that will use a middleware function setField? Alternatively, if it's possible to debunk my issues with a potential solution, prove that it's the best way to go.
Feel free to modify the contents of setField if necessary for the solution and remember that I'm all open for answering anything related to the question.
It seems you are duplicating state of each SubForm: you store it in parent and also in SubForm, why not store state only in parent and pass as props?
I am talking about something like this:
const SubForm = ({ id, form, onChange, onRemove }) => {
return (
<Form>
<Form.Group controlId={`form_text${id}`}>
<Form.Label>Name (ID {id})</Form.Label>
<InputGroup>
<Form.Control
type="text"
value={form.name}
onChange={(e) => onChange(id, { ...form, name: e.target.value })}
/>
<Button variant="danger" onClick={() => onRemove(id)}>
Remove
</Button>
</InputGroup>
</Form.Group>
<br />
<br />
</Form>
);
};
To pass each form data just do:
<SubForm key={e.id} form={e} id={e.id} onChange={update} onRemove={remove} />
No need for useEffect anymore.
You probably want to separate the management of IDs from SubFrom. SubForm shouldn't be able to change it's ID.
Wrap the update & remove functions - so SubForm doesn't need to send the ID back.
<SubForm key={e.id} id={e.id}
onChange={(form) => update(e.id, form)}
onRemove={() => remove(e.id) } />
Make sure that SubForm will not change ID as part of form
const update = (id, value) => {
setField(
"form_list",
// subform shouldn't change id, so we overriding it (to be sure)
forms.form_list.map(e => e.id===id?{...value, id}:e)
);
};
You still optionally may pass the ID to the SubForm, but the management of IDs is separated from it.
Modified code
From React documentation.
Conceptually, components are like JavaScript functions. They accept
arbitrary inputs (called “props”) and return React elements describing
what should appear on the screen.
Considering:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
or
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Will give us the ability to do this:
<Welcome name="Luke" />;
<Welcome name="Leia" />;
to use as we wish in the DOM,
Hello, Luke
Hello, Leia
Now when people prescribe props shouldn't be changed, it would make sense the reason is in my thinking would be like the same as changing the values of attributes of an image tag?
HTML:
<img id="Executor" alt="Picture of Executor" src="/somepath/vaders-star-destroyer-executor.jpg"/>
JS:
Meanwhile in a Javascript file a long time ago in a galaxy far, far away...
var imageOfVadersStarDestroyer = document.getElementById('Executor');
imageOfVadersStarDestroyer.src = "/somepath/vaders-star-destroyer-avenger.jpg"
Because if we keeping changing an elements attribute values this can cause confusion and slower renderings?
So is the reason why the prescription is to never change props in React is because is the library is trying to make elements as predictable as possible?
Setting props outside of React is dangerous and should be avoided. Why? The main reason is that it doesn't trigger re-renders. Hence bugs and unexpected behaviour.
Re-rendering
Most of the time, props are data that is store as state in the parent component, which is manipulated by calling setState() (or the second function returned by React.useState()). Once setState() is called, React re-renders and computes what has changed under the hood, with the latest props and state. Manually assigning values to props, therefore won't notify React that the data has changed and something has to be re-rendered.
The good practice
Making props read-only allows React components to be as pure as possible, which is obviously a good practice anyway even when writing plain JS. Data won't be changed unexpectedly and can only be done so by calling setState() (You might have heard of the single source of truth, which is what React is trying to leverage).
Imagine you notice something went wrong in the app and the data shown to the end user is completely different from the source, it would be a pain trying to find out where the data has been manipulated wouldn't it? :)
never change props in React
means that you should never do this.props.name = "userName" because of React's one way data binding, props are read only, to update a component's props, you should pass a function from the parent that will do that ( in the parent ) , or dispatch an action if you're using redux, a change in the props will trigger a re-render
props is a constant in this case. You will always need it in your components.
But there is a cleaner way to write it or even omit it.
Regular way with Function Expression (same as your exemple)
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
ES6 Object Destructing - explicit
function Welcome(props) {
const {name} = pros
return <h1>Hello, {name}</h1>;
}
ES6 Object Destructing - inplicit, cleaner way
function Welcome({name}) {
return <h1>Hello, {name}</h1>;
}
And of course, you can use the class way which requires the usage of this.props.yourAttr
However, in the new version 3 of create-react-app, changed class components to functional components. You can see this exact modification on Github here.
You can need to learn more about destructing assignment in the old and good MDN linked here or an in-depth approach both array and object destructuring here.
I have been working with Redux & React for a few months. I usually always use Chrome with no issues. ( Endless bugs actually :) ).
When I started testing in Firefox I ran into an issue which I need some help with ... To know if there is a perfect way at dealing with this ...
Issue
Redux Props for MapStateToProps are not yet available when the constructor gets called, which means I cannot construct my components state in the component constructor. These props become available swiftly afterwards in the render function. At this stage, it is too late because I cannot construct state in the render function (Could somehow work that, but wouldn't be good to approach right ?).
For the moment I am using the componentWillReceiveProps and duplicating my constructor function with one exception
Constructor function
constructor(props){
super(props);
//Loads of code named A
this.state = {state:A};
}
Component Will Receive Props Function
componentWillReceiveProps (){
//Loads of code named A
this.setState({state:A});
}
There may be an issue over overwriting my state here, but for my exact case here, its only displaying data, no UI changes happen... This doesn't appear correct method either way...
I read this article
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
I am not quite sure if I understand this fully. I did experiment with it a little with no working solutions.
Ideally, I need the constructor to pause until all redux store is populated which also doesn't make sense. Props arrays could be empty.
There are discussions on Slack but none seem to address this exactly. I tried googling issue but couldn't find exact issue addressed ...
I need the mapStateToProps props to construct my state. It is looking like I won't be able to do this and will need to totally refactor code to work more solely in the render function with loads of ternary operators and/or making calls to set state from the render function before the render returns.
Any thoughts on this issue?
Daniel
Why do you think you need put the data you get from props into the component state?
As far as using the data there is no difference between the two except that you're more likely to get into trouble if you copy props to state (see link you posted).
const { A } = this.state;
const { A } = this.props;
If the data is coming via an async method then you should accommodate that in your render method.
render() {
const { A } = this.props;
if (!A) {
return <LoadingIndicator />
}
...
}
I am currently using strings for ref= in React 15.3.2, and such is deprecated.
So if I use a callback per the docs:
ref={(input) => this.textInput = input}
Then this will attach a DOM Element to my class when the component mounts.
Yay! A DOM element. So now I do not have to do:
ReactDOM.findDOMNode(this.refs.input).value=''; // uncontrolled component
I thought the whole idea of react is to not touch the DOM...
Lets assume I have a TextInput component that is complex, and has an InputError component along with <input type="text" -- this TextInput component does a bit of validation based on props, and it has a state that helps things like when to show error.
Back up in the form, I want to clear the form, so TextInput has a .clear() which resets its state. (Am I doing it wrong already?)
The thing is, I cannot access any child components React Objects unless I use strings as ref=, so I cannot call clear().
What gives?
Am I supposed to route "all communication" through props? What good is this.refs once all refs are callbacks? Can I get my react objects through .children or something? What is the convention on this issue?
Edit:
Obviously I am learning React, I guess the basic question is, is it bad (anti-react) to EVER call methods on a child component?
The components in question can be found here:
RegisterForm.jsx
TextInput.jsx
InputError.jsx
The requirement I find difficult/strange making work via props is TextInput: "onblur then if error then show error, mark as errored until changing passes validate"