Set props Data to context in formik - reactjs

I have two components inside contact page. One is contact form and another is FormData where I'm showing what the user is typing in forms field. I also have a context where I want to store the form data. But in formik there are built-in props, from there I can access the values but from inside the form. But I want to pass the values outside the form to the context so that I can access this data from the FormData component.
Contact Form
import React, { useContext, useEffect } from "react";
import { useFormik } from "formik";
import { Formik } from "formik";
import { ContentContext } from "../Context";
import { FormData } from "./index";
const ContactForm = () => {
const [content, setContent] = useContext(ContentContext);
// useEffect(() => {
// setContent({
// ...content,
// contactFormData: props,
// });
// }, [props]);
// console.log(content);
return (
<Formik
initialValues={{ email: "" }}
onSubmit={async (values) => {
await new Promise((resolve) => setTimeout(resolve, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{(props) => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset,
} = props;
// setContent({
// ...content,
// contactFormData: props,
// });
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email" style={{ display: "block" }}>
Email
</label>
<input
id="email"
placeholder="Enter your email"
type="text"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={
errors.email && touched.email
? "text-input error"
: "text-input"
}
/>
{errors.email && touched.email && (
<div className="input-feedback">{errors.email}</div>
)}
<button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</button>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
{/* <FormData props={props} />; */}
</form>
);
}}
</Formik>
);
};
export default ContactForm;
context
import React, { useState, createContext } from "react";
export const ContentContext = createContext();
export const ContentProvider = ({ children }) => {
const [content, setContent] = useState({
contactFormData: {
email: "",
},
});
return (
<ContentContext.Provider value={[content, setContent]}>
{children}
</ContentContext.Provider>
);
};
setting the context inside the form causes infinite loop. How do I save props to context?

Related

How to submit Formik when button submit outside tag formik in ReactJs?

I have a component composition as above, the box with brown color is the parent and has a blue child box as the formix container and the right side with the green color is the container where the button is placed. is it possible to submit a form with a button outside the formix tag?
I read the documentation but still not found the solution.
you can handle it using the Formik tag innerRef reference. I added a working demo pls find here.
import React, { useRef } from "react";
import { render } from "react-dom";
import { Formik } from "formik";
import * as Yup from "yup";
import "./helper.css";
const App = () => {
const formRef = useRef();
const handleSubmit = () => {
if (formRef.current) {
formRef.current.handleSubmit();
}
};
return (
<div className="app">
<Formik
innerRef={formRef}
initialValues={{ email: "" }}
onSubmit={async (values) => {
await new Promise((resolve) => setTimeout(resolve, 500));
alert(JSON.stringify(values, null, 2));
}}
validationSchema={Yup.object().shape({
email: Yup.string().email().required("Required")
})}
>
{(props) => {
const {
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit
} = props;
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email" style={{ display: "block" }}>
Email
</label>
<input
id="email"
placeholder="Enter your email"
type="text"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={
errors.email && touched.email
? "text-input error"
: "text-input"
}
/>
{errors.email && touched.email && (
<div className="input-feedback">{errors.email}</div>
)}
</form>
);
}}
</Formik>
<button type="submit" onClick={handleSubmit}>
Submit
</button>
</div>
);
};
render(<App />, document.getElementById("root"));

Setting value on TextField input text become non editable

