I am building an interface where you choose a radio and it will reveal different form elements.
When a slider is revealed the value change gets set into an object that is rendered on the page.
In this example I have it so it does update the object but the slider no longer moves even though the value is getting correctly set if you click on the line.
https://codesandbox.io/s/shy-dawn-e5zyu
The object I am setting is declared in the parent :
const defaultActivity = {
group: "",
a: 0,
b: 0
};
const [activity, setActivity] = useState(defaultActivity);
Then in the child component that contains the slider the handleChange function is use within that component and I also use a parent function to access setActivity.
const TestSlider = (props) => {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
// This sets the slider value to the activity object
// if you comment this line out then the sliders slide correctly
props.onDataChanged(props.name.toLowerCase(), newValue);
};
return (
<React.Fragment>
<Typography id="discrete-slider" gutterBottom>
{props.name}
</Typography>
<Slider
value={value}
aria-labelledby="discrete-slider"
valueLabelDisplay="auto"
step={props.step}
marks
min={0}
max={props.max}
name={props.name}
onChange={handleChange}
/>
</React.Fragment>
);
};
And that onDataChanged method in the parent
const onDataChanged = (name, value) => {
setActivity({
...activity,
[name]: value
});
};
As I noted in the comment there, if you remove the onDataChanged method then the sliders work again. I have tried moving handleChange to the parent as well but that did not help.
After creating this Sandbox I seem to have a new problem that is sporadic and unclear to me steps to replicate where I am getting the error :
Cannot read property 'getBoundingClientRect' of null
Maybe related to broken slider issue or something else I screwed up. Thanks in advance for any help with this, I am trying hard to learn my way around React but this problem has me really stumped.
============= UPDATE: Closer to expected and a new problem =============
Still playing with this and decided to try and use onChangeCommitted on the slider to try and disconnect the value getting set in the slider with it getting set to the main object. Now it slides and updates the value which sticks in the object but gets reset to 0 for the slider UI. Hard to explain so maybe easier if you go play with it and see what I mean. I think the way I am setting the default state is probably the issue but not sure how to refactor this :
https://codesandbox.io/s/sleepy-euler-0dv69?file=/src/TestSlider.js:116-163
The root of the problem is that the component RenderSwitch is created (declared) inside the AddActivitySelect (SelectForm) component. Moving RenderSwitch outside and passing all necessary props fixes the problem. Here is a working codesandbox.
What happened:
When you call props.setActivity in TestSlider, the activity state updates in App
App re-renders which causes SelectForm to re-render also.
Because a completely new RenderSwitch component is created inside SelectForm, the previous one in unmounted (destroyed).
The new RenderSwitch is rendered, calls React.useState(0), and that's why the slider value is 0
Related
I started my first real project in React, I'm developing a portfolio site and I have a strange issue when I use the category filter to switch the categories.
The issue is: The site shows all the projects, if you click in Artwort or switch between the buttons you will see that not all the projects are showing the transition animation, it seems that the projects in the current category are not rendering again. Another weird thing is in the react developer tools the profiler shows how all the components are rendering when y change the category.
I think this behavior will have logical explanation, but I couldn't find it because I'm using useEffect dependency with the currentCat state.
you can see the error here: https://toiatemp-manuverrastro.vercel.app/
Here is the components:
https://github.com/manuverrastro/toia/blob/main/src/components/Filter.js
https://github.com/manuverrastro/toia/blob/main/src/components/Work.js
https://github.com/manuverrastro/toia/blob/main/src/components/WorkList.js
https://github.com/manuverrastro/toia/blob/main/src/components/WorkListContainer.js
Does anyone have some idea of what is happening?
It is because the key property in your WorkList.js file. Although the categories are different, work.id is not getting changed while you switch between the tabs. Since you have given work.id as the key parameter React tries to render the same previous element without re-rendering it. Because of that you don't see any animation in those Work components.
You can change your key prop which is given to the Work component, by concatenating the current selected category. So each time you switch between the tabs, key prop will differ. It will result in re-rendering the Work components. I have changed your code as my suggestion.
WorkList.js
import Work from "./Work";
const WorkList = ({ work, currentCat }) => {
return (
<>
{currentCat
? work
.filter((work) => work.category == currentCat)
.map((work) => {
return (
<Work
key={`${currentCat}-${work.id}`}
id={work.id}
slug={work.slug}
thumbnail={work.thumbnail}
image={work.image}
title={work.title}
category={work.category}
/>
);
})
: work.map((work) => {
return (
<Work
key={`all-${work.id}`}
id={work.id}
slug={work.slug}
thumbnail={work.thumbnail}
image={work.image}
title={work.title}
category={work.category}
/>
);
})}
</>
);
};
export default WorkList;
So I have been trying to figure out a solution for big lists in react. and I almost have the perfect solution - but I can't seem to figure out how to get this one detail to work.
I have a list of cards that I want to render quickly - I'm using the react-window package
import{VariableSizeList as List} from 'react-window';
I have a component called MaterialCard, and a component that contains lists of MaterialCards:
On the MaterialCard if you click on an inspect button - it opens up the innards of the card revealing a form input section.
const [cardOpen, setCardOpen] = useState(false);
const cardInnards = //a bunch of jsx.
//in the return
<button className="btn-inspect" onClick={()=>setCardOpen(!cardOpen)}>inspect/edit</button>
{cardOpen?cardInnards:<div></div>}
In the multi-list component container - I have this code.
const materialCardsNew = materialsNew.map((x,i)=><MaterialCard props={x} key ={`${i}matsnew`} />);
//react-window function to make a row:
const Row = array=> ({ index }) => array[index] //is MaterialCard
//in the return
<List
height={755}
itemCount={materialCardsNew.length-1}
itemSize={()=>130} // this is where I'm having trouble.
width={634}
>
{Row(materialCardsNew)}
</List>
Currently the itemSize is what I'm trying to fix...
What happens currently is the item has a fixed size area it appears in - and (thanks to z-index) the innards appear over other items in the list.
What I want to happen is the item size of the MaterialCard that is open - to be of a larger size: such that it doesn't cover other cards - I want it to expand and I don't want it to cover other cards at all.
My trouble is I don't know how to read the component's internal state from the top - how do I determine in the list container component which card is open. I understand I require a function for this... but: well...
the pseudocode I've come up with is this: this of course does not work - but it is more or less what I want to do.
//resize function
const getHeight = (arr)=>(index)=>arr[index].state.cardOpen?500:100; //bigger size if the card is open
//jsx
itemSize={getHeight(materialCardsNew)}
//I've also tried arr[index].style.height,
//and searched through the arr[index] properties in the console to see
//if there was anything I could use... no luck.
My first ideas are bunk... and I'm not really sure how to approach this... I'm pretty sure I shouldn't need a massive additional array for each array of material cards (there are a few categories) to keep track of which cards are open... but I can't seem to find the correct way of going about this.
How do I accomplish this?
For this issue:
My trouble is I don't know how to read the component's internal state
from the top
Lift state up. So in the containing component, you can use a hook like in the top level component:
const [activeCard, setActiveCard] = useState()
And in the card, pass in the function:
<Card setActiveCard={setActiveCard} key={someKey} {...otherProps}/>
And, in the implementation of the card, you can have something like:
useEffect(() => setActiveCard(key), [key])
And the top level component will have the 'active' card information.
Not sure I completely was clear on the issue, but that is one mechanism for sending child information to the parent.
And, if I am understanding the issue, you could have some logic in the child component to check if the active card is equal to the card:
<Card setActiveCard={setActiveCard} activeCard={activeCard} key={someKey} {...otherProps} />
useEffect(() => activeCard === key ? setComponentSize(activeSize) : setComponentSize(defaultSize), [{dependency array}])
Of course, the setComponentSize would be in the top level component, and passed in a similar fashion to setting the card index in the top level. And if everything is set in the containing (parent component), you could just check the index vs the activeCard.
Finally, just make sure however you are checking for the active card, you cleanup and call setActiveCard(-1), or whatever the default parameter you might want to use when the active card's state changes.
Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
Background is at the top, my actual question is simple enough, at the bottom, but I provided the context in case I'm going about this totally wrong and my question turns out to not even be relevant. I have only been using react for about two weeks.
What I'm trying to do is create a singleton, re-usable backdrop that can be closed either by clicking it, or by clicking a control on the elements that use a backdrop. This is to avoid rendering multiple backdrops in multiple places in the DOM (e.g. grouping a backdrop with each different type of modal, side drawer or content preview) or have multiple sources of truth for the state of the backdrop.
What I've done is create the Backdrop itself, which is not exported
const Backdrop = props => (
props.show ? <div onClick={props.onClose} className={classes.Backdrop}></div> : null
);
I've also created a backdrop context, managed by a WithBackdrop higher order class component which manages the state of the backdrop and updates the context accordingly
class WithBackdrop extends Component {
state = {
show: true,
closeListeners: []
}
show() {
this.setState({ show: true });
}
hide() {
this.state.closeListeners.map(f => f());
this.setState({ show: false, closeListeners: [] });
}
registerCloseListener(cb) {
// this.setState({ closeListeners: [...this.state.closeListeners, cb]});
// Does this count as mutating state?
this.state.closeListeners.push(cb);
}
render() {
const contextData = {
isShown: this.state.show,
show: this.show.bind(this),
hide: this.hide.bind(this),
registerCloseListener: this.registerCloseListener.bind(this)
};
return (
<BackdropContext.Provider value={contextData}>
<Backdrop show={this.state.show} onClose={this.hide.bind(this)} />
{this.props.children}
</BackdropContext.Provider>
);
}
}
export default WithBackdrop;
I've also exported a 'backdropable' HOC which wraps a component with the context consumer
export const backdropable = Component => (props) => (
<BackdropContext.Consumer>
{value => <Component {...props} backdropContext={value}/>}
</BackdropContext.Consumer>
);
The usage of this API would be as follows: Wrap the part of your Layout/App that you want to potentially have a backdrop, and provide the context to any component that would activate a backdrop. 'Backdropable' is a just a lazy word I used for 'can trigger a backdrop' (not shown here, but I'm using TypeScript and that makes a little more sense as an interface name). Backdropable components can call show() or hide() and not have to worry about other components which may have triggered the backdrop, or about multiple sources of truth about the backdrop's state.
The last problem I had, however, was how to trigger a backdropable components close handler? I decided the WithBackdrop HOC would maintain a list of listeners so that components that need to react when the backdrop is closed by clicking the backdrop (rather than by that backdropable component's close button or something). Here is the modal component I'm using to test this
const modal = (props) => {
props.backdropContext.registerCloseListener(props.onClose);
return (
<div
className={[
classes.Modal,
(props.show ? '' : classes.hidden)
].join(' ')}>
{props.children}
<button onClick={() => {
props.onClose();
props.backdropContext.hide()
}}>Cancel</button>
<button onClick={props.onContinue}>Continue</button>
</div>
)
}
export default backdropable(modal);
As far as I understand, it is best practice to never mutate state. My question is, does pushing to an array maintained in state count as mutating state, and what potentially bad consequences should I expect from this? Should I copy the array into a new array with the new element every single time, or will I only get undefined React behaviour if I try to change the reference of a state member. As far as I understand react only shallowly compares previous and next state to determine re-renders and provides utilities for more complicated comparisons, and so this should be fine right? The reason is that the array copying method triggers a re-render, then the modal tries to re-register the closeListener, then WithBackdrop tries to add it again...and I get an infinite state update loop.
Even if there is nothing wrong with simply pushing to the same array, do you think there is a better way to go about doing this?
Thanks, I sincerely appreciate the efforts anyone who tries to answer this long question.
EDIT: this.setState({ closeListeners: [...this.state.closeListeners, cb]}); results in an infinite state-update loop.
Mutating state in React is when you change any value or referenced object in state without using setState.
As far as I understand, it is best practice to never mutate state. My
question is, does pushing to an array maintained in state count as
mutating state,
Yes
and what potentially bad consequences should I expect from this?
You can expect to change the value of state and not see the ui update.
Should I copy the array into a new array with the new element every
single time,
Yes:
const things = [...this.state.things]
// change things
this.setState({ things })
or will I only get undefined React behaviour if I try to
change the reference of a state member. As far as I understand react
only shallowly compares previous and next state to determine
re-renders and provides utilities for more complicated comparisons,
and so this should be fine right?
It will compare if you call setState and update if necessary. If you do not use setState, it won't even check.
Any changes directly to the state (without setState()) = mutating the state. In your case it is this line:
this.state.closeListeners.push(cb);
As #twharmon mentioned, you change the values in the memory but this does not trigger the render() of your component, but your component will eventually updated from the parent components leading to ugly and hard to debug side effects.
The solution for your problem using destructuring assignment syntax:
this.setState({
closeListeners: [...this.state.closeListeners, cb]
});
PS: Destructuring also helps to keep your code cleaner:
const Backdrop = ({ show, onClose }) => (
show ? <div onClick={onClose} className={classes.Backdrop}></div> : null
);
I'm rewriting a form generator from a class based to a functional based approach. However, in both approaches I'm running into the same problem.
The form receives a field template, and values, loops the field specifications, and renders the appropriate inputs. Each is given it's value and a handler to carry the value in a state object of the form (which later can be submitted).
This works fine while the form is small, but of course those forms are not small and can grow to be quite large and many types of elaborate fields in them. When the form field specification grows, the form slows down to the point where there is a delay between key press and visible input. Interestingly, that delay is very noticable while in development but is much better when compiled to a production build.
I would like to render the form elements as few times as possible and prevent the whole building of the form every time a key is pressed. However, if i pre-generate the fields, the event handlers don't retain the modified values. If I rebuild it on every render - it just slows things down.
A simplified example of this is here:
https://codesandbox.io/s/black-meadow-wqmzt
Note that this example starts by pre-rendering the form content into state and rendering it later. However, you can change the renders return line (in main.js) from :
return <div>{formContent}</div>;
to
return <div>{build()}</div>;
to have the form re-build on each render. You will notice in this case that the build process runs a lot.
Is it possible to pre-render a set of inputs with event handlers attached and retain the event handler's behaviour?
Edit: The slowness of a large form rendering is manifested in the input - typing some text into a text field for example sees a delay between keypress to rendering of the input because each key press triggers a rebuild of the form.
You can just use local state [and handler] to force item update/rerenderings. This of course duplicates data but can be helpful in this case.
export default function Text({ spec, value, onChange }) {
const [val, setVal] = useState(value);
const handleChange = ev => {
onChange(ev);
setVal(ev.target.value);
};
return (
<React.Fragment>
<label>{spec.label}</label>
<input type="text" name={spec.name} value={val} onChange={handleChange} />
</React.Fragment>
);
}
working example
BTW - use key (and not just index value) on outer element of item rendered from an array:
return (
<div key={spec.name}>
<FormElement
spec={spec}
value={values[spec.name] || ""}
onChange={handleChange}
/>
</div>
You should defer eventHandlers and all the behavior to React. I've simplified your code a bit here: https://codesandbox.io/s/solitary-tree-1hxd2. All the changes are in main.js file. Below I explain what I changed and why.
Removed useEffect hook and trigger of build() in there. Your hook was called only once on the first render and wasn't called on re-renders. That caused values don't update when you changed state.
Added unique key to each field. This is important for performance. It let's React internally figure out what field has updated and trigger DOM update only for that input. Your build() function is super fast and don't have side-effects. You shouldn't worry that it is being called more than once. React may call render multiple times and you have no control over it. For heavy functions you can use useMemo (https://usehooks.com/useMemo/) hook, but it isn't the case here, even if you have 50 fields on a form.
Inlined calls to handleChange and fields. That's minor and personal preference.
I don't see any delay in the code now and render called once or twice on each field update. You can't avoid render because it is controlled component: https://reactjs.org/docs/forms.html#controlled-components. Uncontrolled components isn't recommended when using React.
Final code for form component:
export default function Main({ template, data }) {
const [values, setValues] = useState(data);
console.log("render");
return (
<div className="form">
{template.fields.map((spec, index) => {
const FormElement = Fields[spec.type];
const fieldName = spec.name;
return (
<div>
<FormElement
spec={spec}
value={values[fieldName] || ""}
key={spec.name}
onChange={e => {
setValues({
...values,
[fieldName]: e.target.value
});
}}
/>
</div>
);
})}
</div>
);
}