Passing React Hooks in a stepper component - javascript

I'm learning React and I deep dived into hooks as they are elegant, minimize the use of classes, and (at first) looked easy to understand.
Using Material-ui's stepper and checkboxes component. I'm trying to export the values of what was selected and display it on the next step of the stepper. Checked: This But it seems too complicated in my case.
I'm not sure though if I need to pass the state array as props and pass it when returning the component of the 2 checkboxes or map the array and pass it through function?
const [state, setState] = React.useState({
checkedA: false,
checkedB: false,
});
const handleChange = (event) => {
setState({ ...state, [event.target.name]: event.target.checked });
//In my try to export the state I'm passing it to a funcvtion every time a change is made
SelectedBoxes({state})
};
return (
<FormGroup row>
<FormControlLabel
control={
<Checkbox checked={state.checkedA} onChange={handleChange} name="checkedA" />
}
label="Secondary"
/>
<FormControlLabel
control={
<Checkbox
checked={state.checkedB}
onChange={handleChange}
name="checkedB"
color="primary"
/>
}
label="Primary"
/>
</FormGroup>
);
}
//Here is where the function of Selectedboxes is defined
export function SelectedBoxes(checked) {
return (
<div>You selected: {checked}</div>
);
}
function getSteps() {
return ['Checkboxes', 'SelectedBoxes'];
}
function getStepContent(step) {
switch (step) {
case 0:
return <Checkboxes />;
case 1:
return <SelectedBoxes />;
default:
return 'Unknown step';
}
}
export default function HorizontalLinearStepper() {...}
How can I avoid making it so complicated?
Thank you

I solved my problem by moving the state inside the main component.
Then I was easily passing the props from the parent to the child compenent.

Related

SonarQube "Do not define components during render" with MUI/TS but can't send component as prop

I am getting the following error during sonarqube scan:
Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state. Instead, move this component definition out of the parent component “SectionTab” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.
I understand that it says that I should send the component as a prop from the parent, but I don't want to send the icon everytime that I want to use this component, is there another way to get this fixed?
import Select from "#mui/material/Select";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faAngleDown } from "#fortawesome/pro-solid-svg-icons/faAngleDown";
const AngleIcon = ({ props }: { props: any }) => {
return (
<FontAwesomeIcon
{...props}
sx={{ marginRight: "10px" }}
icon={faAngleDown}
size="xs"
/>
);
};
const SectionTab = () => {
return (
<Select
id="course_type"
readOnly={true}
IconComponent={(props) => <AngleIcon props={props} />}
variant="standard"
defaultValue="cr"
disableUnderline
/>
);
};
export default SectionTab;
What can you do:
Send the component as the prop:
IconComponent={AngleIcon}
If you need to pass anything to the component on the fly, you can wrap it with useCallback:
const SectionTab = () => {
const IconComponent = useCallback(props => <AngleIcon props={props} />, []);
return (
<Select
id="course_type"
readOnly={true}
IconComponent={IconComponent}
variant="standard"
defaultValue="cr"
disableUnderline
/>
);
};
This would generate a stable component, but it's pretty redundant unless you need to pass anything else, and not via the props. In that case, a new component would be generated every time that external value changes, which would make it unstable again. You can use refs to pass values without generating a new component, but the component's tree won't be re-rendered to reflect the change in the ref.
const SectionTab = () => {
const [value, setValue] = useState(0);
const IconComponent = useCallback(
props => <AngleIcon props={props} value={value} />
, []);
return (
<Select
id="course_type"
readOnly={true}
IconComponent={IconComponent}
variant="standard"
defaultValue="cr"
disableUnderline
/>
);
};

Prevent a component rendering inside formik

