How do I pass state between functional components? - reactjs

I am currently writing a sign-up page with react-js with react-hooks and I am still learning so please excuse me if this is a very simple question.
I have a signup.js written in functional component with hooks. signup.js imports 'EmailTextField', 'PasswordTextField', 'NameTextField', 'CellPhoneTextField' ... components of which are also written in functional components with hooks.
I made all these textfields as separate components to simplify the code as I have a requirement to have many different checks on each text fields. (and having all these fields in signup.js page makes very long code)
At the end of the process in signup.js, I would like to get state of all it's sub-components (all those textfields) status (whether the user is good to sign in or not.) but I am not sure how to pass a state (or variable) from these textfields up to signup.js.
I know redux can manage state but is there anyway to achieve this without redux?
Thank you.
I've created a CodeSandbox Sample with a minimal sample code to work with.
In here, I use EmailTextfield component in apptest.js. I would like to get isValid state on EmailTextfield from apptest.js so that I can make sure all fields are validated before the user signs up.
'./components/UI/Textfield/EmailTextField.js'
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
export const EmailTextField = props => {
const [value, setValue] = useState("");
const [helperText, setHelperText] = useState(
"Email address will be used as your username."
);
const [isValid, setIsValid] = useState("true");
const handleOnChangeEmailAddress = event => {
// Email Validation logic
if (true) {
setIsValid(true);
} else {
setIsValid(false);
}
};
return (
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="email address"
error={!isValid}
helperText={helperText}
name="email"
autoComplete="email"
margin="dense"
onBlur={handleOnChangeEmailAddress}
/>
</Grid>
);
};
export default EmailTextField;
'aptest.js'
import React from "react";
import CssBaseline from "#material-ui/core/CssBaseline";
import Grid from "#material-ui/core/Grid";
import { makeStyles } from "#material-ui/core/styles";
import Container from "#material-ui/core/Container";
import { EmailTextField } from "./components/UI/Textfield/EmailTextField";
const useStyles = makeStyles(theme => ({
"#global": {
body: {
backgroundColor: theme.palette.common.white
}
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
mainBox: {
// margin: '200px',
width: "550px",
textAlign: "left",
boxShadow: "0 2px 3px #ccc",
border: "1px solid #eee",
padding: "40px 70px 50px 70px",
boxSizing: "border-box"
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3)
}
}));
const Apptest = props => {
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<div className={classes.mainBox}>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<EmailTextField />
</Grid>
</form>
</div>
</div>
</Container>
);
};
export default Apptest;

I have a very crude implementation in mind.
There should be a consistent data-model around the Input fields. This data model should be a single source of truth for the that particular Input field. It should be able to tell whether that particular field is touched, has errors, is pristine, what is it's value and all that stuff.
So let's say you have it like this:
errors: [],
onChange: false,
pristine: true,
touched: false,
value,
Let's call it a StateChangeEvent.
Now each Input field will have a handler for events like change and blur. Here that individual component will update the StateChangeEvent. And these methods will eventually call a callback function with StateChangeEvent as an argument.
That way, the parent will know that there was a change in one of the fields and it can respond accordingly.
In the parent component, to make the Submit Button on the form enabled, we can also have a side effect that will update the overall state of the form. Something like this:
useEffect(() => {
const isValid = !fieldOne.onChange &&
fieldOne.errors.length === 0 &&
fieldOne.value.length !== 0 &&
!fieldTwo.onChange &&
fieldTwo.errors.length === 0 &&
fieldTwo.value.length !== 0 &&
...;
setIsFormValid(isValid);
}, [fieldOne, fieldTwo, ...]);
I'm sure this isn't a complete solution. But I'm sure it would get you started.
UPDATE:
Based on the CodeSandbox that you provided, here's what you can do to make this work:
import ...
const useStyles = makeStyles(theme => ({ ... }));
const Apptest = props => {
const classes = useStyles();
const [isInvalid, setIsInvalid] = useState(true);
const handleStateChange = updatedState => {
console.log("updatedState: ", updatedState);
updatedState.errors.length === 0 ? setIsInvalid(false) : setIsInvalid(true);
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<div className={classes.mainBox}>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<EmailTextField onStateChange={handleStateChange} />
</Grid>
<Button
variant="contained"
color="primary"
disabled={isInvalid}
className={classes.button}
>
Submit
</Button>
</form>
</div>
</div>
</Container>
);
};
export default Apptest;
And in the EmailTextField component:
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
export const EmailTextField = props => {
const { onStateChange } = props;
const [state, setState] = useState({
errors: [],
onChange: false,
pristine: true,
touched: false,
value: null
});
const helperText = "Email address will be used as your username.";
const handleBlur = event => {
// Email Validation logic
const matches = event.target.value.match(
`[a-z0-9._%+-]+#[a-z0-9.-]+.[a-z]{2,3}`
);
if (matches) {
const updatedState = {
...state,
touched: true,
value: event.target.value,
errors: []
};
setState(updatedState);
onStateChange(updatedState);
} else {
const updatedState = {
...state,
touched: true,
value: event.target.value,
errors: ["Please enter a valid email"]
};
setState(updatedState);
onStateChange(updatedState);
}
};
return (
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="email address"
error={state.errors.length > 0}
helperText={state.errors.length > 0 ? state.errors[0] : helperText}
name="email"
autoComplete="email"
margin="dense"
onBlur={handleBlur}
/>
</Grid>
);
};
export default EmailTextField;
Here's a Working CodeSandbox Sample for your ref.

