Formik Form not updating with onclick - reactjs

I have a custom event handler (when clicked on a button) that injects data in the nested arrays based on a drop down selection. After the event handler added the data the form doesn't update properly. Calling any other event handler on any other input of the form will trigger the form update. The data is set correctly but the form doesnt update properly after the initial onClick event (see code)
I have enableReinitialize set
https://codesandbox.io/s/updateissue-fy72h
import { useEffect, useState } from "react";
import { Formik, Form, Field, FieldArray, TextField } from "formik";
export default function Design() {
const q = {
questions: ["a", "b", "c", "d"],
selectedLanguage: "nl",
};
const [questionnaire, setQuestionnaire] = useState(q);
function addLanguageValue() {
questionnaire.questions.push(questionnaire.selectedLanguage);
setQuestionnaire(questionnaire);
}
return (
<div>
<Formik
initialValues={questionnaire}
enableReinitialize
onSubmit={() => {}}
>
{({ values, handleChange }) => (
<Form>
<div>
<Field as="select" name="selectedLanguage">
<option value="fr">French</option>
<option value="nl">Dutch</option>
<option value="en">English</option>
</Field>
<button
type="button"
className="bg-gradient-to-b"
onClick={(e) => {
addLanguageValue(values);
}}
>
Add language
</button>
</div>
<div>
<FieldArray
name="questions"
render={(rootHelper) => (
<div>
{values.questions.map((value, j) => {
return <div>{value}</div>;
})}
</div>
)}
/>
</div>
</Form>
)}
</Formik>

You're mutating the state object, which causes the problem. If you create a fresh object in addLanguageValue, it works as expected:
function addLanguageValue() {
setQuestionnaire({
...questionnaire,
questions: [...questionnaire.questions, questionnaire.selectedLanguage]
});
}
Sandbox example

Because the onClick function doesn't cause a re-render of the state, you can use the following work around / trick by using an inoffensive function as setStatus to trigger re-render:
<button
type='button'
className='bg-gradient-to-b'
onClick={e => {
addLanguageValue(values);
//Used for rerendering.
props.setStatus('Adding language!');
}}
>
Add language
</button>;

Related

Send values from one browser tab to another in nextjs and populate formik form

I have a form in next with a single input e.g. name on my page. On submit I would like the browser to open a new tab which contains a form with more inputs e.g. name, address, age. How can I pre-populate the name in the new tab?
I was considering the useContext hook but as far as I can tell that does not work across tabs. Do I need to use Redux at this point?
Edit: My attempt using localStorage so far:
// User inputs name which on submit is
// saved to local storage and /register page is opened
const Home: NextPage = () => {
return (
<Formik
initialValues={{ name: '' }}
onSubmit={(values) =>
localStorage.setItem('name', values.name)
}
>
{() => (
<Form>
<div>
<InputBox
type="text"
id="name"
name="name"
htmlFor="name"
label="Name"
placeholder=""
/>
<button
type="submit"
// ? Not sure using window is the correct way to do this in nextjs
onClick={(event) => window.open('/register', '_blank')}
>
Start KYC
</button>
</div>
</Form>
)}
</Formik>
)
import { Field, Form, Formik } from 'formik'
import { NextPage } from 'next'
import React from 'react'
const Register: NextPage = () => {
let nameInit = ''
React.useEffect(() => {
if (typeof window !== 'undefined') {
console.log('You are on the browser')
// 👉️ can use localStorage here
} else {
console.log('You are on the server')
// 👉️ can't use localStorage
}
nameInit = localStorage.getItem('name') ?? ''
console.log(nameInit)
}, [])
return (
<Formik
initialValues={{
name: nameInit,
}}
onSubmit={async (values) => {
console.log(values)
}}
>
{() => (
<Form className="space-y-8 divide-y divide-gray-200">
<div className="sm:col-span-2">
<label
htmlFor="name"
>
First name
</label>
<Field
type="text"
name="name"
id="name"
autoComplete="given-name"
/>
</div>
<div className="flex justify-end">
<button
type="submit"
>
Submit
</button>
</div>
</Form>
)}
</Formik>
)
}
Inspecting the page I can see that the value is being set and the console.log at the end of the useEffect also returns the expected value. But the form is still not being populated... My guess is the form is rendered before the useEffect is executed? When I just write the code in the function body instead of using useEffect it seems that is being executed on the server where localStorage is not available.
You're closer than you think! It works across tabs, but you might be using <link> or <a> tags to navigate.
If you use the built in next/link, your app won't refresh and useContext will work for you. Your app will also take advantage of Next JS's speed optimisations. Example:
import Link from "next/link"
const Nav = () => (
<Link href="gosomewhere">
<a>I'm a Link</a>
</Link>
)

How to open input fields based on radio button selection in React?

I have two radio buttons. Based on which radio button is selected, I need to open input fields to collect some data. How can I achieve this using react?
I tried manipulating the onClick and onChange functions.
Here is the code
import { useState, useEffect } from 'react';
const App = () => {
const [itWorks, setItWorks] = useState(false)
function handleClick(){ SetItWorks(!itworks) }
return (
<div>
{itWorks ?
<>
'It works!'
<input type="text" />
</>: 'It does not'}
<button onClick={() => setItWorks(!itWorks)} >Press me
<input type='radio' aria-selected onClick={handleClick} />
</div>
) }
export default App;

In React with Formik how can I build a search bar that will detect input value to render the buttons?

New to Formik and React I've built a search component that I'm having issues with the passing of the input value and rendering the buttons based on input length.
Given the component:
const SearchForm = ({ index, store }) => {
const [input, setInput] = useState('')
const [disable, setDisable] = useState(true)
const [query, setQuery] = useState(null)
const results = useLunr(query, index, store)
const renderResult = results.length > 0 || query !== null ? true : false
useEffect(() => {
if (input.length >= 3) setDisable(false)
console.log('input detected', input)
}, [input])
const onReset = e => {
setInput('')
setDisable(true)
}
return (
<>
<Formik
initialValues={{ query: '' }}
onSubmit={(values, { setSubmitting }) => {
setInput('')
setDisable(true)
setQuery(values.query)
setSubmitting(false)
}}
>
<Form className="mb-5">
<div className="form-group has-feedback has-clear">
<Field
className="form-control"
name="query"
placeholder="Search . . . . ."
onChange={e => setInput(e.currentTarget.value)}
value={input}
/>
</div>
<div className="row">
<div className="col-12">
<div className="text-right">
<button type="submit" className="btn btn-primary mr-1" disabled={disable}>
Submit
</button>
<button
type="reset"
className="btn btn-primary"
value="Reset"
disabled={disable}
onClick={onReset}
>
<IoClose />
</button>
</div>
</div>
</div>
</Form>
</Formik>
{renderResult && <SearchResults query={query} posts={results} />}
</>
)
}
I've isolated where my issue is but having difficulty trying to resolve:
<Field
className="form-control"
name="query"
placeholder="Search . . . . ."
onChange={e => setInput(e.currentTarget.value)}
value={input}
/>
From within the Field's onChange and value are my problem. If I have everything as posted on submit the passed query doesn't exist. If I remove both and hard code a true for the submit button my query works.
Research
Custom change handlers with inputs inside Formik
Issue with values Formik
Why is OnChange not working when used in Formik?
In Formik how can I build a search bar that will detect input value to render the buttons?
You need to tap into the props that are available as part of the Formik component. Their docs show a simple example that is similar to what you'll need:
<Formik
initialValues={{ query: '' }}
onSubmit={(values, { setSubmitting }) => {
setInput('')
otherStuff()
}}
>
{formikProps => (
<Form className="mb-5">
<div className="form-group has-feedback has-clear">
<Field
name="query"
onChange={formikProps.handleChange}
value={formikProps.values.query}
/>
</div>
<button
type="submit"
disabled={!formikProps.values.query}
>
Submit
</button>
<button
type="reset"
disabled={!formikProps.values.query}
onClick={formikProps.resetForm}
>
</Form>
{/* ... more stuff ... */}
)}
</Formik>
You use this render props pattern to pull formiks props out (I usually call them formikProps, but you can call them anything you want), which then has access to everything you need. Rather than having your input, setInput, disable, and setDisable variables, you can just reference what is in your formikProps. For example, if you want to disable the submit button, you can just say disable={!formikProps.values.query}, meaning if the query value in the form is an empty string, you can't submit the form.
As far as onChange, as long as you give a field the correct name as it corresponds to the property in your initialValues object, formikProps.handleChange will know how to properly update that value for you. Use formikProps.values.whatever for the value of the field, an your component will read those updates automatically. The combo of name, value, and onChange, all handled through formikProps, makes form handing easy.
Formik has tons of very useful prebuilt functionality to handle this for you. I recommend hanging out on their docs site and you'll see how little of your own code you have to write to handle these common form behaviors.

React app showing form in dialog results in a "findDOMNode is deprecated" error

I have a react app, the parent component has a button which when clicked shows a simple dialog with one text input and a submit button. Strict mode is enabled. There are two issues
The form input is set to show an initial value (formik initialValues is set) in the input but that is not being set
When the button is clicked I see an error in the console;
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
The dialog component comes from Material UI and the form comes from Formik. I've created a simple repro here. The error is in the dev tools console. What would cause that error and why is the value not initialising?
Here's the parent component;
import React, { useState } from "react";
import { Button, Typography } from "#material-ui/core";
import ProfileEditor from "./ProfileEditor";
function ProfileManager() {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
return (
<div>
<Typography variant="h5">Profile Manager</Typography>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open profile editor dialog
</Button>
<ProfileEditor open={open} onClose={handleClose}></ProfileEditor>
</div>
);
}
export default ProfileManager;
and the dialog component displayed when the button is clicked in the component above;
import React from "react";
import {
Button,
Dialog,
DialogContent,
LinearProgress,
TextField
} from "#material-ui/core";
import { Formik, Form } from "formik";
interface Props {
open: boolean;
onClose: () => void;
}
function ProfileEditor(props: Props) {
return (
<Dialog open={props.open}>
<DialogContent>
<Formik
// initial value not being displayed !!! 😢
initialValues={{
firstName: "Billy"
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
>
{({ submitForm, isSubmitting }) => (
<Form>
<TextField name="firstName" type="text" label="First name" />
{isSubmitting && <LinearProgress />}
<br />
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
>
Submit
</Button>
<Button variant="contained" onClick={props.onClose}>
Close
</Button>
</Form>
)}
</Formik>
</DialogContent>
</Dialog>
);
}
export default ProfileEditor;
You need to include a value prop to the form field to have it initialized properly.
{({ submitForm, isSubmitting, values }) => (
<Form>
<TextField
name="firstName"
type="text"
label="First name"
value={values.firstName} /* you need this prop */
/>
...
CodeSandBox: https://codesandbox.io/s/so-react-formik-inside-material-dialog-sfq4e?file=/ProfileEditor.tsx
Regarding your issue on the console, I'm not entirely sure at this point what is causing it, but if it bothers you or is causing additional problems, perhaps you can opt to move out of strict mode
<React.Fragment>
<ProfileManager></ProfileManager>
</React.Fragment>

How to focus a Material UI Textfield on button click?

How to focus a Textfield after clicking a button. I tried to use autoFocus but it did not work out: Example sandbox
<div>
<button onclick={() => this.setState({ focus: true })}>
Click to focus Textfield
</button>
<br />
<TextField
label="My Textfield"
id="mui-theme-provider-input"
autoFocus={this.state.focus}
/>
</div>
You need to use a ref, see https://reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-dom-element
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<button onClick={this.focusTextInput}>
Click to focus Textfield
</button>
<br />
<TextField
label="My Textfield"
id="mui-theme-provider-input"
inputRef={this.textInput}
/>
</div>
);
}
}
Updated ref to inputRef for Material-UI v3.6.1.
if you are using a stateless functional component then you can use react hooks.
import React, { useState, useRef } from "react";
let MyFunctional = (props) => {
let textInput = useRef(null);
return (
<div>
<Button
onClick={() => {
setTimeout(() => {
textInput.current.focus();
}, 100);
}}
>
Focus TextField
</Button>
<TextField
fullWidth
required
inputRef={textInput}
name="firstName"
type="text"
placeholder="Enter Your First Name"
label="First Name"
/>
</div>
);
};
First, onclick must be correct like onClick,
then if you want to use it inline your JSX code, it can help.
I tested it with react 16, it works.
<button onClick={() => this.myTextField.focus()}>
Click to focus Textfield
</button>
<TextField
label="My Textfield"
id="mui-theme-provider-input"
inputRef={(el) => (this.myTextField = el)} />
If you are using Material-ui <TextField/> with react functional Component, you can implement focus using inputRef. The trick here is the if condition if(input != null). You can simply do:
<TextField
variant="filled"
inputRef={(input) => {
if(input != null) {
input.focus();
}
}}
/>
Here is an working example for you. CodeSandBox- Material-ui-TextFieldFocus

Resources