I want to disable the rendering of a component inside react.js formik library
here is an example of code structure I have currently
<formik
initialValue={{
"show":false
}}>
return (
<button name="showbtn" onclick={setFieldValue("show",true)}/>
{values?.show ?
(
<Text>Hello</Text>
) :
null}
<Rerenderedcomponent /> //no prop passed here
)
</formik>
And here is an example of my Rerendered component file
function Rerenderedcomponent()
{
const callingAPI = useCallback(()=>response,[])
}
export default React.memo(Rerenderedcomponent)
Now as I am clicking on the button(name showbtn) formik "show" field value is getting updated but my component(Rerenderedcomponent) is also getting rerendered & hence the api in it is getting called again
I tried by setting enableReinitialize={false} but nothing works
Is it possible to prevent this rerendering of the component(Rerenderedcomponent) on formik field update
PS:- The component should remain inside formik tag only
I prevent the component rerendering inside formik using the below workaround:
Created a new component say (Hello.js) & included the conditonal rendering(that was inside formik tag previously) inside it, like an example shown below
function Hello({show})
{
return(
<>
{show && <Text>Hello</Text>}
</>
)
}
export default React.memo(Hello);
Now I just imported & use the Hello.js component inside formik as shown below
<formik
initialValue={{
"show":false
}}>
return (
<button name="showbtn" onclick={setFieldValue("show",true)}/>
<Hello show={values?.show}/> // Hello.js component
<Rerenderedcomponent /> //this will not rerender now
)
</formik>
Now since the component is already mounted into the DOM the rerendering will not occur on show value change
Also there is one another workaround to resolve this issue just by changing the order of components inside formik tag
<formik
initialValue={{
"show":false
}}>
return (
<button name="showbtn" onclick={setFieldValue("show",true)}/>
<Rerenderedcomponent /> //placed above conditional rendering
{ values?.show ?
(
<Text>Hello</Text>
) :
null
}
)
I moved the rerendered component above the conditional rendering & it resolved the issue
To prevent RerenderedComponent from contacting the api every time. You must define a state in the parent component and pass it to child component:
const [apiData, setApiData] = useState(); // <===
return (
<Formik
initialValues={{ show: false }}
onSubmit={(values) => {}}
>
{({ setValues, values }) => (
<Form>
<button
type="button"
onClick={() => setValues({ show: !values.show })}
>
{values.show ? "hide" : "show"}
</button>
{values.show && (
<Rerenderedcomponent apiData={apiData} setApiData={setApiData} /> // <===
)}
</Form>
)}
</Formik>
);
And in the child component, you can check the existence of apiData and communicate with the api if needed:
function Rerenderedcomponent({ apiData, setApiData }) {
useEffect(() => {
if (!apiData) {
// fetch data here ...
setApiData('<response>');
}
}, []);
return null; // A Redact component must return a value
}

React-Native: cannot update a component while rendering a different component

I've got this simple component Login:
function Login() {
const [isFormValidState, setIsFormValidState] = React.useState(false);
const [credentialState, setCredentialState] = React.useState();
function getFormErrors(errors: any, dirty: boolean) {
setIsFormValidState(!Object.keys(errors).length && dirty);
}
function getFormValues(values: any) {
setCredentialState(values);
}
function doAction() {
//credentialState rest call...
}
return (
<View>
<Text>Login</Text>
<UserCredentialForm getFormValues={getFormValues} getFormErrors={getFormErrors}/>
<Button title='Entra' disabled={!isFormValidState} onPress={doAction}/>
</View>
);
}
Which calls UserCredentialForm:
export default function UserCredentialForm({ getFormValues, getFormErrors }) {
[...]
return (
<Formik innerRef={formRef} validationSchema={formSchema} initialValues={state.form} onSubmit={() => { }}>
{({ handleChange, values, touched, errors, dirty }) => {
getFormValues(values);
getFormErrors(errors, dirty);
return <React.Fragment>
// <TextInput/>....
</React.Fragment>
}}
</Formik>
);
[...]
}
While navigating in my app I've got this error:
react native cannot update a component Login while rendering a
different component Formik.
Then it points me to the error in the setCredentialState inside getFormValues handler in Login component.
I've resolved this using a ref instead of a state, but the problem itself is unsolved to me.
What if I need to update my parent component view after a child event?
The reason for that error is because you call setState inside render(). The call getFormValues(values), which set the state of credentialState is called inside the render.
When the state is set, the Login component get rerendered, thus recreating a new function of getFormValues. As this is used as the prop of UserCredentialForm, it also causes that component to rerender, which causes the render prop inside Formik to calls again, which calls getFormValues causing the state change, causing an infinite loop.
One solution you can try is to add useCallback to the two functions, which prevent them to have new identities after the state changes and consequently change the props, thus creating infinite rerender.
function Login() {
const [isFormValidState, setIsFormValidState] = React.useState(false);
const [credentialState, setCredentialState] = React.useState();
const getFormErrors = useCallback(function getFormErrors(errors: any, dirty: boolean) {
setIsFormValidState(!Object.keys(errors).length && dirty);
}, []);
const getFormValues = useCallback(function getFormValues(values: any) {
setCredentialState(values);
}, []);
function doAction() {
//credentialState rest call...
}
return (
<View>
<Text>Login</Text>
<UserCredentialForm getFormValues={getFormValues} getFormErrors={getFormErrors}/>
<Button title='Entra' disabled={!isFormValidState} onPress={doAction}/>
</View>
);
}
However, there is still an issue and that is the identity of values may not be stable and by setting it to state, it will keep causing rerender. What you want to do is to tell UserCredentialForm not to rerender even when that state changes, and since the state is not used as a prop in UserCredentialForm, you can do that with React.memo.
export default React.memo(function UserCredentialForm({ getFormValues, getFormErrors }) {
[...]
return (
<Formik innerRef={formRef} validationSchema={formSchema} initialValues={state.form} onSubmit={() => { }}>
{({ handleChange, values, touched, errors, dirty }) => {
getFormValues(values);
getFormErrors(errors, dirty);
return <React.Fragment>
// <TextInput/>....
</React.Fragment>
}}
</Formik>
);
[...]
})
I think you got an unlimited loop of rendering,
you setState by getFormValues and the Login component re-render make UserCredentialForm re-render too, so it call getFormValues again and again
You can call getFormValues(values) in a useEffect hook after values of formik update
You are calling getFormValues and getFormErrors inside a callback provided by Formik, that means you cannot wrap them inside an effect hook to suppress this warning or it will violate rules of hooks.
I faced the same issue in React JS and got rid of it by using the following: approach.
I used useFormik hook as an alternate.
and afterwards I refactored Formik form into a new component from where I made state changes to parent component.
This way I neither violated rules of hooks nor got this warning.
Also in the that newly refactored component you might need useFormikContext and useField
Simple example can be like
UserCredentialForm:
// Formik x React Native example
import React from 'react';
import { Button, TextInput, View } from 'react-native';
import { Formik } from 'formik';
export const MyReactNativeForm = ({onSubmit}) => (
<Formik
initialValues={{ email: '' }}
onSubmit={values => onSubmit(values)}
>
{({ handleChange, handleBlur, handleSubmit, values }) => (
<View>
<TextInput
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
/>
<Button onPress={handleSubmit} title="Submit" />
</View>
)}
</Formik>
);
Usage like
function Login() {
function doAction(values) {
console.log(values);
//credentialState rest call...
}
return (
<View>
....
<UserCredentialForm onSubmit={doAction} />
....
</View>
);
}

