React: Managing form state with a combination of useReducer and useState - javascript

The component I'm working on is a time input for a form. The form is relatively complex and is generated dynamically, with different fields appearing based on data nested inside other data. I'm managing the state of the form with useReducer, which has worked very well so far. Now that I'm trying to implement a time input component, I'd like to have some basic validation, in particular so I don't get junk non-formatted data into my database. My way of thinking about it was that my database wants one thing: a time, formatted per ISO8601. The UI on the other hand could get that date any number of ways, in my case via an "hour" field, a "minute" field, and eventually an am/pm field. Since multiple fields are being individually validated, and then combined into a single ISO string, my approach was to have useState manage the individual fields and their validation, and then dispatch a single processed ISO string to my centralized state.
To get that to work I tried having the onChange listener of the input fields simply update the local state with a validated input, and then have useEffect "listen" to the local state using its dependency array. So each time local state changes, the useEffect callback dispatches an action with the new input, now processed into an ISO string, in its payload. I was a bit surprised this worked, but I still have a lot to learn about.. all of it. Anyways this worked great, or so I thought..
Since the component in question, TimePiece, is being rendered dynamically (inside nested loops) inside of its parent's parent component, when the user changes the form a bit, the TimePiece component gets rendered with new props and state. But therein lies the rub, every time TimePiece is rendered, it has the same state as every other "instance" of TimePiece (it's a function component though). I used some console.logs to find out it's actually maintaining it's separate state until the moment in renders, when it's then set to the state of the last "instance" that was modified.
My central useReducer state is keyed by a series of ids, so it's able to persist as the user changes the view without a similar problem. It's only the local state which isn't behaving properly, and somewhere on the re-render it sends that state to the central useReducer state and overwrites the existing, correct value...
Something is definitely off, but I keep trying different version and just breaking the thing. At one point it was actually fluttering endlessly between the two states... I thought I would consult the internet. Am I doing this completely wrong? Is it some slight tweak? Should I not have dispatch inside of useEffect with a local state dependency?
In particular, is it strange to combine useState and useReducer, either broadly or in the specific way I've done it?
Here is the code.. if it makes no sense at all, I could make a mock version, but so often the problem lies in the specifics so I thought I'd see if anyone has any ideas. Thanks a bunch.
The functions validateHours and validateMinutes shouldn't have much effect on the operation if you want to ignore those (or so I think.....).
"Mark" is the name of the field state as it lives in memory, e.g. the ISO string.
io is what I'm calling the user input.
function TimePiece({ mark, phormId, facetParentId, pieceType, dispatch, markType, recordId }) {
const [hourField, setHourField] = useState(parseIsoToFields(mark).hour);
const [minuteField, setMinuteField] = useState(parseIsoToFields(mark).minute);
function parseFieldsToIso(hour, minute) {
const isoTime = DateTime.fromObject({ hour: hour ? hour : '0', minute: minute ? minute : '0' });
return isoTime.toISOTime();
}
function parseIsoToFields(isoTime) {
const time = DateTime.fromISO(isoTime);
const hour = makeTwoDigit(`${time.hour}`);
const minute = makeTwoDigit(`${time.minute}`);
return {
hour: hour ? hour : '',
minute: minute ? minute : ''
}
}
function makeTwoDigit(value) {
const twoDigit = value.length === 2 ? value :
value.length === 1 ? '0' + value : '00'
return twoDigit;
}
function validateHours(io) {
const isANumber = /\d/g;
const is01or2 = /[0-2]/g;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setHourField(io)
} else if (io.length === 2) {
if (io[0] === '0') {
setHourField(io);
} else if ( io[0] === '1' && is01or2.test(io[1]) ) {
setHourField(io);
} else {
console.log('Invalid number, too large..');
}
}
} else {
console.log('Invalid characeter..');
}
}
function validateMinutes(io) {
const isANumber = /\d/g;
const is0thru5 = /[0-5]/;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setMinuteField(io);
} else if (is0thru5.test(io[0])) {
setMinuteField(io);
} else {
console.log('Invalid number, too large..');
}
} else {
console.log('Invalid character..');
}
}
useEffect(() => {
dispatch({
type: `${markType}/io`,
payload: {
phormId,
facetId: facetParentId,
pieceType,
io: parseFieldsToIso(hourField, minuteField),
recordId
}
})
}, [hourField, minuteField, dispatch, phormId, facetParentId, pieceType, markType, recordId])
return (
<React.Fragment>
<input
maxLength='2'
value={hourField} onChange={(e) => {validateHours(e.target.value)}}
style={{ width: '2ch' }}
></input>
<span>:</span>
<input
maxLength='2'
value={minuteField}
onChange={(e) => { validateMinutes(e.target.value) }}
style={{ width: '2ch' }}
></input>
</React.Fragment>
)
}
P.S. I made another version which avoids using useState and instead relies on one functions to validate and process the fields, but for some reason it seemed weird, even if it was more functional. Also having local state seemed ideal for implementing something that highlights incorrect inputs and says "invalid number" or whatever, instead of simply disallowing that input.
EDIT:
Live code here:
https://codesandbox.io/s/gv-timepiecedemo-gmkmp?file=/src/components/TimePiece.js
TimePiece is a child of Facet, which is a child of Phorm or LogPhorm, which is a child of Recorder or Log... Hopefully it's somewhat legible.
As suggested I managed to get it working on Codesandbox. I was running a local Node server to route to a Mongo database and didn't know how to set that up, so I just plugged it with a dummy database, shouldn't effect the problem at hand.
To create the problem, in the top left dropdown menu, choose "Global Library", and then click on either "Pull-Up" or "Push-Up". Then in the main window, try typing in to the "Time" field. "Pull-Up" and "Push-Up" are both using this TimePiece component, when you click on the other one, you'll see that the Time field there has changed to be the same as other Time field. The other fields ("Reps", "Load") each maintain their own independent state when you switch between exercises, which is what I'm going for.
If you click "generate record" withs some values in the "Time" field, it makes a "record" which will now show up on the right side. If you click on that it expands into a similar display as the main window. The same problem happens over here with the "Time" field, except the state is independent from the state in the main window. So there are basically two states: one for all Time fields in the main window, one for all Time fields in the right window. Those are being rendered by different parents, Phorm and LogPhorm respectively, maybe that is a hint?
Thanks all!!