I figured it out, sorry for the late reply. I was asleep.Basically an onBlur() takes a callback, now in this case you need to pass the value in the input box to the callback so you can have access to the value of the user input. The other way is to use an onChange() to track the change and set it so that when the onblur is called you can check the value and then you can perform your validations.
So you just have to pass the target value of the event to the callback like so onBlur={(e) => handleOnChangeEmailAddress(e.target.value)} and then you can have access to the value in method. I have refactored the code you shared in the sandbox. Find below a snippet of what I did.
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
export const EmailTextField = props => {
const [value, setValue] = useState("");
const [helperText, setHelperText] = useState(
"Email address will be used as your username."
);
const [isValid, setIsValid] = useState("true");
const handleOnChangeEmailAddress = value => {
// Email Validation logic
if (!value) {
setIsValid(true);
} else {
setIsValid(false);
}
console.log(isValid)
};
return (
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="email address"
error={!isValid}
helperText={helperText}
name="email"
autoComplete="email"
margin="dense"
onBlur={(e) => handleOnChangeEmailAddress(e.target.value)}
/>
</Grid>
);
};
export default EmailTextField;
I hope it helps.. if you have any problems don't hesitate to ask questions..

From your codesandbox example it looks like you were almost there you just needed to pass your onStateChange function as a prop:
<EmailTextField onStateChange={onStateChange} />
Then implement the onStateChange function in your apptest.js file which will get the updated object.
Check out my example below and open the console, you will see console logs for errors and an "isValid" response if the email is valid.
https://codesandbox.io/s/loving-blackwell-nylpy?fontsize=14

Related

React Hook Form w/ FormProvider and MUI - Not working

I have been trying to abstract my form components for some time now. I took the example of RHF JS and rewrote it in Typescript.
Unfortunately, I do not get any output of the validation results. When I submit the form nothing happens either. My understanding is that I need to pass the FormContext through to the last component.
Can anyone with experience help me?
App.tsx
import { yupResolver } from "#hookform/resolvers/yup";
import { Button, Grid, TextField } from "#mui/material";
import { memo } from "react";
import {
FormProvider,
useForm,
useFormContext,
UseFormReturn,
} from "react-hook-form";
import { object, SchemaOf, string } from "yup";
type FormData = {
name: string;
};
const schema: SchemaOf<FormData> = object().shape({
name: string()
.required("Input is Required")
.matches(/^[a-zA-Z0-9]+$/, "Only alphanummeric"),
});
type TextInputProps = {
methods: UseFormReturn;
name: string;
label: string;
};
// we can use React.memo to prevent re-render except isDirty state changed
const TextFieldMemo = memo(
({ methods, name, label }: TextInputProps) => (
<TextField
label={label}
variant="outlined"
error={!!methods.formState.errors[name]}
helperText={
(methods.formState.errors[name]?.message as unknown as string) ?? ""
}
fullWidth
margin="normal"
{...methods.register(name)}
InputLabelProps={{
shrink: true,
}}
FormHelperTextProps={{
sx: {
position: "absolute",
bottom: "-1.5rem",
},
}}
/>
),
(prevProps, nextProps) =>
prevProps.methods.formState.isDirty ===
nextProps.methods.formState.isDirty &&
prevProps.methods.formState.errors !== nextProps.methods.formState.errors
);
export const TextFieldMemoContainer = () => {
const methods = useFormContext();
return <TextFieldMemo methods={methods} name="name" label="Name" />;
};
export default function App() {
const methods = useForm<FormData>({
resolver: yupResolver(schema),
mode: "onChange",
});
const { isDirty } = methods.formState;
const onSubmit = (form: FormData) => {
console.log(isDirty);
console.log(form);
};
return (
<Grid container>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Grid item>
<TextFieldMemoContainer />
</Grid>
<Grid item>
<Button
type="submit"
disabled={
!methods.formState.isDirty || !methods.formState.isValid
}
>
Submit
</Button>
</Grid>
</form>
</FormProvider>
</Grid>
);
}
Thank you all in advance.
StackBlitz MRE