I am creating a form for updating the data from mongodb I was able to fetch the data and added onchange if the currentId exist then all the data will populate on the form but, my problem is I cannot edit or I cannot type anything on the input to edit the value. I really need your eyes to see something that have missed or missed up. Thanks in advance y'all.
Profile container
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getProfile } from '../../../actions/profile'; //fetch method
import Profile from './Profile';
function Index() {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts);
const currentId = useState(null);
useEffect(() => {
dispatch(getProfile());
}, [currentId, dispatch]);
return (
<div className="custom-container">
{posts.map((profile) => (
<div key={profile._id}>
<Profile profile={profile} currentId={currentId} />
</div>
))}
</div>
);
}
export default Index;
Profile form component
import './Profile.css';
import { React, useState, useEffect } from 'react';
import Button from 'react-bootstrap/Button';
import { TextField } from '#material-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { updateProfile } from '../../../actions/profile';
const Profile = ({ profile, currentId }) => {
const dispatch = useDispatch();
currentId = profile._id;
const [postData, setPostData] = useState(
{
profile: {
name: "",
description: "",
email: "",
number: "",
}
}
);
const post = useSelector((state) => currentId ? state.posts.find((p) => p._id === currentId) : null);
useEffect(() => {
if(post) setPostData(post);
}, [post])
const handleSubmit = (e) => {
e.preventDefault();
if(currentId) {
dispatch(updateProfile(currentId, postData));
}
}
// const [ImageFileName, setImageFileName] = useState("Upload Profile Picture");
// const [fileName, setFileName] = useState("Upload CV");
return (
<form autoComplete="off" noValidate className="form" onSubmit={handleSubmit}>
<TextField
id="name"
name="name"
className="name"
label="Full Name"
variant="outlined"
value={postData.profile.name}
onChange={(e) => setPostData({ ...postData, name: e.target.value })}
/>
<TextField
id="outlined-multiline-static"
label="Multiline"
multiline
rows={4}
variant="outlined"
size="small"
className="mb-3"
name="description"
value={postData.profile.description}
onChange={(e) => setPostData({ ...postData, description: e.target.value })}
fullWidth
/>
<TextField
id="email"
label="Email"
variant="outlined"
size="small"
className="mb-3"
name="email"
value={postData.profile.email}
onChange={(e) => setPostData({ ...postData, email: e.target.value })}
/>
<TextField
id="phone"
label="Phone Number"
variant="outlined"
size="small"
name="phone"
value={postData.profile.number}
onChange={(e) => setPostData({ ...postData, number: e.target.value })}
/>
<Button variant="light" type="submit" className="Save">Save</Button>
</form>
);
}
export default Profile;
If you'd look at the name field for example, you can see the value is postData.profile.name while onChange your setting postData.name.
Try setting the profile object, for example:
onChange={(e) => setPostData({ ...postData, profile: { ...postData.profile, name: e.target.value } })}

How to add field to upload image by Redux Form?

I want to make a submit form by Redux-Form which has a image upload field along with other text fields. I have tried the following approach for image upload and the problem is whenever I try to upload image the form gets re-rendered. How can I do it in a proper way? And another thing is How can I send entire form data (including uploaded image) to Back end? I have used here react,redux-form and material-ui
<Box className={classes.controlTitle}>
Upload Organization Logo
</Box>
<Field
name="logo"
type="file"
component={renderField}
placeholder="Upload your organization logo"
className={classes.field}
/>
I suggest using something like react-uploady. It takes care of the file upload for you and you can use any form/components/ui libraries with it:
import React, { useState, useCallback, useMemo, forwardRef } from "react";
import styled, { css } from "styled-components";
import Uploady, {
useBatchAddListener,
useBatchFinishListener,
useUploadyContext
} from "#rpldy/uploady";
import { asUploadButton } from "#rpldy/upload-button";
const MyUploadField = asUploadButton(
forwardRef(({ onChange, ...props }, ref) => {
const [text, setText] = useState("Select file");
useBatchAddListener((batch) => {
setText(batch.items[0].file.name);
onChange(batch.items[0].file.name);
});
useBatchFinishListener(() => {
setText("Select file");
onChange(null);
});
return (
<div {...props} ref={ref} id="form-upload-button" title={text}>
{text}
</div>
);
})
);
const MyForm = () => {
const [fields, setFields] = useState({});
const [fileName, setFileName] = useState(null);
const uploadyContext = useUploadyContext();
const onSubmit = useCallback(() => {
uploadyContext.processPending({ params: fields });
}, [fields, uploadyContext]);
const onFieldChange = useCallback(
(e) => {
setFields({
...fields,
[e.currentTarget.id]: e.currentTarget.value
});
},
[fields, setFields]
);
const buttonExtraProps = useMemo(
() => ({
onChange: setFileName
}),
[setFileName]
);
return (
<Form>
<MyUploadField autoUpload={false} extraProps={buttonExtraProps} />
<br />
<input
onChange={onFieldChange}
id="field-name"
type="text"
placeholder="your name"
/>
<br />
<input
onChange={onFieldChange}
id="field-age"
type="number"
placeholder="your age"
/>
<br />
<button>
id="form-submit"
type="button"
onClick={onSubmit}
disabled={!fileName}
>
Submit Form
</button>
</Form>
);
};
export default function App() {
return (
<div className="App">
<Uploady
clearPendingOnAdd
destination={{ url: "[upload-url]" }}
multiple={false}
>
<MyForm />
</Uploady>
</div>
);
}
You can check out this sandbox for a complete example.