Ok, after spending a few hours just trying to trace the data flow from TimePiece back through all the abstraction to "state", and back, and all I can really say is that you've a ton of prop drilling. Almost all your components consume the same, or very similar, props
What I finally found is that TimePiece doesn't unmount when switching between what I guess you are calling Phorms(??), which you've abstracted via a Widget. Once I found what wasn't unmounting/remounting as I'd expect to display the different hours & minutes state the solution was simple: Add a React key corresponding to the Phorm when you switch between pull-ups and push-ups.
Phorm.js
<Widget
key={phormId} // <-- add react key here
mark={marks[facetParentId][piece.pieceType]}
phormId={phormId}
facetParentId={facetParentId}
dispatch={dispatch}
pieceType={piece.pieceType}
markType={markType}
recordId={recordId}
/>
Using a react key here forces React to treat the two exercises widget time pieces as two separate "instances", when you switch between the two the component remounts and recomputes the initial component state in TimePiece.

Related

Javascript (React) not dynamically displaying collection

I'm trying to create a basic multi-stage web form in Javascript. I wanted to accomplish this kind of like the forms section of Full Stack Open, by creating a collection (an array) of questions, then displaying them as labels on my web page, filtered so that only the appropriate ones appeared at certain times. For example, when you first visit the page, it would say "The next few questions will assess your budget situation", then after pressing start - it would transition to the first question, then pressing next, to the second, and so on.
I thought I accomplished this the correct way, by displaying the filtered collection (phase is initialized to 0 outside of the app):
const questionPhase = () => {
if (phase < 3){
phase = phase + 1;
}
else if(phase == 3){
phase = 0;
addToBudget(attribute);
}
console.log(phase);
}
return (
<div>
<h3> Please answer some questions to start</h3>
<ul>
{questions.filter(question => question.phase === phase).map(question =>
{ < label > {question.script}
<input type="text"
question = {question.attribute}
value={question.attribute}
onChange={handleInput}
/>
</label>})}
</ul>
<button onClick={questionPhase}> {phase === 2 ? 'Next' : 'Submit'} </button>
</div>
)
I've done some logging and determined that phase actually is changing every time I press the button at the bottom. But what doesn't happen, is either the questions (and labels) displaying, or the lettering on the buttons changing.
I'm not sure what I've done wrong? I'm certain there's some subtle aspect of the control flow I've missed but I don't know what - I figured that, as explained in FSO, the app is continually being run through every time there's a change by pressing a button or something, which should be the event created by the button press.
Thank you in advance for any help
appendix: here is the questionsList class (from which questions is imported) and the event handler:
import React from 'react'
const questions = [
{
phase: 1 ,
script: 'What is your monthly income?',
attribute: "income"
},
{
phase: 2,
script: 'what are you monthly expenses?',
attribute: "expenses"
}
]
export default questions
const handleInput = (event) => {
const value = event.target.value;
console.log('valued!')
setAttribute({
...attribute,
[event.target.question]: value
})
}
The only thing that will trigger a re-render in React is a change in state, so if a variable's change should cause a re-render, you should stick it in state.
You can change your questionPhase component to a class or function (same thing), and then in the constructor, define
this.state = {phase};
Then this.state.phase will equal whatever phase was when the component instantiated. You'll want to change the rest of the component to use that variable. The correct way to change its value and trigger a re-render is with a call to setState.
That's the way I would do it; although, it would be easier to just call forceUpdate. That will make react re-render. It's not the react way though. The entire purpose of react is to strongly tie the UI to the underlying state, so I wouldn't recommend using forceUpdate.