Strange behavior of material-ui when component is inside a function

I'm trying to split my code in more functions inside react functional components, so it's clearer to read and maintain the code, ie:
import React, { useEffect, useState } from "react";
import { StyledExchangeRateProvider } from "./styles";
import useUpdateRates from "../../hooks/useUpdateRates";
import {
FormControl,
InputLabel,
MenuItem,
Select,
TextField
} from "#material-ui/core";
export default function ExchangeRateProvider() {
// rates hook
const ratesContext = useUpdateRates();
const rates = ratesContext.state.rates;
// update rate on component did mount
useEffect(() => {
async function updateRates() {
if (!rates) {
await ratesContext.updateRate();
}
}
updateRates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// save input values
const [values, setValues] = useState({
country: "VES",
amount: "",
total: ""
});
// change values
const handleChange = event => {
setValues({
...values,
[event.target.name]: event.target.value
});
};
function Amount() {
return (
<TextField
name="amount"
variant="filled"
label="Amount"
onChange={handleChange}
value={values.amount}
fullWidth
/>
);
}
function Country() {
return (
<FormControl fullWidth variant="filled" className="input">
<InputLabel id="Country">Country</InputLabel>
<Select
labelId="Country"
id="country"
name="country"
value={values.country}
onChange={handleChange}
>
<MenuItem value="ARS">Argentina</MenuItem>
<MenuItem value="BRL">Brazil</MenuItem>
<MenuItem value="INR">India</MenuItem>
<MenuItem value="VES">Venezuela</MenuItem>
<MenuItem value="ZAR">South Africa</MenuItem>
</Select>
</FormControl>
);
}
return (
<StyledExchangeRateProvider>
<Amount />
<Country />
</StyledExchangeRateProvider>
);
}
In this code, I'm separating in functions what I'll render in this component, so, ie, the Amount function returns a material-ui TextField. It will return more things, but for simplicity of this question, let's consider just this.
This code renders well, and all elements are shown. However, when I type something in the TextField, the cursor moves away from the TextField each caracter I type.
If I move the <TextField ... /> away from the Amount function and put it directly in the React Component return (switch the <Amount /> for <TextField ... />), the TextField works fine.
I've made a CodeSandBox with the behavior: https://codesandbox.io/s/dreamy-brattain-r4irj
My question is: why does it happen and how to fix it maintaining the code separated in functions?
Move Amount and Country Components outside of ExchangeRateProvider and pass data via props. The Issue is because on each render the functions are being recreated

How to remove an index from array in react on specific click

I'm building a quiz/survey builder, kind of CMS interface which will allow users to add as many questions as they want by clicking the type of question they want.
To start with, my state is set up in the App component as follow:
state = {
components: API.components,
comps: []
}
The admin screen has a selected number of buttons which will activate a question onClick. The question comes from the API.components.
For example, we have:
- Welcome Message
- Thank You Message
- Yes or No
/* 1. Generate a list of buttons with onClick props.addQuestion passing a fixed id via 'i' parameter */
const QuestionTypes = props => {
return (
<div data-name='typequestion'>
{props.details.map((el, i) => <div key={i}><button className="sm" onClick={() => props.addQuestion(i)}>{el.label}</button></div>)}
</div>
)
}
See Menu Questions screenshot: https://www.awesomescreenshot.com/image/3982311/8964261115690780c3f67d390ce08665
onClick, each of these buttons will trigger the 'addQuestion' method, which will pass a fixed ID (key) to the 'selectComponent' function to add the selected component to the comps[] array:
/* onClick, a method 'addQuestion' is called */
/* This method will setState and call a function selectComponent passing a key (id) in order to select the correct component */
addQuestion = key => {
this.setState({
comps: [...this.state.comps, this.selectComponent(key)]
});
}
The selectComponent function has a switch to pass the correct component:
selectComponent = (key) => {
switch(key) {
case 0:
return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 1:
return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 2:
return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
default:
return 'Please select a component from the left side menu'
}
}
This will add an element to comps[] array:
[
0: {element here ..}
1: {element here ..} etc.
]
Here an example of the code for the component:
const ThankYouMessage = props => (
<section className="ui-component-view" data-name="thankyou">
<img src={ThankYouImage} alt="x" />
<h3>Thanks for completing our form!</h3>
<div>
<DeleteButton deleteQuestion={props.deleteQuestion} />
</div>
</section>
);
See Selected Welcome Message Component screenshot: https://www.awesomescreenshot.com/image/3982315/f59e1bf79a31194aa3ee3ad2467658a0
PROBLEM:
As you can see, each component will have a delete button.
While each component is added to the array without issues, I can't find a way to delete ONLY the selected component when I click the delete button.
I've tried to use .filter(), splice() but I don't have the right index for the newly created or updated array list.
I want to use the React way to do it, not jQuery or Javascript-ish.
Example of Delete Button. Please note that the props.index is passing the original clicked button id (key), which will not match the newly comps[] array index:
const DeleteButton = props => (
<span className="deleteButton" onClick={() => props.deleteQuestion(props.index)}>×<small>Delete</small></span>
);
export default DeleteButton;
Here the Delete method:
deleteQuestion = e => {
const comps = [...this.state.comps]
// 2. here I need to add something that will DELETE ONLY the clicked button index for that component
// 3. Update state
this.setState({ comps });
}
Please see the full code for the App component:
class App extends React.Component {
state = {
components: API.components,
comps: [],
multichoice: {}
}
selectComponent = (key) => {
switch(key) {
case 0:
return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 1:
return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 2:
return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
default:
return 'Please select a component from the left side menu'
}
}
addQuestion = key => {
this.setState({
comps: [...this.state.comps, this.selectComponent(key)]
});
}
deleteQuestion = e => {
const comps = [...this.state.comps]
// 2. here I need to add something that will DELETE ONLY the component related to the delete button
// 3. Update state
this.setState({ comps });
}
render() {
return (
<Container>
<Row>
<Col>
<h1>Survey Builder </h1>
</Col>
</Row>
<Row>
<Col lg={3} className="border-right">
<QuestionTypes addQuestion={this.addQuestion} details={this.state.components} />
</Col>
<Col lg={9}>
<Row>
<Col lg={12}>
<QuestionEdit comps={this.state.comps} details={this.state.components} />
</Col>
</Row>
</Col>
</Row>
</Container>
)
}
}
export default App;
You should not keep the components inside the state (cause that breaks the components lifecycle and it is hard to compare them). Instead, just keep the keys:
addQuestion = key => {
this.setState({
comps: [...this.state.comps, key]
});
}
Then inside render(), map the keys to the components:
{this.state.comps.map((key, index) => <SomeComponent remove={() => this.removeQuestion(index)} />)}
Now removeQuestion can simply be:
removeQuestion(index) {
this.setState(({ comps }) => ({ comps: comps.filter((_, i) => i !== index) }));
}
EDITED:
No components should be held on the state, just objects representing the questions.
The comps state should be immutable, that means that each time a question is added or deleted you should create a new Array out of the old one, currently on the state.
Since you're not using any advanced state managers (like Redux, etc.), and nor should you at this point, I would suggest having a data atribute on each question with the question ID on it. Once clicked you can fetch the question ID from the target the click event is carrying and use it to figure out where is the question item reside on the comps state. Once you have it, create a new comps state, by constructing a new Array which does not have that question you've just deleted.
I would also like to recommend not using a switch/case here, since it defies the open/close principle. I think you will find a dictionary approach, where you map the type of the question to the corresponding component, much more scaleable and maintainable.
Hope this helps :)

Categories