Is there any way to apply styles to one of the elements in value? TextField from MUI | styled-components

I have the TextField component from MUI and pass a messagEdited useMemo function to the value property.
index.jsx
import React, { useMemo } from "react";
import Box from "#mui/material/Box";
import TextField from "#mui/material/TextField";
export default function BasicTextFields() {
const hasComment = true;
const comment = "this is a comment";
const messageEdited = useMemo(() => {
return hasComment ? `${comment} (edited)` : comment;
}, [comment, hasComment]);
console.log(`messageEdited:`, messageEdited);
return (
<Box component="form" noValidate autoComplete="off">
<TextField
id="outlined-basic"
variant="outlined"
value={messageEdited}
style={{ width: "350px" }}
/>
</Box>
);
}
Is there any way to apply a style to the (edited) text that's in useMemo?
styled-components
export const Edited = styled.span`
color: red;
`;
Edited.displayName = 'Edited';
Attempts
const messageEdited = useMemo(() => {
const EditedText = () => <Edited>edited</Edited>;
return comment.editedText && !editEnabled
? `${commentText} ${EditedText()}} `
: commentText;
}, [comment.editedText, commentText, editEnabled]);
Try this instead, you can use useState for toggle hasComment
import React from "react";
import Box from "#mui/material/Box";
import TextField from "#mui/material/TextField";
export default function BasicTextFields() {
const hasComment = true;
const comment = "this is a comment";
return (
<Box component="form" noValidate autoComplete="off">
<TextField
id="outlined-basic"
variant="outlined"
sx={{ input: { color: hasComment ? "red" : undefined } }}
value={hasComment ? `${comment} (edited)` : comment}
style={{ width: "350px" }}
/>
</Box>
);
}
An InputAdornment can do the trick for you easily.
You can use InputProps for the TextField component and pass it your <Edited/> component conditionally. The InputProps prop takes an object and the endAdornment property will set content visually at the end of the input.
Here's a super basic self-contained example:
import React, { useState } from 'react';
import styled from 'styled-components';
import Box from '#mui/material/Box';
import TextField from '#mui/material/TextField';
// Your Styled Component
const Edited = styled.span`
color: red;
`;
// CommentField can receive an initialValue for a comment as a prop
// If the value is changed, the edited state is set to true and
// the endAdornment value is set to the <Edited /> component
function CommentField({ initialValue = '', name }) {
const [comment, setComment] = useState(initialValue);
const [edited, setEdited] = useState(false);
const handleChange = (e) => {
setComment(e.target.value);
setEdited(true);
};
return (
<TextField
id="outlined-basic"
variant="outlined"
value={comment}
style={{ width: '350px' }}
onChange={handleChange}
name={name}
InputProps={{
endAdornment: edited ? <Edited>edited</Edited> : null,
}}
/>
);
}
export default function App() {
return (
<Box component="form" noValidate autoComplete="off">
<CommentField name={'comment1'} />
</Box>
);
}

MUI TextField breaks Validation Schema when addind Onchange