Conditional Rendering of Arrays in React: For vs Map

I'm new to React and building a calendar application. While playing around with state to try understand it better, I noticed that my 'remove booking' function required a state update for it to work, while my 'add booking' function worked perfectly without state.
Remove bookings: requires state to work
const [timeslots, setTimeslots] = useState(slots);
const removeBookings = (bookingid) => {
let newSlots = [...timeslots];
delete newSlots[bookingid].bookedWith;
setTimeslots(newSlots);
}
Add bookings: does not require state to work
const addBookings = (slotid, tutorName) => {
timeslots[slotid].bookedWith = tutorName;
}
I think that this is because of how my timeslot components are rendered. Each slot is rendered from an item of an array through .map(), as most tutorials online suggest is the best way to render components from an array.
timeslots.map(slot => {
if (!slot.bookedWith) {
return <EmptyTimeslot [...props / logic] />
} else {
return <BookedTimeslot [...props / logic]/>
}
})
So, with each EmptyTimeslot, the data for a BookedTimeslot is available as well. That's why state is not required for my add bookings function (emptyTimeslot -> bookedTimeslot). However, removing a booking (bookedTimeslot -> emptyTimeslot) requires a rerender of the slots, since the code cannot 'flow upwards'.
There are a lot of slots that have to be rendered each time. My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots? This I assume would require state to be used for both the add booking and remove booking function. Like this:
for (let i=0;i<timeslots.length;i++) {
if (!timeslot[i].bookedWith) {
return <EmptyTimeslot />
} else {
return <BookedTimeslot />
}
}
Hope that makes sense. Thank you for any help.
Your addBooking function is bad. Even if it seems to "work", you should not be mutating your state values. You should be using a state setter function to update them, which is what you are doing in removeBookings.
My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots?
Your map approach is not rendering both. For each slot, it uses an if statement to return one component or the other depending on whether the slot is booked. I'm not sure how the for loop you're proposing would even work here. It would just return before the first iteration completed.
This I assume would require state to be used for both the add booking and remove booking function.
You should be using setTimeslots for all timeslot state updates and you should not be mutating your state values. That is true no matter how you render them.

The correct way to store complex form data in React app