Formik catch-22

I'm new to React and I've run into a catch 22 situation when using Formik that I seem to have a mental block with. If I use withFormik() then my component can't use hooks in its submit handler.
import React, { useEffect } from "react";
import { Form, Field, withFormik } from "formik";
import { useDatabase, useAlerts } from "./Hooks";
const MyForm = props => {
const { resetForm, dirty, isSubmitting, setSubmitting } = props;
const { loadData, saveData } = useDatabase();
const { success } = useAlerts();
const reset = data => resetForm({ values: data });
useEffect(() => {
loadData().then(data => reset(data));
}, []);
// Problem: How can I execute this on submit?
const handleSubmit = async values => {
await saveData(values);
reset(values);
success("Values saved");
setSubmitting(false);
};
return (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field name="lastName" placeholder="Last name" readOnly={isSubmitting} />
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
);
};
export default withFormik({
mapPropsToValues: () => ({
firstName: "",
lastName: ""
}),
enableReinitialize: true,
handleSubmit: () => {
// Has no access to saveData() and success() hook methods
}
})(MyForm);
https://codesandbox.io/s/sleepy-blackburn-q1mt4?file=/src/MyForm.js
Alternatively if I don't use withFormik then I can't reset the form when my data has loaded because I don't have a reference to resetForm.
import React, { useEffect } from "react";
import { Form, Field, Formik } from "formik";
import { useDatabase, useAlerts } from "./Hooks";
const MyForm = props => {
const { loadData, saveData } = useDatabase();
const { success } = useAlerts();
// Problem: how can I reset the form on data load?
useEffect(() => {
loadData().then(data => resetForm({ values: data }));
}, []);
const initialValues = {
firstName: "",
lastName: ""
};
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
await saveData(values);
resetForm({ values });
success("Values saved");
setSubmitting(false);
};
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ isSubmitting, dirty }) => (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field
name="lastName"
placeholder="Last name"
readOnly={isSubmitting}
/>
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
)}
</Formik>
);
};
export default MyForm;
https://codesandbox.io/s/hardcore-sound-048wf?file=/src/MyForm.js
What would be the best way to do this?
It seems that writing out the problem triggered some mind-cogs to start turning and I came up with a possible solution.
I introduced a context like this...
import React, { useState, useContext } from "react";
import { useDatabase } from "./Hooks";
export const DataContext = React.createContext(null);
export const DataContextConsumer = DataContext.Consumer;
export const useDataContext = () => useContext(DataContext);
export const DataContextProvider = props => {
const [data, setData] = useState({
firstName: "",
lastName: ""
});
const { loadData, saveData } = useDatabase();
const load = async () => setData(await loadData());
const save = async newData => await saveData(newData);
const contextValues = { load, save, data };
return (
<DataContext.Provider value={contextValues}>
{props.children}
</DataContext.Provider>
);
};
export const withDataContext = () => WrappedComponent => props => (
<DataContextProvider>
<WrappedComponent {...props} />
</DataContextProvider>
);
export default {
DataContext,
DataContextConsumer,
DataContextProvider,
useDataContext,
withDataContext
};
And then passed its data to Formik's initialValues. Now Formik gets the new values on load and I can call the save hooks in the submit hander.
import React, { useEffect } from "react";
import { Form, Field, Formik } from "formik";
import { withDataContext, useDataContext } from "./DataContext";
import { useAlerts } from "./Hooks";
const MyForm = () => {
const { load, save, data } = useDataContext();
const { success } = useAlerts();
useEffect(() => {
load();
}, []);
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
await save(values);
resetForm({ values });
success("Values saved " + JSON.stringify(values));
setSubmitting(false);
};
return (
<Formik
initialValues={data}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ isSubmitting, dirty }) => (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field
name="lastName"
placeholder="Last name"
readOnly={isSubmitting}
/>
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
)}
</Formik>
);
};
export default withDataContext()(MyForm);
https://codesandbox.io/s/throbbing-cache-txnit?file=/src/MyForm.js
Perhaps this is a classic case of solving a React problem by lifting state.
You can lift the form inside the render prop for formik up into a new component allowing you to use hooks inside it. Now you can move the loadData effect into the lifted form and know that you are now defining the effect inside of both scopes where you can get access to resetForm via the render props and loadData via the useDatabase hook.
const LiftedForm = ({ isSubmitting, dirty, resetForm }) => {
const { loadData } = useDatabase();
useEffect(() => {
loadData().then((data) => resetForm({ values: data }));
}, [loadData, resetForm]);
return (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field name="lastName" placeholder="Last name" readOnly={isSubmitting} />
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
);
};
and passing the formik bag via the render props right to the new lifted form component.
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{(props) => <LiftedForm {...props} />}
</Formik>
The handleSubmit needs no changing because it already receives everything it needs in the callback and has access to saveData and success via hooks.
const { saveData } = useDatabase();
const { success } = useAlerts();
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
await saveData(values);
resetForm({ values });
success("Values saved");
setSubmitting(false);
};
the codesandbox for that