this is my first thread I hope i do this right,
I am trying to send data for mysql server and it goes well, however when I use TextField from MUI the validation schema starts looking funny and gives error ("require field") even with the text there, However if i submit the data the mysql receives the data.
and here goes the code...
//uses Express send data to mysql
import { useState, useEffect } from 'react'
import axios from "axios"
import { Container, Grid, Typography} from '#material-ui/core';
import { makeStyles } from '#material-ui/core/styles';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import Textfield from './Forms/IndividualComponents/TextField';
import Button from './Forms/IndividualComponents/Button'
const useStyles = makeStyles((theme) => ({
formWrapper: {
marginTop: theme.spacing(10),
marginBottom: theme.spacing(8),
},
}));
const INITIAL_FORM_STATE = {
firstName: ''
};
const FORM_VALIDATION = Yup.object().shape({
firstName: Yup.string("enter your first name")
.required('Required')
});
export default function ImagesUpload() {
const [firstName, setFirstName] = useState()
const [submitform, setSubmitForm] = useState([])
useEffect(() => {
(async() => {
const result = await axios.get('/submitform')
setSubmitForm(result.data.submitform)
})()
}, [])
const submit = async event => {
event.preventDefault()
const data = new FormData()
data.append('firstName', firstName)
const result = await axios.post('/submitForm', data)
setSubmitForm([result.data, ...submitform])
console.log(firstName)
}
const classes = useStyles();
return (
<Grid item xs={12}>
<Container maxWidth="md">
<div className={classes.formWrapper}>
<Formik
initialValues={{
...INITIAL_FORM_STATE
}}
validationSchema={FORM_VALIDATION}
>
<Form onSubmit={submit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
Personal details
</Typography>
</Grid>
<Grid item xs={6}>
<Textfield
name="firstName"
label="First Name"
value={firstName}
onChange={(event => setFirstName(event.target.value) )}
type="text"
/>
</Grid>
<button type="submit">Submit</button>
</Grid>
</Form>
</Formik>
<main>
{submitform.map(post => (
<figure key={post.id}>
<figcaption>{post.firstName}</figcaption>
</figure>
))}
</main>
</div>
</Container>
</Grid>
);
}
the Textfield im using is a costum one with the following code:
import React from 'react';
import { TextField } from '#material-ui/core';
import { useField } from 'formik';
const TextfieldWrapper = ({
name,
...otherProps
}) => {
const [field, mata] = useField(name);
const configTextfield = {
...field,
...otherProps,
fullWidth: true,
};
if (mata && mata.touched && mata.error) {
configTextfield.error = true;
configTextfield.helperText = mata.error;
}
return (
<TextField {...configTextfield} />
);
};
export default TextfieldWrapper;
the problem here is the onChange: If i don't use it I cant setFirstName to send the data, but when I use it it breaks the validation... I tried everything i can find online but this is taking away my sleep for real ! :)

Material-UI Autocomplete, React Hook Form - Changing InputValue in Material UI Autocomplete with Use State in an OnChange?

I've been customising a Material UI Autocomplete within a Controller from React Hook Form, as part of a much larger form, with some difficulty.
The dropdown lists suggestions drawn from the database (props.items, represented here as objects) and if the suggestion is not there, there's the option to add a new one in a separate form with a button from the dropdown. This 'secondComponent' is opened with conditional rendering.
As it gets passed to the second form, the data is stored in state (heldData) and then passed back into the form via React Hook Form's reset, here as reset(heldData).
This updates the value of the form perfectly, as I have an onChange event that sets the value according to what was passed in. React Hook Form handles that logic with the reset and gives the full object to the onChange.
However, I also want to set the InputValue so that the TextField is populated.
In order to create a dynamic button when there are no options ('Add ....(input)... as a guest'), I store what is typed into state as 'texts'. I thought that I could then use the OnChange event to use the same state to update the inputValue, as below. However, when I setTexts from the onChange, the change isn't reflected in the inputValue.
Perhaps this is because the useState is async and so it doesn't update the state, before something else prevents it altogether. If so, it's much simpler than the other code that I have included, but wasn't certain. I have excluded most of the form (over 500 lines of code) but have tried to keep any parts that may be appropriate. I hope that I have not deleted anything that would be relevant, but can update if necessary.
Apologies. This is my first question on Stack Overflow and I'm quite new to React (and coding) and the code's probably a mess. Thank you
**Form**
import React, { useState, useEffect} from "react";
import AutoCompleteSuggestion from "../general/form/AutoCompleteSuggestion";
import SecondComponent from './SecondComponent'
import { useForm } from "react-hook-form";
const items = {
id: 2,
name: "Mr Anderson"
}
const items2 = {
id: 4,
name: "Mr Frog"
}
const defaultValues = {
guest: 'null',
contact: 'null',
}
const AddBooking = () => {
const { handleSubmit, register, control, reset, getValues} = useForm({
defaultValues: defaultValues,
});
const [secondComponent, setSecondComponent] = useState(false);
const [heldData, setHeldData] = useState(null)
const openSecondComponent = (name) => {
setSecondComponent(true)
const data = getValues();
setHeldData(data);
}
useEffect(() => {
!secondComponent.open?
reset(heldData):''
}, [heldData]);
const onSubmit = (data) => {
console.log(data)
};
return (
<>
{!secondComponent.open &&
<form onSubmit={handleSubmit(onSubmit)}
<AutoCompleteSuggestion
control={control}
name="guest"
selection="id"
label="name"
items={items}
openSecondComponent={openSecondComponent}
/>
<AutoCompleteSuggestion
control={control}
name="contact"
selection="id"
label="name"
items={items2}
openSecondComponent={openSecondComponent}
/>
</form>
};
{secondComponent.open?
<SecondComponent/>: ''
};
</>
);
};
And this is the customised AutoComplete:
**AutoComplete**
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete, from "#material-ui/lab/Autocomplete";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";
import { Controller } from "react-hook-form";
import Button from "#material-ui/core/Button";
const AutoCompleteSuggestion = (props) => {
const [texts, setTexts] = useState('');
return (
<>
<Controller
name={props.name}
control={props.control}
render={({ onChange }) => (
<Autocomplete
options={props.items}
inputValue={texts} //NOT GETTING UPDATED BY STATE
debug={true}
getOptionLabel={(value) => value[props.label]}
noOptionsText = {
<Button onClick={()=> props.opensSecondComponent()}>
Add {texts} as a {props.implementation}
</Button>}
onChange={(e, data) => {
if (data==null){
onChange(null)
} else {
onChange(data[props.selection]); //THIS ONCHANGE WORKS
setTexts(data[props.label]) //THIS DOESN'T UPDATE STATE
}}
renderInput={(params) => (
<TextField
{...params}
onChange = { e=> setTexts(e.target.value)}
/>
)}
renderOption={(option, { inputValue }) => {
const matches = match(option[props.label1, inputValue);
const parts = parse(option[props.label], matches);
return (
<div>
{parts.map((part, index) => (
<span
key={index}
style={{ fontWeight: part.highlight ? 700 : 400 }}
>
{part.text}
</span>
))}
</div>
);
}}
/>
)}
/>
</>
);
};
export default AutoCompleteSuggestion;

How to set the height knowing that under input will helper text appear in React MaterialUI v3.7?

As you see below, there is helper text appears under the input if invalid value in input.
But when is valid, there are no helper text.
In process of typing with validation inputs jump. How can I solve this problem?
Rudolf's answer is close to what you need, but the minHeight needs to be applied to the TextField (FormControl if using lower-level components directly) rather than FormHelperText because when the helper text is nil the FormHelperText component isn't displayed at all so the minHeight has no effect.
Here's a working example (I'm using hooks for managing state for my convenience, so this currently only works with the react alpha, but the styling approach is independent of that):
import React, { useState } from "react";
import ReactDOM from "react-dom";
import TextField from "#material-ui/core/TextField";
function App(props) {
const [value, setValue] = useState("");
const errorMessage = value.length === 0 ? "Please enter something" : null;
const helperTextProps = {
error: value.length === 0 ? true : false
};
const textFieldStyle = { minHeight: "5rem" };
return (
<div>
<TextField label="name" style={textFieldStyle} />
<br />
<TextField
label="email"
helperText={errorMessage}
FormHelperTextProps={helperTextProps}
value={value}
onChange={event => setValue(event.target.value)}
style={textFieldStyle}
/>
<br />
<TextField label="other" style={textFieldStyle} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
And here it is in a code sandbox.
If you are using TextField you can pass in extra properties for the helper text:
render() {
const errorMessage = this.state.error ? "error happened" : null;
const helperTextProps = {
error: this.state.error ? true : false,
style: { minHeight: "1rem" }
};
return (
<TextField label="email" helperText={errorMessage} FormHelperTextProps={helperTextProps} />
);
}
Note: I have not tested this code out, it's just to get the gist of it.
It's been a few years, but I stumbled upon this issue myself. I also wanted the minHeight to adjust dynamically based upon the margin prop passed to the TextField component (Here for more info). This is what I came up with - hope it helps some other folks out there.
import React from 'react';
import { TextField, makeStyles } from '#material-ui/core';
const findMinHeight = ({ margin }) =>
(margin && margin.toLowerCase() === 'dense') ? '4em' : '5em';
const inputBoxMinHeight = makeStyles({
minHeightBox: {
minHeight: findMinHeight
}
});
const InputExample = ({ styleVariants }) => {
const { minHeightBox } = inputBoxMinHeight(styleVariants);
return (
<TextField className={ minHeightBox } margin={ styleVariants.margin } />
);
}
export default InputExample;

Resources