I have a complex form in React containing a dozen sub-components. It also includes my own form components that operate by not single values. Some parts of the form use trees of objects as data.
I have the main component (let's call it EditForm) which stores the whole object tree of data. I assume each component of this form should use a specific part of this main object. I should have the ability to store whole form data into JSON and read it back from JSON (and update all sub-components automatically when JSON changed).
The idea was to refresh only those parts of the form that required when I change the main JSON. But the code which handles this behavior is dramatically big and complex. I have to implement comparison functionality into each sub-component and also transfer parts of the main JSON into props of each sub-component.
const [testobj, setTestobj] = useState({'obj1': {......}, 'obj2': {.......}, .......});
function revalidateSub(sub_name, value)
{
let t2 = cloneDeep(testobj);
t2[sub_name] = value;
setTestobj(t2);
}
return (<MySubComponent subobj={testobj['sub1']} ident={"sub1"} onUpdateData={v => revalidateSub('sub1', v)}/>)
(subobj is the piece of data which is controlled by a specific sub-component, onUpdateData is a handler which is called inside the subcomponent each time when it makes any changes to a controlled piece of data)
This scheme, however, does not work as expected. Because revalidateSub() function stores its own copy of testobj and onUpdateData of each component rewriting the changes of other sub-components...
To be honest, I don't quite understand how to store data correctly for this type of React apps. What do you propose? Maybe I should move the data storage out of React components at all? Or maybe useMemo?
Thank you for the shared experience and examples!
Update1: I have coded the example in the Sandbox, please check
https://codesandbox.io/s/thirsty-browser-xf4u0
To see the problem, please click some times to the "Click Me!" button in object 1, you can see the value of obj1 is changing as well as the value of the main object is changing (which is CORRECT). Then click one time to the "Click Me!" of the obj2. You will see (BOOM!) the value of obj1 is reset to its default state. Which is NOT correct.
I think I have solved this by myself. It turned out, that the setState function may also receive the FUNCTION as a parameter. This function should return a new "state" value.
I have moved revalidateSub() function OUT from the EditForm component, so it does not duplicate the state variable "testobj" anymore. And it is now working as expected.
Codebox Page
First of all, I think the idea is correct. In React components, when the value of the props passed from the state or the parent component changes, re-rendering should be performed. Recognized as a value.
Because of this issue, when we need to update a multilayered object or array, we have to use ... to create a new object or array with an existing value.
It seems to be wrong during the copying process.
I don't have the full code, so a detailed answer is not possible, but I think the bottom part is the problem.
let t2 = cloneDeep(testobj);
There are many other solutions, but trying Immutable.js first will help you find the cause of the problem.
Consider also useReducer.
update
export default function EditForm(props) {
const [testobj, setTestobj] = useState({
sub1: { id: 5, v: 55 },
sub2: { id: 7, v: 777 },
sub3: { id: 9, v: 109 }
});
function revalidateSub(sub_name, value) {
let t2 = cloneDeep(testobj);
t2[sub_name] = value;
return t2;
}
console.log("NEW EditForm instance was created");
function handleTestClick() {
let t2 = cloneDeep(testobj);
t2.sub2.v++;
setTestobj(t2);
}
return (
<div>
<div style={{ textAlign: "left" }}>
<Button onClick={handleTestClick} variant="contained" color="secondary">
Click Me to change from main object
</Button>
<p>
Global value:{" "}
<span style={{ color: "blue" }}>{JSON.stringify(testobj)}</span>
</p>
<MyTestObj
subobj={testobj["sub1"]}
ident={"sub1"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub1", v))}
/>
<MyTestObj
subobj={testobj["sub2"]}
ident={"sub2"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub2", v))}
/>
<MyTestObj
subobj={testobj["sub3"]}
ident={"sub3"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub3", v))}
/>
</div>
</div>
);
}
The reason it bursts when you do the above setState updates the local state asynchronously.
The setState method should be thought of as a single request, not an immediate, synchronous execution. In other words, even if the state is changed through setState, the changed state is not applied immediately after the method is executed.
Thus, your code
setTestobj((p) => revalidateSub(p, "sub3", v))
It is safer to use the first parameter of setstate to get the full state like this!
Thanks to this, I was able to gain new knowledge. Thank you.

Proper Way to merge new props into state

I am currently working on calendar component using react-big-calendar.
I currently have one Wrapper for it that pulls data from an database and sends it into the Calendar Component. However, I am making a feature that one can 'send' calendar events to that component from anywhere.
Currently I have only have use cases, which is from the website's built in notification system, there will be a feature to add an external event sent over as a notification to your own calendar.
I might however want to push events to users calendars from another webapplication through my socket server (socket.io).
I am not sure what is the "proper" way to go about it in react, technically.
How I went about it now is that I added
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.propInput === true){
nextProps.reset()
return {events : {...prevState.events, ...nextProps.events}}
} else return null
}
inside my CalendarWrapper, which basically does the following for me:
1) I update the props sent into the wrapper, either from parent component or from the redux store (i will be using the 2nd)
2) This triggers getDrivedStateFromProps and checks if it receives propInput as true, and if that's the case, it merges the new events being sent down as props into the current events of the calendar.
3) it then runs a callback function (nextProps.reset()), to reset propInput to FALSE, which triggers another run of getDrivedStateFromProps, but this time returns null and doesn't cause a new setState.
This was my own dreamed up solution on HOW to push new events to this calendar wrapper from the redux store. It seems unorthodox, but for me it's the only way to have a middle ground with pure redux and everything saved in redux, and having local state of each component.
Is there a technically better and more perfomant way to go about solving this problem?
Thanks in advance.
I would save a timestamp (instead boolean propInput) with new events sent to redux. Comparing 'last update time' is enough to take a decision of update - no need to clear propInput flag (no reset() call ... reducers... etc.).
You don't even need to store this timestamp in state - no need to use getDerivedStateFromProps ("fired on every render, regardless of the cause") - classic solution:
componentDidUpdate(prevProps) {
if (this.props.newEventsTime !== prevProps.newEventsTime) {
setState({events : {...this.state.events, ...this.props.newEvents}})
}
}
More optimal should be
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.newEventsTime !== this.props.newEventsTime) {
setState({events : {...this.state.events, ...this.props.newEvents}});
return false; // no render needed in this pass
}
return (nextState.events === this.state.events) ? false : true;
}
- update on events object ref change.
You have to return a merge of your prevState with your new state, like this:
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.propInput === true){
nextProps.reset()
return {...prevState, ...{
events : nextProps.events
}
}
} else return null
}