Fetch and use response to change state in React

I would like to change the state of a component based on the response of a PUT request using react-refetch.
Especially when the response of the PUT is unsuccessful, as is the case with for example a 500 response.
The following example is an example in a form. When a user submits the form it should then fire off a PUT.
If the PUT response is fulfilled, it should reset the form. Otherwise nothing should happen, and the user should be able to retry.
./MyForm.jsx
import React from "react";
import PropTypes from "prop-types";
import { PromiseState } from "react-refetch";
import { Formik, Form, Field, ErrorMessage } from "formik";
import ResetOnSuccess from "./ResetOnSuccess";
const MyForm = ({ settingsPut, settingsPutResponse }) => {
const submitForm = (values, formik) => {
settingsPut(true);
// Here it should pick up the settingsPutResponse,
// and then do the following ONLY if it's successful:
//
// formik.resetForm({ values });
// window.scrollTo(0, 0);
};
return (
<div>
<Formik
noValidate
initialValues={{ name: "", password: "" }}
onSubmit={submitForm}
>
{({ dirty }) => (
<Form>
<ResetOnSuccess settingsPutResponse={settingsPutResponse} />
<Field type="text" name="name" />
<ErrorMessage name="name" component="div" />
<Field type="password" name="password" />
<ErrorMessage name="password" component="div" />
<button type="submit" disabled={dirty !== null ? !dirty : false}>
Submit
</button>
{settingsPutResponse && settingsPutResponse.rejected && (
<p style={{ color: "red" }}>Please try again</p>
)}
</Form>
)}
</Formik>
</div>
);
};
MyForm.propTypes = {
settingsPut: PropTypes.func.isRequired,
settingsPutResponse: PropTypes.instanceOf(PromiseState)
};
MyForm.defaultProps = {
userSettingsPutResponse: null
};
export default MyForm;
I might have a solution by creating a component:
./ResetOnSuccess.jsx
import React, { useEffect, useState } from "react";
import { useFormikContext } from "formik";
import PropTypes from "prop-types";
import { PromiseState } from "react-refetch";
const ResetOnSuccess = ({ settingsPutResponse }) => {
const { values, resetForm } = useFormikContext();
const [success, setSuccess] = useState(false);
useEffect(() => {
if (settingsPutResponse && settingsPutResponse.fulfilled) {
setSuccess(true);
}
}, [settingsPutResponse]);
// only if settingsPutResponse is fulfilled will it reset the form
if (success) {
resetForm({ values });
window.scrollTo(0, 0);
setSuccess(false);
}
return null;
};
ResetOnSuccess.propTypes = { settingsPutResponse: PropTypes.instanceOf(PromiseState) };
ResetOnSuccess.defaultProps = { settingsPutResponse: null };
export default ResetOnSuccess;
And then in ./MyForm.jsx add the reset component:
<Formik
noValidate
initialValues={{ name: "", password: "" }}
onSubmit={submitForm}
>
{({ dirty }) => (
<Form>
<ResetOnSuccess settingsPutResponse={settingsPutResponse} />
<Field type="text" name="name" />
<ErrorMessage name="name" component="div" />
<ResetOnSuccess settingsPutResponse={settingsPutResponse} />
// etc...
But since it's a component that returns a 'null'. This feels a bit like an anti-pattern.
Is there a better way?
I've created an codesandbox example here: https://codesandbox.io/s/quizzical-johnson-dberw

Resources