In my application I use Redux to manage its state.
The task is when user types query in search panel application dispatches an action with payload and then request goes to API. I want to delay dispatch of an action, so when user types query the request only sends after user stops typing.
I implemented it with debounce function but kinda want to refactor in with useDeferredValue.
And that's when I found it difficult to implement this functional.
import { useState, useMemo } from 'react';
import { FormRow, FormRowSelect } from '.';
import Wrapper from '../assets/wrappers/SearchContainer';
import { useSelector, useDispatch } from 'react-redux';
import { handleChange, clearFilters } from '../features/allJobs/allJobsSlice';
export default function SearchContainer() {
const { isLoading, search, searchStatus, searchType, sort, sortOptions } =
useSelector((store) => store.allJobs);
const { jobTypeOptions, statusOptions } = useSelector((store) => store.job);
const dispatch = useDispatch();
const [localSearch, setLocalSearch] = useState('');
function onHandleSearch(e) {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
}
function omHandleSubmit(e) {
e.preventDefault();
dispatch(clearFilters());
}
const debounce = () => {
let timeoutID;
return (e) => {
setLocalSearch(e.target.value);
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
}, 1000);
};
};
const optimizedDebounce = useMemo(() => debounce(), []);
return (
<Wrapper>
<form className='form'>
<h4>search form</h4>
<div className='form-center'>
<FormRow
type='text'
name='search'
value={localSearch}
handleChange={optimizedDebounce}
/>
<FormRowSelect
labelText='status'
name='searchStatus'
value={searchStatus}
handleChange={onHandleSearch}
list={['all', ...statusOptions]}
/>
<FormRowSelect
labelText='type'
name='searchType'
value={searchType}
handleChange={onHandleSearch}
list={['all', ...jobTypeOptions]}
/>
<FormRowSelect
name='sort'
value={sort}
handleChange={onHandleSearch}
list={sortOptions}
/>
<button
className='btn btn-block btn-danger'
disabled={isLoading}
onClick={omHandleSubmit}
>
clear filters
</button>
</div>
</form>
</Wrapper>
);
}
From the react website this is how it is done:
function App() {
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
return (
<div className="App">
{/* Continue to give the current text to the input */}
<input value={text} onChange={handleChange} />
...
{/* But the list of results is allowed to be "late" in case it is not load yet */}
<MySlowList text={deferredText} />
</div>
);
}
so in your case this might be this:
import { useDeferredValue } from 'react';
export default function SearchContainer() {
const [localSearch, setLocalSearch] = useState('');
const deferredValue = useDeferredValue(localSearch, { timeoutMs: 1000 });
...
return (
...
<FormRow
type='text'
name='search'
value={localSearch}
handleChange={e => setLocalSearch(e.target.value)}
/>
);
}
Related
EssayManager.js
import React from "react";
import { useState, useEffect } from "react";
import CollegeEssays from "./CollegeEssays";
import { getDocs, collection } from "firebase/firestore";
import { db } from "../Firebase";
import TextEditor from "./TextEditor";
function EssayManager() {
const [college, setCollege] = useState();
const [collegeList, setColleges] = useState([]);
const [essayList, setEssays] = useState([]);
useEffect(() => {
if (sessionStorage.getItem("user") == null) {
sessionStorage.setItem(
"previousPage",
"http://localhost:3000/essaymanager"
);
window.location = "http://localhost:3000/signIn";
}
});
useEffect(() => {
async function loadUser() {
const colleges = await getDocs(
collection(db, JSON.parse(sessionStorage.getItem("user")).email)
);
colleges.forEach((doc) => {
if (doc.id != "user") {
setColleges((current) => [
...current,
<div
className="collegedivclass"
id={doc.id}
key={doc.id}
onClick={() => changeCollege(doc.id)}
>
<CollegeEssays name={doc.id} />
</div>,
]);
}
});
}
loadUser();
}, []);
useEffect(() => {
}, [])
useEffect(() => {
async function loadEssays() {
setEssays([]);
const essays = await getDocs(
collection(
db,
JSON.parse(sessionStorage.getItem("user")).email,
`${college}`,
"essays"
)
);
essays.forEach((doc) => {
if (doc.id != "exists") {
setEssays((current) => [
...current,
<li key={doc.id}>
<TextEditor
prompt={doc.id}
text={doc.data().text}
count={doc.data().count}
countType={doc.data().countType}
college={college}
/>
</li>,
]);
}
});
}
if (college != undefined) {
loadEssays();
}
}, [college]);
async function changeCollege(id) {
setCollege(id);
}
return (
<>
{collegeList.length > 0 ? (
<div id="page">
<ul id="listul">{collegeList}</ul>
<h6>*New essays will only show up when the college is clicked again</h6>
<h1 id="collegename">{college}</h1>
<ul>{essayList}</ul>
</div>
) : (
<h1>No Colleges in List</h1>
)}
</>
);
}
export default EssayManager;
CollegeEssays.js
import React from "react";
import AddEssay from "./AddEssay";
import "../UsersEssays.css";
function CollegeEssays(props) {
return (
<>
<div className="useressay">
<h5 id={props.name}>{props.name}</h5>
<AddEssay id="addessaybutton" college={props.name} />
</div>
</>
);
}
export default CollegeEssays;
AddEssay.js
import React from "react";
import Popup from "reactjs-popup";
import { useState } from "react";
import { db } from "../Firebase";
import { setDoc, doc } from "firebase/firestore";
import "../UsersEssays.css";
function AddEssay(props) {
const [prompt, setPrompt] = useState("");
const [countType, setCountType] = useState();
const [count, setCount] = useState("");
const college = props.college;
function handlePrompt(e) {
setPrompt(e.target.value);
}
function handleCountType(e) {
if (
document.getElementById("wordCount").checked ||
document.getElementById("charCount").checked
) {
document.getElementById("countLabel").hidden = false;
document.getElementById("count").hidden = false;
} else {
document.getElementById("countLabel").hidden = true;
document.getElementById("count").hidden = true;
}
setCountType(e.target.value);
}
function handleCount(e) {
setCount(e.target.value);
}
async function handleSubmit(e) {
e.preventDefault();
try {
await setDoc(
doc(
db,
JSON.parse(sessionStorage.getItem("user")).email,
college,
"essays",
prompt
),
{
text: "",
countType: countType,
count: count,
}
);
} catch (e) {
console.error("Error adding document: ", e);
}
}
return (
<Popup id="popup" trigger={<button>Add</button>} modal>
{(close) => (
<div id="form">
<form>
<h3>Add an essay for {college}</h3>
<label>Prompt:</label>
<input type="text" value={prompt} onChange={handlePrompt} />
<br />
<fieldset onChange={handleCountType}>
<legend>Count Type</legend>
<input
type="radio"
name="countType"
id="wordCount"
value="Word Count"
/>
<label>Word Count</label>
<br />
<input
type="radio"
name="countType"
id="charCount"
value="Character Count"
/>
<label>Character Count</label>
<br />
<input
type="radio"
name="countType"
id="noCount"
value="No Count"
/>
<label>No Count</label>
<br />
</fieldset>
<label id="countLabel" hidden>
Count:
</label>
<input
type="text"
value={count}
id="count"
onChange={handleCount}
hidden
/>
<br />
<input
type="submit"
value="Add"
onClick={(event) => {
handleSubmit(event);
close();
}}
/>
</form>
</div>
)}
</Popup>
);
}
export default AddEssay;
I'm using firestore through the google cloud to save the essay information. The essays are pulled from the database and rendered only when the user selects a new college, changing the college variable and triggereing the useeffect. This means that if a user adds a new essay through the essay form, that new essay won't show up until the user selects the college again.
I'm hoping to find a way to cause the useeffect or at least the function in EssayManager.js to trigger when the user submits the form in AddEssay.js.
You can wrap your EssayComponent with a useContext provider. In this provider, you create the essay state. In this way, any component within this context can access the essay state.
https://beta.reactjs.org/reference/react/useContext
I want to submit a form into mongoDB using nodejs API & reactJs. With the exception of the multiple select option, everything is operating as it should be.
Being new to react, I have no idea how to handle the multi select option's onChange method.
Here is what I've tried:
import React, { useState, useRef } from "react";
import { useForm } from "react-hook-form";
import { v4 as uuidv4 } from 'uuid';
import axios from "axios";
import Select from 'react-select';
export default function EventForm(props) {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm();
const form = useRef();
const [loading, setLoading] = useState(false);
const [info, setInfo] = useState("");
const [analysis, setAnalysis] = useState("Undefined");
const [relatedEvent, setRelatedEvent] = useState([]);
const handleInfoChange = (e) => {
setInfo(e.target.value)
}
const handleAnalysisChange = (e) => {
setAnalysis(e.target.value)
}
const handleRelatedEvents = (e) => {
setRelatedEvent(e.target.value)
}
const relatedEventsData = props.data.map(opt => ({ label: opt.info, value: opt._id }));
const onSubmit = async () => {
setLoading(true);
const MySwal = withReactContent(Swal);
const eventData = {
UUID: uuidv4(),
info: info,
analysis: analysis,
relatedEvent: relatedEvent,
}
axios
.post(`${process.env.REACT_APP_PROXY}/api/events`, eventData)
.then((res) => {
console.log(res);
setLoading(false);
MySwal.fire(
"Success!",
"A new event has been saved successfully",
"success"
);
})
.catch((error) => {
console.log(error);
});
};
return (
<div className="panel-body">
<Form
ref={form}
onSubmit={handleSubmit(onSubmit)}
className="form-horizontal"
>
<div className="row">
<div className="col-lg-6">
<div className="mb-3">
<Form.Label>Info</Form.Label>
<Form.Control
type="text"
placeholder="Enter info..."
{...register("info", { required: true })}
value={info}
onChange={handleInfoChange}
/>
{errors.info && (
<ul className="parsley-errors-list filled" id="parsley-id-7" aria-hidden="false">
<li className="parsley-required">This value is required.</li>
</ul>
)}
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<Form.Label>Related events</Form.Label>
<Select
options={relatedEventsData}
value={relatedEvent}
isMulti
onChange={handleRelatedEvents}
/>
</div>
</div>
<div className="col-lg-12">
<Button variant="primary" type="submit">
{loading ? "Saving..." : "Save"}
</Button>
</div>
</div>
</Form>
</div>
);
}
Could you please guide me how to make it work!
Thank you
you can make use of Select onChange event handler which passes the selected options as an array as argument ..
from that you can map over it to get the values as required
something as below:
const handleChange = (opts) => {
const selectedValues = opts.map((opt) => opt.value);
setSelectedValues(selectedValues);
};
Please check the working sample for better clarity 😉 -
i am using react hook form to create a from with multiple pages
i am able to create it and it working with all filed except file-input-type how do i pass i file from another page and finaly pass it to api in the final page
i a have actualy 3 pages i have only added the 1st and final page (fist page has the file input filed and final page has the api to which it must be submitted)
form with file upload field
import { useForm } from "react-hook-form";
export default function Form(props) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
<input style={styles.file} type="file" />
</div>
<input {...register("name", { required: true })}
name="husband"
value={props.getState("name")}
onChange={props.handleChange}
style={styles.input}
type="text"
placeholder="Name"
/>
<input onClick={handleSubmit(props.next)}
type="submit"
value="Next"
/>
form with submit button and api to which it must be uploaded
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const submitValue = (e) => {
// e.preventDefault();
props.state.patient = "true";
const data = props.state;
axios
.post("registration/", data)
.then(() => {
alert("updated data");
window.location = "/clogin";
})
.catch((error) => {
//var my_obj_str = JSON.stringify(error.response.data);
alert(JSON.stringify(error.response.data));
});
};
codesandbox
https://codesandbox.io/s/wizardly-worker-zicnr?file=/src/App.js
There are 2 options
Single form wraps all the steps
You can wrap the <Steps /> component with one form. Make the <Step />s components stateless that accepts onInputChange which will called upon input changes.
onInputChange call setValue to update the form's state.
When the form submitted, you have the file (among other inputs) so you can send it to the server.
import { useEffect } from "react";
import { Steps, StepsProvider, useSteps } from "react-step-builder";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, setValue } = useForm();
useEffect(() => {
register("myFile");
}, [register]);
const onInputChange = (e) => {
setValue(e.target.name, e.target.files[0]);
};
const onSubmit = (data) => {
alert(`Your file name: ${data.myFile.name}, size: ${data.myFile.size}`);
};
return (
<StepsProvider>
<form onSubmit={handleSubmit(onSubmit)}>
<MySteps onInputChange={onInputChange} />
</form>
</StepsProvider>
);
}
const MySteps = ({ onInputChange }) => {
const { next, prev } = useSteps();
return (
<Steps>
<div>
<h1>Step 1</h1>
<input type="file" name="myFile" onChange={onInputChange} />
<button onClick={next}>Next</button>
</div>
<div>
<h1>Step 2</h1>
<button>Submit</button>
</div>
</Steps>
);
};
https://codesandbox.io/s/gifted-wozniak-of14l?file=/src/App.js
Multiple forms in each step
If you want need to have a form inside each step, you can pass the step's data up to the parent when upon step's form submission. Still the parent has the form state so it can handle when all the steps completed
import { useRef } from "react";
import { Steps, StepsProvider, useSteps } from "react-step-builder";
import { useForm } from "react-hook-form";
export default function App() {
const formState = useRef();
const onStepComplete = (data) => {
formState.current = {
...formState.current,
...data
};
};
const onComplete = (data) => {
onStepComplete(data);
const {
name,
myFile: [file]
} = formState.current;
alert(
`Your name: ${name} Your file name: ${file.name}, size: ${file.size}`
);
};
return (
<StepsProvider>
<MySteps onStepComplete={onStepComplete} onComplete={onComplete} />
</StepsProvider>
);
}
const MySteps = ({ onStepComplete, onComplete }) => {
return (
<Steps>
<Step1 onStepComplete={onStepComplete} />
<Step2 onComplete={onComplete} />
</Steps>
);
};
const Step1 = ({ onStepComplete }) => {
const { register, handleSubmit } = useForm();
const { next } = useSteps();
const onSubmit = (data) => {
onStepComplete(data);
next();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>Step 1</h1>
<input type="file" {...register("myFile")} />
<button>Next</button>
</form>
);
};
const Step2 = ({ onComplete }) => {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => {
onComplete(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>Step 2</h1>
<input type="text" {...register("name")} />
<button>Submit</button>
</form>
);
};
https://codesandbox.io/s/condescending-leaf-6gzoj?file=/src/App.js
Passing down the function and tracking changes at parent level is not a great idea. react-hook-form provides a form context option which allows you to do this independently. Such that errors, onChange are handled in each step separately. But when you need to submit the data you can get all of those in the parent component.
Refer to this documentation: https://react-hook-form.com/api/useformcontext/
Note: Many people make the mistake of placing the FormProvider inside the parent component. Remember that FormProvider should wrap the Parent component as well.
I'm trying to set a form field value with useState.
The settings.values.apiKey variable has a value, but the textarea element is empty. What's wrong with my useState?
I tried to change value={apiKey} to value={settings.values.apiKey} and then the value is displayed, but then I can't change the value of the field. When I try to enter something, it always shows the original value.
App.js
const App = () => {
const [apiKey, setApiKey] = useState(settings.values.apiKey)
useEffect(() => {
const getSettings = async () => {
const settingsFromServer = await fetchSettings()
setSettings(settingsFromServer)
}
getSettings()
}, [])
const fetchSettings = async () => {
const res = await fetch('http://127.0.0.1/react-server/get.php')
return await res.json()
}
const saveSettings = async (settings) => {
}
return (
<div className="container">
<Header />
<Settings
settings={settings}
saveSettings={saveSettings}
/>
<Footer />
</div>
);
}
export default App;
Settings.js:
import { useState } from 'react';
const Settings = ({ settings, saveSettings }) => {
const [apiKey, setApiKey] = useState(settings.values.apiKey)
const onSubmit = (e) => {
e.preventDefault()
saveSettings({ apiKey})
}
return (
<div>
<form className='add-form' onSubmit={onSubmit}>
<div className='form-control'>
<label>Api key</label>
<textarea
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
<input type='submit' value='Save settings' className='mt15' />
</form>
</div>
)
}
export default Settings
It looks like by mistake you have used apiKey in App.js file as your state variable. It should be replaced by settings.
const [settings, setSettings] = React.useState();
The above code would make value={apiKey} work properly for textarea in Settings.js file.
And, then onChange will also start working properly.
UPDATE
In addition to the above mentioned error, in case settings props is undefined in Settings.js, this might cause your code to break at useState. So, instead put a check for settings values in useEffect and then set the value. The code would look like this or you can check the codesandbox link here for working demo.
Settings.js
import { useEffect, useState } from "react";
const Settings = ({ settings, saveSettings }) => {
const [apiKey, setApiKey] = useState();
useEffect(() => {
if (settings?.values?.apiKey) {
setApiKey(settings.values.apiKey);
}
}, [settings]);
const onSubmit = (e) => {
e.preventDefault();
saveSettings({ apiKey });
};
return (
<div>
<form className="add-form" onSubmit={onSubmit}>
<div className="form-control">
<label>Api key</label>
<textarea
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
<input type="submit" value="Save settings" className="mt15" />
</form>
</div>
);
};
export default Settings;
App.js
const [settings, setSettings] = useState()
const saveSettings = async (settings) => {
setSettings(settings);
}
Looking for a gentle push in the right direction. Working on a react project and using hooks. Yes, have read documents, but not fully understanding yes.
The ask is about a login routine. Login form works, but does not reflect failed login state until repeat submission; so I am getting previous state, not current.
Tried useEffect...no change. Code follows, and appreciated any constructive feedback:
From the Login form
import React, { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Redirect } from 'react-router-dom'
import getAuthStatus from 'common/cyclone/auth/authenticated.status'
import {
authenticateByLogin,
authenticationSelector,
} from '../services/auth.service'
import Form from 'react-validation/build/form'
import Input from 'react-validation/build/input'
import CheckButton from 'react-validation/build/button'
const required = (value) => {
if (!value) {
return (
<div className="alert alert-danger" role="alert">
This field is required!
</div>
)
}
}
const Login = (props) => {
const form = useRef()
const checkBtn = useRef()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const dispatch = useDispatch()
const { session, hasErrors } = useSelector(authenticationSelector)
useEffect(() => {}, [session, hasErrors])
const onChangeUsername = (e) => {
const username = e.target.value
setUsername(username)
}
const onChangePassword = (e) => {
const password = e.target.value
setPassword(password)
}
const handleLogin = (e) => {
e.preventDefault()
setLoading(true)
form.current.validateAll()
if (checkBtn.current.context._errors.length === 0) {
dispatch(authenticateByLogin(username, password))
.then(() => {
setLoading(false)
if (hasErrors) {
setErrorMessage(session.error.message)
} else {
//props.history.push('/profile')
// window.location.reload()
}
})
.catch(() => {
setLoading(false)
})
} else {
setLoading(false)
}
}
if (session.success) {
//console.log(session.success)
return <Redirect to="/profile" />
}
if (getAuthStatus()) {
return <Redirect to="/profile" />
}
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Form onSubmit={handleLogin} ref={form}>
<div className="form-group">
<label htmlFor="username">Username</label>
<Input
type="text"
className="form-control"
name="username"
value={username}
onChange={onChangeUsername}
validations={[required]}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Input
type="password"
className="form-control"
name="password"
value={password}
onChange={onChangePassword}
validations={[required]}
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block" disabled={loading}>
{loading && (
<span className="spinner-border spinner-border-sm"></span>
)}
<span>Login</span>
</button>
</div>
{hasErrors && (
<div className="form-group">
<div className="alert alert-danger" role="alert">
{errorMessage}
</div>
</div>
)}
<CheckButton style={{ display: 'none' }} ref={checkBtn} />
</Form>
</div>
</div>
)
}
export default Login
From the auth slice:
/** Third Party Libraries */
import { createSlice } from '#reduxjs/toolkit'
import qs from 'qs'
/**Axios Wrapper...nothing fancy here*/
import CycloneAPIInstance from 'common/cyclone/api/api.client'
import CycloneConfig from 'config/base'
/** Main API Server URL */
const API_URL = CycloneConfig.API_URL
const session = JSON.parse(localStorage.getItem('authentication'))
/** Define Initial State */
export const initialState = session
? {
hasErrors: false,
session: session,
}
: {
hasErrors: false,
session: [],
}
/** Define Slice */
const authenticationSlice = createSlice({
name: 'authentication',
initialState,
reducers: {
authenticateUser: (state) => {
state.hasErrors = false
},
authenticateUserSuccess: (state, { payload }) => {
state.hasErrors = false
state.session = payload
console.log(state.session)
},
authenticateUserFailure: (state, { payload }) => {
state.hasErrors = true
state.session = payload
},
deauthenticateUser: (state) => {
state.session = []
},
},
})
export const {
authenticateUser,
authenticateUserSuccess,
authenticateUserFailure,
deauthenticateUser,
} = authenticationSlice.actions
export const authenticationSelector = (state) => state.authentication
export default authenticationSlice.reducer
export function authenticateByLogin(user_name, user_password) {
let requestBody = {
user_name: user_name,
user_password: user_password,
}
let config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
return async (dispatch) => {
dispatch(authenticateUser())
try {
const response = await CycloneAPIInstance.post(
API_URL + 'auth/login',
qs.stringify(requestBody),
config
)
//console.log(response.data.content)
localStorage.setItem('session', JSON.stringify(response.data.content))
dispatch(authenticateUserSuccess(response.data.content))
} catch (error) {
//console.log(JSON.stringify(error.response.data))
dispatch(authenticateUserFailure(error.response.data))
}
}
}
export function deauthenticateByLogout() {
return async (dispatch) => {
dispatch(deauthenticateUser())
localStorage.removeItem('session')
}
}
Try to set the message when hasError change
useEffect(()=> {
if(hasErrors) {
setErrorMessage(session.error.message)
}
}, [hasErrors]);
This is quite some code so I just skipped through to fix the problem and not pick everything apart. My best guess is this part:
dispatch(authenticateByLogin(username, password))
.then(() => {
setLoading(false)
if (hasErrors) {
setErrorMessage(session.error.message)
} else {
//props.history.push('/profile')
// window.location.reload()
}
})
.catch(() => {
setLoading(false)
})
Here you execute the async authentication and then do thing based on "hasError". This "hasError" comes from a hook. We (or at least I) have no clear idea how this is managed. Thing is you cant be 100% sure that hasError is really trustworthy at the point you check it in the then-block. The hook might run just after the next render, which explains why you see the previous state, not the actual one.
Best guess would be to use the response from that async call, because there should be one => authenticate.then((response) => if(response.hasError) ...)
With this check you can set your own error state and your component should be up-to-date
Let me know if this fixes your error.