Resetting redux state without breaking react components

Been working with redux in a complex react application.
I put some data in redux state and use this data to render a component.
Then, you want to change the page and I do need to reset some fields in the redux state. Those fields were used to render the previous component.
So, if I reset the state before going to the next page, the previous page rerenders and throws errors because data is missing (because of the reset). But I can't reset in the next page, because that page is reached in many flows so it can be difficult to manage when to reset and when not.
How problems like this are managed in react applications?
All the example are simplified to show the problem. in the actual application, there are many fields to reset.
so, page 1
redux state
{field: someValue}
component uses the field value
function myComponent(props) => {
return (props.field.someMappingOperation());
}
Now, when going to page 2, field should be null, so we reset it
redux state
{field: null}
the component above rerenders, throwing an error because of
props.field.someMappingOperation()
and field is null.
What can be done is to load the next page, so this component is not in the page and then reset the state. yet, this becomes very hard to manage, because you the are in page B (suppose clients list with the list saved in redux) and you want to see the details of a client you go to page C, when you press back you don't want to reset again the state. So you add conditions on the reset. But because there are many flows in the application, that page can be reached in many ways. Have conditions for each way is not the best solution I suppose.
Edit:
I would like to add that state reset was not required initially and components aren't designed for that. As the application grew and became enough complex it became necessary. I'm looking for a solution that does not require me to add props value checking in every and each component, like
{this.props.field && <ComponentThatUsesField />}
This is really a lot of work and it's bug-prone as I may miss some fields.
As the application has a lot of pages and manages a lot of different data, the store results to be big enough to not want to have clones of it.
You have to design your components in a more resilient way so they don't crash if their inputs are empty.
For example:
render() {
return (
{ this.props.field && <ComponentUsingField field={this.props.field} />}
)
}
That's a dummy example, but you get the point.
you can change your code like below:
function myComponent(props) => {
const {fields} = props;
return (fields && fields.someMappingOperation());
}
This will not throw any error and is a safe check
You don't need to use conditions or reset your state, your reducer can update it and serve it to your component. You can call an action in componentDidMount() and override the previous values so they will be available in your component. I assume this should be implied for each of your component since you are using a different field values for each.

Categories

Resources