Javascript (React) not dynamically displaying collection - javascript

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.

Related

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

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.

React usestate not updating on first Click Or on First time

Please, See this - https://codesandbox.io/s/morning-grass-z8qrq
https://codesandbox.io/s/blue-flower-wl92u
** the second click, third, fourth, fifth click - menuOpen is true, then again click false - behaves as expected**
let [menuOpen, setMenuOpen] = useState(false);
<div
onClick={() => {
// setMenuOpen(true);
setMenuOpen(!menuOpen); // I's not updated in the First time.
console.log(menuOpen); // First time: false // not updating
>
.......// some code
</div>
Please give me, some answers. I have been trying to solve this problem for Two days. I just can't solve it.
Try this:
export default function App() {
const [menuOpen, setMenuOpen] = useState(false);
return (
<>
<button onClick={() => setMenuOpen(!menuOpen)}>Click</button>
Is menu Open: { menuOpen ? "True": "False"}
</>
);
}
Example demo can be found here.
useState create queues for React core to update the state object of a React component. So the process to update React state is asynchronous for performance reasons. That's why changes don't feel immediate.
Give this a try
setMenuOpen(prevMenuOpenState => !prevMenuOpenState);
or
<div
onClick={() => setMenuOpen(!menuOpen)}
>
I had even this problem in my code. My scenario is as follows:
Its hotel detail page. There is horizontal tab menu of room types. If a hotel has more than 3 types of room, then there is show room button. I am using React Functional components in through the code. hotel detail basic page and room section page are different components created. values are passed to room section components through props.
My problem: When I click to room type further 3rd type, then show room value in function (setSelectedTab()) room component doesn't set at an instant. And hence as function moves further, it doesn't set document.getElementById(id) since show room had not been set. As function (setSelectedTab()) completes in first click it sets the show room to true, but selected tab doesn't set. I had to click 2nd time to set the tab.
solution:
After a long try and error, I settle down to the following:
I declare the function as async and made await the setshowRoom() value.
This solved my complete problem.
async function setSelectedTab(e, data) {
firstScroll += 1;
data >= 2 && await setMenuOpen(true);
if (data >= 0) {
.................
const id = e.href;
const anchor = document.getElementById(id);
..............
..............
}
}
and in room component: showRoom, setshowRoom in useState and calling the setSelectedTab() using props. This solves problem of single click
Drawback: I found delay of 1 second to set this tab.
If anyone have better solution than this without making async await, then please post here.
Thanks.
The Answer is just refactoring the code into class Component without using hooks useState. Using state and setState to update. The Problem will solve.
But If I use useState hooks the problem remains the same Whatever I do with the code.

Best way to keep track of progress bar type state when component unmounts and remounts?

I was rebuilding/refactoring an old web game of mine built with vanilla JS into React + Redux. There is a specific type of button I created which looks like this:
Some buttons have a large cooldown of minutes.
I have successfully rebuilt the button component with hooks but realized a big issue. There are "tabs" in the game which when clicked, would bring you to a different screen. This means that these buttons would unmount and lose their state.
The way I handled this when I was working on the vanilla JS version was to keep all buttons in an array and through the main logic loop, I would loop through all buttons and update their progress.
//some pseudocode
const gameTick = 30
const buttons = [
{
name: 'chop tree',
duration: 2000 // milliseconds,
currentDuration: 1230,
...
}, {...}
]
// gameLoop gets fired every 30milliseconds
const gameLoop = () => {
buttons.forEach(btn => {
if (btn.currentDuration < btn.duration) {
btn.currentDuration += gameTick
}
})
}
setInterval(gameLoop, gameTick)
I would then calculate the percentage and set the width of the progress bar to it.
Using React + Redux though, I can store the buttons into the state but how would I go about handling un-mounts? If a button gets unmounted while in progress, it still needs to be updating in the background so when a user goes back to the tab, the button would be further along or maybe even finished.
I don't see how I could have this information in the reducer while constantly updating the state and passed into the button component.
Another possible idea I had to handle this is to store the timestamp when I change tabs, then when I switch back to the tab with the buttons, I would calculate the time difference and add it up. The problem with this method is you would have to switch to the correct tab to complete something.
Any ideas?

Alter react component state properly

I'm working at a project in which I have to display graphs.
For displaying graphs I'm using vis.js in particular react-vis-network a implementation for using parts of vis.js in React with its stateful approaches.
Initial nodes and edges are loaded before my component is mounted and are passed as props for an initial state.
I attached two eventHandler one direct to a vis.js (the underlying DOM library) and the other at a decorator (button).
The desired/expected behaviour:
A node is removed by clicking either the node or the corresponding button.
Observed behavior:
Sometimes a node is removed and sometimes a node just disappears for a few ms and is reattached but without a decorator/button.
I already tried to start with an empty state and attaching the nodes,edges in componentDidMount() but I got the same result. I hope you can give me a hint.
BTW: Is the way I use to attach components a/the right way?
Every other help to improve my class is appreciated also
class MyNetwork extends Component {
constructor(props){
super(props);
let componentNodes = [];
for (let node of props.nodes){
componentNodes.push(this.createNode(node));
}
let componentEdges = [];
for (let edge of props.edges){
componentEdges.push(this.createEdge(edge));
}
this.state = {nodes:componentNodes,edges:componentEdges};
["_handleButtonClick"].forEach(name => {
this[name] = this[name].bind(this);
});
}
createNode(node){
const Decorator = props => {
return (
<button
onClick={() =>{this._handleButtonClick(props);}}
>
Click Me
</button>
);
};
node.decorator = Decorator;
return React.createElement(Node,{...node})
}
createEdge(edge){
return React.createElement(Edge,{...edge})
}
addNode(node){
this.setState({
nodes: [...this.state.nodes, this.createNode(node)]
})
}
_handleButtonClick(e) {
if(e){
console.log("clicked node has id:" +e.id);
this.removeNode(e.id);
}
}
onSelectNode(params){
console.log(params);
window.myApp.removeNode(params[0]);
}
removeNode(id) {
let array = [...this.state.nodes]; // make a separate copy of the array
let index = array.findIndex(i => i.props.id === id );
array.splice(index, 1);
this.setState({nodes: array});
}
render() {
return (
<div id='network'>
<Network options={this.props.options} onSelectNode={this.onSelectNode}>
{[this.state.nodes]}
{[this.state.edges]}
</Network>
</div>
);
}
}
export default MyNetwork
Before clicking node 2
After clicking node 2
Update 1
I created a live example at stackblitz which isn't working yet caused by other failures I make and can't find.
The components I use are:
Network
Node
Edge
Edge and Node are extending Module
I reworked my MyNetwork component according to some mistakes xadm mentioned.
Components (espacially dynamic) shouldn't be stored in state.
I implemented two new functions nodes() and edges() // line 15-41*
key prop should be used, too.
key is used now // line 18 + 32*
Passed props cannot be modified, you still have to copy initial data
into state. State is required for updates/rerendering.
line 9*
*line numbers in live example I mentioned above
Update 2
I reworked my code and now the life sample is working.
My hope is that I could use the native vis.js events and use them in MyNetwork or other Components I will write.
I read about using 3rd Party DOM event in this question can't figure out to adapt it for my particular case. Because I don't know how to attach the event handler to . Is this possible to do so I can use the event in other components?
Or should I open another question for this topic?
I see several possibilities of problems here.
<Decorator/> should be defined outside of <MyNetwork /> class. Click handler should be passed as prop.
Components (espacially dynamic) shouldn't be stored in state. Just render them in render or by rendering method (called from render). Use <Node/> components with decorator prop, key prop should be used, too.
Passed props cannot be modified, you still have to copy initial data into state. State is required for updates/rerendering. You probably need to remove edge(-es) while removing node.
Create a working example (on stackblitz?) if a problem won't be resolved.
It sounds like React is re-initializing your component when you are clicking a button. Maybe someone smarter than I am can figure out why that is happening...
But since no one has commented on this yet, one way I have handled these sorts of issues is to take the state management out of the display component. You say you are passing the nodes and edges via props from a parent component. You might consider moving the addNode, removeNode, createEdge, and other methods up to the parent component so that it is maintaining the state of the node/edge structure and your display component <MyNetwork/> is only displaying what it receives as props.
Perhaps this isn't an option in your app, but I generally use Redux to remove the state management from the components all together. I find it reduces situations like this where "who should own the state" isn't always clear.

React + Redux: Changing input focus programatically

I have a react application with a 'yes/no' question interface. I'm using redux to manage state.
The app presents a series of input fields, which are added dynamically.
When a question is answered with a 'y' or 'n' keystroke, I want the next input in the series to get focus automatically -- enabling fast data-entry. This is proving surprisingly difficult!
My redux store contains the current question's index - I want this to translate into focus on that input.
/*Input Component*/
const quizQs = ({
questionArray = ["Q1", "Q2", "Q3", "Q4"]
currentQIndex, //From Store
changeQIndex, //Action
}) => {
const _handleKeyDown = (e) => {
if(e.key == 'y' || e.key == 'n'){
//Dispatches Action that increases current currentQIndex'
}
}
//_handleFocus()... for updating currentQIndex if an input is focused by the user
return (
{questionArray.map((q, index) => {
return(
<input
key={index}
onKeyDown={_handleKeyDown}
onFocus={_handleFocus}
type="text"
placeholder={q}
/>
)
})}
)
}
/*Main Component -- Connected to Store*/
class myQuiz extends React.Component {
constructor(props){
super(props);
}
render(){
return(
<div>
<quizQs
currentQIndex = {this.props.currentQIndex}
changeQIndex = {this.props.changeQIndex}
/>
</div>
)}
}
I have tried setting autoFocus = true, if the store's 'currentQIndex' matches the index of that particular question, in the 'quizQs' component. This method is able to focus the specified field when the page first renders, but the focus does not change when the 'currentQIndex' of the store changes.
As I have searched for an answer, React 'refs' + use of a callback would seem to be the way to go, (https://reactjs.org/docs/refs-and-the-dom.html#the-ref-callback-attribute), but I cannot figure out how to set up such 'focus' callback, that responds to changes in the Redux store.
In addition, in order to use Refs, the component must be set up as a class, not an arrow function. AFAIK, it is not good practice to have multiple classes in one file, and it does not seem appropriate to connect so many different components to a redux store.
I'd appreciate help.
Here is simple example of what you are trying to achieve: https://codesandbox.io/s/z64qw3nkzx
I simplified it a bit, but the point is there.
As .focus() is native method on the DOM element, you need a way of tracking those input elements. For that in React there is ref prop. It accepts a function with has one parameter which is the actual DOM element of the component.
You'll see that I put all of the DOM references into an array:
<input
ref={el => this.questionInputElements[index] = el}
key={index}
// all other props
/>
and on key up* find the next element in the array and focus it:
const nextInput = this.questionInputElements[index + 1];
if (nextInput) {
nextInput.focus();
}
* it needs to be on key up (rather than key down) as it would focus the next field before and print y/n in the next input. Try it for fun :)

Categories

Resources